この記事はEthereumアドベントカレンダー2023 (https://qiita.com/advent-calendar/2023/ethereum) 21日目の投稿です。
はじめに
皆さん、こんにちは!
久しぶりの記事投稿&Ethereumアドベントカレンダー2023への投稿になります!!
2024年に向けて皆さんへ一つでも多くの知識を共有できれば幸いです。
今回のテーマは、Web3Auth と Biconomy を使ったAAアプリの作り方についての解説記事になります!!!
それでは本編スタートです!!
Tips
まずこの記事を読む上で必要になってくる用語の簡単な解説だけしたいと思います!
今回は3つ取り上げます!
-
AA(アカウントアブストラクション)
イーサリアムの創始者であるヴィタリックさんの長年の夢でもあったものです!!
AAは、プロトコルレベルでの改修無しにEOAとコントラクトウォレットの差異を埋めるためのアーキテクチャです。
2022年後半あたりからこのAAを実装しやすくなり、ハッカソンなどでもこのアーキテクチャを採用したプロダクトが爆発的に増えました。ブロックチェーンアプリケーションの可能性を大きく広げるものとして今後も注目されるでしょう。
上位入賞しているプロダクトの多くが当然のごとくAAを利用しています!!
このAAを実装するための規格として最もよく知られているが、ERC4337ですね!(スマートコントラクト側)
下記記事などでもまとめられているので気になる方はそちらもご覧ください!!
-
Web3Auth
Web3Authは、Web3アプリケーションの認証プロセスを簡単にするサービスです。
Ethereumブロックチェーンを利用してユーザー認証を行い、サーバーレスで安全な方法を提供します。Metamaskのように従来の面倒な秘密鍵保管を行う必要なくユーザーが資産の管理を行えるセルフカストディウォレットの作成を可能にするサービスです。
なんだか難しいことを書きましたが端的に言うと 「MetamaskがなくてもWeb3の世界にアクセスできるUXが提供できるようになります!!」
現在、新規ユーザーをWeb3の世界に案内するためにウォレットなどについての知識については自己学習が基本的に必要となりますが、これだとマス受けはまず難しいでしょう。
Web2アプリでメガヒットしているものはどれも直感的に分かりやすいUXを提供してします。それを可能にするのがWeb3Authです!!
下記記事などでもまとめられているので気になる方はそちらもご覧ください!!
-
Biconomy
上述したAAを実装するための便利なSDKやAPIを提供しているAA特化のインフラプロバイダーです!!
他に存在するプレイヤーとしてはStackUp社やBlocknative社が有名ですね。ペイマスターなどAAを実装するために必須となる要素がすぐに使えるので開発効率がものすごく上がります!!
ペイマスターに預ける暗号資産が必要なので、開発時はfaucetで沢山集めて預けると良いと思います。ちなみにこんな感じで各ネットワーク毎にペイマスターが設定でき、ガス代用の暗号資産を追加でdepositすることもできます!!
気になる方はぜひ下記ページにアクセスしてみてください。
Web3AuthとBiconomyを利用したAAアプリのシステムアーキテクチャ
Tipsの解説も終わったのでいよいよ本題です。
今回のテーマであるWeb3AuthとBiconomyを組み合わせるとどんなアーキテクチャになるのかということですが、ポンチ絵を用意してきました。
アクセス初回時には下記のような流れになります。
Web3Authの方でIDプロバイダーとの連携が済むと裏でアカウントごとに秘密鍵が作成され、APIを利用するとその情報を取得できます。その秘密鍵を元にSignerオブジェクトを作成して、BiconomySDKのメソッドを呼び出します。
そうしてようやくユーザーのウォレットコントラクトが作成されるという流れになります。
結局ユーザーごとに秘密鍵を作っているという流れになってしまっていますが、操作時はその存在を意識することなく使うことができるのでUXの向上が可能です。(セキュリティ的な課題はありますね・・)
Web3AuthはMPCにも対応しているということなのですが、結局分散していても 間違って console.logなどで秘密鍵を出力させてしまっていたりしたら大変ですね・・。
上手に使えば非常に有益なツールであることは間違いないですが、商用利用する時などは注意が必要そうです。
実装例(PushFi)
では実際にはどのように実装するのかを見ていきたいと思います!!
先日 CryptoLandで開発した PushFi というプロダクトで実際にこの組み合わせを使いました!!
Web3AuthとBiconomyの機能を使いやすくするためにそれぞれの機能をまとめたファイルを作って外出しすると整理しやすくなります!
-
Web3Authの機能を利用するための実装をまとめたファイル
内容は至ってシンプルで、ログイン・ログアウト用のメソッドに加えて、秘密鍵のデータを取得するメソッドが実装されています。
もっとも重要なメソッドは、loginメソッドで最終的にSignerオブジェクトを返しています。
import { ResponseData } from "@/pages/api/env"; import { decimalToHex } from "@/utils/constants"; import { getEnv } from "@/utils/getEnv"; import { CHAIN_NAMESPACES, SafeEventEmitterProvider } from "@web3auth/base"; import { Web3Auth } from "@web3auth/modal"; import { Wallet, ethers } from "ethers"; // 変数 var web3auth: Web3Auth; var idToken; /** * ログイン メソッド */ export const login = async( chainId: number, rpcUrl: string ) => { // get env const env: ResponseData = await getEnv(); web3auth = new Web3Auth({ clientId: env.WEB3_AUTH_CLIENT_ID, web3AuthNetwork: "testnet", chainConfig: { chainNamespace: CHAIN_NAMESPACES.EIP155, chainId: await decimalToHex(chainId), rpcTarget: rpcUrl }, }); // initModal await web3auth.initModal(); await web3auth.connect(); const authenticateUser = await web3auth.authenticateUser(); // set idToken idToken = authenticateUser.idToken; // get privateKey const pKey = await getPrivateKey(web3auth.provider!); // Avalanche RPC const provider = new ethers.providers.JsonRpcProvider(rpcUrl!); // create Signer Object const signer = new Wallet(pKey, provider) as any; return signer; } /** * logout method */ export const logout = async() => { // logout await web3auth.logout(); } /** * getPrivateKey method * @param provider * @returns */ const getPrivateKey = async(provider: SafeEventEmitterProvider) => { return (await provider.request({ method: "private_key", })) as string; };
-
Biconomyの機能を利用するための実装をまとめたファイル
次にBiconomy側の実装です。
こちらも至ってシンプルで、ユーザーのスマートウォレットを作成するメソッドとUserOpを送信するメソッドの2つが実装されています。スマートウォレットを作るためには、Signerオブジェクトが必要になりますが、そのSignerオブジェクトは、前に解説したWeb3Authのloginメソッドで作成したものを使います。
import { ResponseData } from "@/pages/api/env"; import { getEnv } from "@/utils/getEnv"; import { BiconomySmartAccountV2, DEFAULT_ENTRYPOINT_ADDRESS } from "@biconomy/account"; import { Bundler } from '@biconomy/bundler'; import { DEFAULT_ECDSA_OWNERSHIP_MODULE, ECDSAOwnershipValidationModule } from "@biconomy/modules"; import { BiconomyPaymaster, IHybridPaymaster, PaymasterMode, SponsorUserOperationDto } from '@biconomy/paymaster'; import { Signer } from "ethers"; import 'react-toastify/dist/ReactToastify.css'; import { TxData } from "./useContract"; var smartAccount: BiconomySmartAccountV2; /** * createSmartWallet method * @param chainId * @param signer */ export const createSmartWallet = async( chainId: number, signer: Signer ) => { // getEnv info const env: ResponseData = await getEnv(); // eslint-disable-next-line @next/next/no-assign-module-variable const module = await ECDSAOwnershipValidationModule.create({ signer: signer, moduleAddress: DEFAULT_ECDSA_OWNERSHIP_MODULE }); // バンドラーやpaymasterの情報をセット const bundler = new Bundler({ bundlerUrl: `https://bundler.biconomy.io/api/v2/${chainId.toString()}/${env.BICONOMY_BUNDLER_KEY}`, chainId: chainId, entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, }) const paymaster = new BiconomyPaymaster({ paymasterUrl: `https://paymaster.biconomy.io/api/v1/${chainId.toString()}/${env.BICONOMY_PAYMASTER_KEY}` }) let biconomySmartAccount = await BiconomySmartAccountV2.create({ chainId: chainId, bundler: bundler!, paymaster: paymaster!, entryPointAddress: DEFAULT_ENTRYPOINT_ADDRESS, defaultValidationModule: module, activeValidationModule: module }) const smartContractAddress = await biconomySmartAccount.getAccountAddress(); smartAccount = biconomySmartAccount; return { smartContractAddress, }; } /** * sendUserOp method * @param txData * @returns */ export const sendUserOp = async ( txDatas: TxData[] ) => { try { let userOp = await smartAccount.buildUserOp(txDatas); console.log({ userOp }) const biconomyPaymaster = smartAccount.paymaster as IHybridPaymaster<SponsorUserOperationDto>; let paymasterServiceData: SponsorUserOperationDto = { mode: PaymasterMode.SPONSORED, smartAccountInfo: { name: 'BICONOMY', version: '2.0.0' }, calculateGasLimits: true }; const paymasterAndDataResponse = await biconomyPaymaster.getPaymasterAndData( userOp, paymasterServiceData ); userOp.paymasterAndData = paymasterAndDataResponse.paymasterAndData; if ( paymasterAndDataResponse.callGasLimit && paymasterAndDataResponse.verificationGasLimit && paymasterAndDataResponse.preVerificationGas ) { userOp.callGasLimit = paymasterAndDataResponse.callGasLimit; userOp.verificationGasLimit = paymasterAndDataResponse.verificationGasLimit; userOp.preVerificationGas = paymasterAndDataResponse.preVerificationGas; } const userOpResponse = await smartAccount.sendUserOp(userOp); console.log("userOpHash", userOpResponse); const { receipt } = await userOpResponse.wait(1); console.log("txHash", receipt.transactionHash); return receipt.transactionHash; } catch (err: any) { console.error("sending UserOp err... :", err); return; } }
これらのファイルを一つでファイルで呼び出すと次のような実装になります。
※ あくまで一例ですが、ぜひ参考にしてください。
Web3Auth用のファイルで実装していたloginメソッドを呼んだ後にスマートウォレット作成メソッドを呼び出しています。
=== 中略
import { createSmartWallet, sendUserOp } from '@/hooks/biconomy';
import { login, logout } from './../hooks/web3auth';
=== 中略
/**
* Home Component
* @returns
*/
export default function Home() {
const [address, setAddress] = useState<string>("")
const [loading, setLoading] = useState<boolean>(false);
const [chainId, setChainId] = useState<number>(ChainId.AVALANCHE_TESTNET)
const [opening, setOpening] = useState<boolean>(true);
const [game, setGame] = useState<GameInfo>()
const [gameStatus, setGameStatus] = useState<string>(GameStatus.NOT_START);
const [count, setCount] = useState<number>(0);
const [verifyFlg, setVerifyFlg] = useState<boolean>(false);
// reCAPTCHAからtokenを取得する No.2の処理
const { executeRecaptcha } = useGoogleReCaptcha();
/**
* logIn method
*/
const logIn = async () => {
try {
setLoading(true);
// init UseContract instance
createContract(GAMECONTRACT_ADDRESS, gameContractAbi, RPC_URL);
// get Status
// get GameInfo
const gameInfo: GameInfo = await getGameInfo(GAME_ID);
console.log("gameInfo:", gameInfo)
// login & create signer
const signer = await login(chainId, RPC_URL);
console.log("signer:", signer)
// create smartWallet
const {
smartContractAddress: smartWalletAddress,
} = await createSmartWallet(chainId, signer);
console.log("smartWalletAddress:", smartWalletAddress)
setGame(gameInfo);
setOpening(gameInfo.openingStatus);
setAddress(smartWalletAddress)
} catch (error) {
console.error(error);
} finally {
setLoading(false)
}
};
/**
* logout
*/
const logOut = async() => {
await logout();
setVerifyFlg(false);
setAddress("");
}
=== 中略
}
実装の話はこの辺で一旦終わりです!!
UI
ではPushFiのUIはどうなっているのかというといくつかスクリーンショットを用意しましたので下記をご覧ください。
ユーザーは、面倒なウォレット作成をすることなく既存のIDを利用することでWeb3の世界にアクセスすることができるようになっています。
連打ゲームなので基本的にはユーザーがやることはボタンを押すだけです!!
ここまでシンプルなUI/UXにすることができればもっと多くの人がこのWeb3の世界に入りやすくなると思います!!
まとめ
いかがでしたでしょうか?
2023年はあらゆるハッカソンでAAが使われていたと思います。特に後半はAlchemyやStackUp、Biconomyの機能が拡充されて非常に開発しやすくなりました。
皆さんもこの記事で紹介した実装例を参考にぜひハッカソンでAAアプリを作ってみてください!!
来年以降のハッカソンではAIの要素も取り入れていきたいですね・・。
参考文献
- Web3Auth - Web3アプリケーションの認証をシンプル化するサービス
- web3authとは?使い方やethers.js/web3.jsとの連携まで紹介
- A web3 researcher's soliloquy - [9.22] Web3Authを完全に理解しよう
- Account Abstraction(ERC4337)を、具体的な処理を追ってしっかりと理解してみましょう。
- Account Abstraction とは何か? ブロックチェーンの体験を変えるERC4337について
- ERC-4337 UserOps Explorer
- Blocknative HP
- Blocknative Docs
- Biconomy HP