まいにち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](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F2eb236e1c72dad329400bd9fddd878df4badf703-2226x1050.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
作成できました。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Fc79900edcbadd1f0edef976182d09195a6f5eba4-2928x538.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
次にAutotaskを作成します。
TriggerはWebhookに、codeにはThirdwebが提供してくれているものをコピペして貼り付けます。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F87c01cc1a0cafe42813e3cdb498158996bd8cd5a-2166x478.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
環境変数に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](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Fa6b6fe04eeb7c75300c31742b8ea2f0d56df9d6e-1656x368.png%3Fw%3D1656%26auto%3Dformat&w=3840&q=75)
まずhttps://dashboard-gasless.biconomy.io/auth/signupにアクセスしてサインインします。 https://dashboard.biconomy.io/とは違うので気をつけてください(私も間違えました😅)。
Register a Dappからネットワークを選んでDappを登録します。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F6960a9d00288126767aff23d5e8798b33b0fc29d-674x752.png%3Fw%3D674%26auto%3Dformat&w=3840&q=75)
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Ffac9000e9b0b43f9fa05d4fef93fa81353a48cb8-1710x1340.png%3Fw%3D1710%26auto%3Dformat&w=3840&q=75)
コントラクトのアドレスとABIを設定してAddボタンを押します。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F7b041e1a9d0178dbb5cb3d77133a4658bbd2392e-1654x1220.png%3Fw%3D1654%26auto%3Dformat&w=3840&q=75)
Dapp APIsタブからAddを押してMethodを選択します。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F4d67cdfebec055fb8dcd32efed30daf1f623a399-1568x1142.png%3Fw%3D1568%26auto%3Dformat&w=3840&q=75)
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](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F62ef02f6496655aa7eb350c06103d7a2b38b31cc-1570x1118.png%3Fw%3D1570%26auto%3Dformat&w=3840&q=75)
Gelato + Safe Relay kit
Gelatoは分散型の各種バックエンドを提供してくれているサービスです。
そのサービスの1つであるGelato Relayを利用してガスレスのDappsが作れるようなので試してみましょう。
こちらの記事を参考に進めていきます。
Safe のwebサイトにいきます。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Fbbf87880c7bc011f1d231ae4c2c7312f34aa187d-2920x1496.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
テストネットのGoerliでSafe Accountを作成します。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F893e4e66aef24735dadc3300566a8aaffa88d643-1660x1050.png%3Fw%3D1660%26auto%3Dformat&w=3840&q=75)
ここでは1/1policyのままでOKです。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F526a559f9d8e4951650f544da679107134e531a4-2430x1454.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Ff31c4497111961d7ceee78589e1fd32849168e4e-2376x1486.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
Goerli上ではガス代なしで作れました(Mainnetでは不明)
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Fb793491c7dc61b58fc1bdb5cacc633fd62413752-2460x1390.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F22dde771d221ec233a11a4b07d41cc3a077a0b50-1734x1082.png%3Fw%3D1734%26auto%3Dformat&w=3840&q=75)
etherscanへ行き、Is this a proxyを押してコントラクトをverifyします。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F03272f648abcf750317d6b7ef00cf7fefc2edcdf-730x352.png%3Fw%3D730%26auto%3Dformat&w=3840&q=75)
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F8944950a1194907940b5db2fe1747e8f3efd3100-2352x936.png%3Fw%3D2000%26auto%3Dformat&w=3840&q=75)
https://relay.gelato.network/balance に行き、Gelatoのアカウントにデポジットします。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Fb5f27559ac22fd3c5089e8bea862584b7a85a1e7-1722x908.png%3Fw%3D1722%26auto%3Dformat&w=3840&q=75)
https://relay.gelato.network/apps でログインしてCreate New Appをします。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F15e536ef83bb2529b970a6ec15dd748eac8b68e0-1762x544.png%3Fw%3D1762%26auto%3Dformat&w=3840&q=75)
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F243d7a7fa6a4eb3a74391a76cb764e575d22bc44-1636x1258.png%3Fw%3D1636%26auto%3Dformat&w=3840&q=75)
記事内のコントラクトをデプロイして設定してみます。
// 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](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2Fb91810f8a9648f4c450ec3f3bc11a721f93574ae-1562x1294.png%3Fw%3D1562%26auto%3Dformat&w=3840&q=75)
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F3331e7891ede0e606b1d0b41753dd8a24cc9dcd5-1836x1238.png%3Fw%3D1836%26auto%3Dformat&w=3840&q=75)
実行してみます。今回はフロントエンドからではなく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.network/balance からも確認できました。
![Image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Ffrqcsxz2%2Fproduction%2F34bfb644aef333ce98698084f95f67a06ea0b384-1606x682.png%3Fw%3D1606%26auto%3Dformat&w=3840&q=75)
おまけ:自分で実装する方法
これらのサービスで行われていることは、当然自力でも一部簡略化して実装することは可能です。動画を紹介しておきます。
まとめ
以上今回は3つの実装方法をご紹介しました。
個人的にはOpenZeppelin Defenderが使い慣れているので今後も使っていくかと思いますが、どの方法を選んでも良いかと思います。
次回はweb3のウォレット接続の先に、既存のweb2のAuth(Firebase Auth)と連携してオフチェーンのユーザーデータ管理をする方法について解説していきます。
弊社Pontechはweb3に関わる開発を得意とするテック企業です。サービス開発に関するご相談はこちらのフォームからお願いいたします。
また、受託開発案件に共に取り組むメンバーを募集しています!ご興味のある方はぜひお話させてください!