まいにちDapps#4 ガスレスで使えるDappsの作り方まとめ

ponta

ponta

· 6 min read
Thumbnail

まいにちDappsの4日目では、ガスレスでmintやtransferが可能なDappsの作成に取り組みます。

3日目ではサービス内ウォレットを作成しましたが、新たに作られたウォレットには元々ガス代となる資金が存在しません。非クリプトユーザーを対象としたユーザーにとっての快適な体験を提供するためには、ガス代を必要とせずにトランザクションを行える、いわゆるガスレスなDappsの開発が求められます。

ガスレスの概要

EVM(Ethereum Virtual Machine)におけるガスレスな体験を実現する方法としては主に二つの方向性が存在します。

一つ目は、今回取り扱うメタトランザクションを利用して実現する方法です。メタトランザクションは、ユーザーがトランザクションを起こす際にガス代を直接支払うのではなく、別のエンティティ(リレイヤーと呼ばれる)がそのコストを負担するという仕組みです。ユーザーはトランザクションに対して秘密鍵で署名を行い、その署名をリレイヤー(Relay Server)に送信します。リレイヤーはガス代を支払い、トランザクションをブロックチェーンネットワークに送信します。次にコントラクトで署名が検証され、トランザクションが実行されます。

二つ目の方法は、独自のサイドチェーンやLayer2のチェーンを立ち上げ、チェーン全体のロジックでガス代を運営体が負担するというやり方です。このテーマについては、今後取り上げる予定です。


Thirdweb × OpenZeppelin Defender

毎度登場のThirdwebとOpenZeppelin Defenderというサービスを用いてDapps上の全てのトランザクションをガスレスにしていきます。

こちらの記事を参考に進めます。

まず、Thirdwebを用いてNFTを用意します。

次に、Next.jsのプロジェクトを立ち上げます。

そして、ThirdwebのProviderを設定します。この部分は以前の記事で既に紹介しているので、ここでは割愛します。

次にOpenZeppelinにログインします。

https://defender.openzeppelin.com

新しくRelayerを作成します。名前とチェーンを入力します。

Image

作成できました。

Image

次にAutotaskを作成します。

TriggerはWebhookに、codeにはThirdwebが提供してくれているものをコピペして貼り付けます。

Image

環境変数にNEXT_PUBLIC_OPENZEPPELIN_URL=NEXT_PUBLIC_OPENZEPPELIN_URL=https://api.defender.openzeppelin.com/autotasks/secret-stuffのようにAutotaskのwebhook urlを設定します。

ThirdwebのProviderに設定すればOKです。

<ThirdwebProvider
      activeChain="mumbai"
      supportedWallets={[
        paperWallet({
          paperClientId: process.env.NEXT_PUBLIC_PAPER_CLIENT_ID || "",
        }),
      ]}
      sdkOptions={{
        gasless: {
          openzeppelin: {
            relayerUrl: process.env.NEXT_PUBLIC_OPENZEPPELIN_URL || "",
          },
        },
      }}
    >
      {children}
    </ThirdwebProvider>

テストしてみましょう。

"use client";

import { Web3Button } from "@thirdweb-dev/react";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <Web3Button
        contractAddress="0xE71eE93Ad57c8b355e1bCDFe435B05673d8B4930"
        action={(contract) => {
          contract.erc721.claim(1);
        }}
      >
        Claim
      </Web3Button>
    </main>
  );
}

無事mintが行えることが確認できました!

Thirdweb × Biconomy

次にBiconomyというサービスを使って実装していきましょう。

Thirdweb曰く、特定の機能をガスレスにするにはDefenderよりBiconomyが適しているそうです。「NFTのmintだけガス代を負担するがTransferなどそれ以外の機能はユーザーがガス代を入手する必要がある」というような設定が可能ということですね。

Image

まずhttps://dashboard-gasless.biconomy.io/auth/signupにアクセスしてサインインします。 https://dashboard.biconomy.io/とは違うので気をつけてください(私も間違えました😅)。

Register a Dappからネットワークを選んでDappを登録します。

ImageImage

コントラクトのアドレスとABIを設定してAddボタンを押します。

Image

Dapp APIsタブからAddを押してMethodを選択します。

Image

Next.jsのプロジェクトを立ち上げてThirdwebの設定をします。

providersに以下の設定をします。

"use client";

import { ThirdwebProvider, paperWallet } from "@thirdweb-dev/react";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThirdwebProvider
      activeChain="mumbai"
      supportedWallets={[
        paperWallet({
          paperClientId: process.env.NEXT_PUBLIC_PAPER_CLIENT_ID || "",
        }),
      ]}
      sdkOptions={{
        gasless: {
          biconomy: {
            apiKey: process.env.NEXT_PUBLIC_BICONOMY_API_KEY,
            apiId: process.env.NEXT_PUBLIC_BICONOMY_API_ID,
          },
        },
      }}
    >
      {children}
    </ThirdwebProvider>
  );
}

特定のNFTを持っている人にだけガスレスにする、ということもできるようでした!

Image

Gelato + Safe Relay kit

Gelatoは分散型の各種バックエンドを提供してくれているサービスです。

そのサービスの1つであるGelato Relayを利用してガスレスのDappsが作れるようなので試してみましょう。

こちらの記事を参考に進めていきます。

Safe のwebサイトにいきます。

Image

テストネットのGoerliでSafe Accountを作成します。

Image

ここでは1/1policyのままでOKです。

ImageImage

Goerli上ではガス代なしで作れました(Mainnetでは不明)

ImageImage

etherscanへ行き、Is this a proxyを押してコントラクトをverifyします。

ImageImage

https://relay.gelato.network/balance に行き、Gelatoのアカウントにデポジットします。

Image

https://relay.gelato.network/apps でログインしてCreate New Appをします。

ImageImage

記事内のコントラクトをデプロイして設定してみます。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.14;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract GelatoNFT is ERC721, ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    string public constant gelatoInitialURI = "https://bafkreid6qtmeanbpfygu3e5bbw5fxfexas4dimaigsm6nbigji3naxtz74.ipfs.nftstorage.link/";

    constructor() ERC721("GelatoNFT", "GNFT") {}

   function mintGelato(address to) public {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(to, newItemId);
        _setTokenURI(newItemId, gelatoInitialURI);
    }

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}
Image

Image

実行してみます。今回はフロントエンドからではなくnode.jsのバックエンドから実行しますが、フロントからでも同様にできるはずです。

import { ethers } from "ethers";
import { GelatoRelayPack } from "@safe-global/relay-kit";
import Safe, {
  EthersAdapter,
  getSafeContract,
} from "@safe-global/protocol-kit";
import {
  MetaTransactionData,
  MetaTransactionOptions,
  OperationType,
  RelayTransaction,
} from "@safe-global/safe-core-sdk-types";
import dotenv from "dotenv";

dotenv.config();

const nftContractAddress = "0x180B60552B6f7A36DC09c63089D32674A99559D1";
const nftContractAbi = [
  {
    inputs: [],
    stateMutability: "nonpayable",
    type: "constructor",
  },
  中略
];

const RPC_URL = "https://rpc.ankr.com/eth_goerli";
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const safeAddress = "0x4de4cD64FBd9C0A44414C32528F554c2d1906293"; // Safe from which the transaction will be sent. Replace with your Safe address
const chainId = 5;

// Get Gelato Relay API Key: https://relay.gelato.network/
const GELATO_RELAY_API_KEY = process.env.GELATO_RELAY_API_KEY!;

// Usually a limit of 21000 is used but for smart contract interactions, you can increase to 100000 because of the more complex interactions.
const gasLimit = "1000000";

const nftContract = new ethers.Contract(
  nftContractAddress,
  nftContractAbi,
  provider
);

// Create a transaction object
const safeTransactionData: MetaTransactionData = {
  to: nftContractAddress,
  data: nftContract.interface.encodeFunctionData("mintGelato", [
    "0x68B38f944d2689537f8ed8A2F006b4597eE42218",
  ]),
  value: "0",
  operation: OperationType.Call,
};
const options: MetaTransactionOptions = {
  gasLimit,
  isSponsored: true,
};

// Create the Protocol and Relay Kits instances

async function relayTransaction() {
  const ethAdapter = new EthersAdapter({
    ethers,
    signerOrProvider: signer,
  });

  const safeSDK = await Safe.create({
    ethAdapter,
    safeAddress,
  });

  const relayKit = new GelatoRelayPack(GELATO_RELAY_API_KEY);

  // Prepare the transaction
  const safeTransaction = await safeSDK.createTransaction({
    safeTransactionData,
  });

  const signedSafeTx = await safeSDK.signTransaction(safeTransaction);
  const safeSingletonContract = await getSafeContract({
    ethAdapter,
    safeVersion: await safeSDK.getContractVersion(),
  });

  const encodedTx = safeSingletonContract.encode("execTransaction", [
    signedSafeTx.data.to,
    signedSafeTx.data.value,
    signedSafeTx.data.data,
    signedSafeTx.data.operation,
    signedSafeTx.data.safeTxGas,
    signedSafeTx.data.baseGas,
    signedSafeTx.data.gasPrice,
    signedSafeTx.data.gasToken,
    signedSafeTx.data.refundReceiver,
    signedSafeTx.encodedSignatures(),
  ]);

  const relayTransaction: RelayTransaction = {
    target: safeAddress,
    encodedTransaction: encodedTx,
    chainId: chainId,
    options,
  };
  const response = await relayKit.relayTransaction(relayTransaction);

  console.log(
    `Relay Transaction Task ID: https://relay.gelato.digital/tasks/status/${response.taskId}`
  );
}

relayTransaction();

実行結果

https://relay.gelato.digital/tasks/status/0x4d2deb76c40d4c8903380cdf00cab3331884c504ea4fd1cf16fea7f14ffab902

https://relay.gelato.network/balance からも確認できました。

Image

おまけ:自分で実装する方法

これらのサービスで行われていることは、当然自力でも一部簡略化して実装することは可能です。動画を紹介しておきます。

まとめ

以上今回は3つの実装方法をご紹介しました。

個人的にはOpenZeppelin Defenderが使い慣れているので今後も使っていくかと思いますが、どの方法を選んでも良いかと思います。

次回はweb3のウォレット接続の先に、既存のweb2のAuth(Firebase Auth)と連携してオフチェーンのユーザーデータ管理をする方法について解説していきます。

弊社Pontechはweb3に関わる開発を得意とするテック企業です。サービス開発に関するご相談はこちらのフォームからお願いいたします。

また、受託開発案件に共に取り組むメンバーを募集しています!ご興味のある方はぜひお話させてください!

ponta

About ponta

2019年からEthereumを中心にDapp開発に従事。スキーとNBAとTWICEが好き。

Copyright © 2023 Pontech.Inc