🙌

Web3AuthとBiconomyでAAアプリを実装する

2023/12/21に公開

3.png

この記事はEthereumアドベントカレンダー2023 21日目の投稿です。

https://qiita.com/advent-calendar/2023/ethereum

はじめに

皆さん、こんにちは!
久しぶりの記事投稿&Ethereumアドベントカレンダー2023への投稿になります!!

2024年に向けて皆さんへ一つでも多くの知識を共有できれば幸いです。

今回のテーマは、Web3AuthBiconomy を使ったAAアプリの作り方についての解説記事になります!!!

それでは本編スタートです!!

Tips

まずこの記事を読む上で必要になってくる用語の簡単な解説だけしたいと思います!

今回は3つ取り上げます!

  • AA(アカウントアブストラクション)

    イーサリアムの創始者であるヴィタリックさんの長年の夢でもあったものです!!

    AAは、プロトコルレベルでの改修無しにEOAとコントラクトウォレットの差異を埋めるためのアーキテクチャです。

    2022年後半あたりからこのAAを実装しやすくなり、ハッカソンなどでもこのアーキテクチャを採用したプロダクトが爆発的に増えました。ブロックチェーンアプリケーションの可能性を大きく広げるものとして今後も注目されるでしょう。

    上位入賞しているプロダクトの多くが当然のごとくAAを利用しています!!

    このAAを実装するための規格として最もよく知られているが、ERC4337ですね!(スマートコントラクト側)

    下記記事などでもまとめられているので気になる方はそちらもご覧ください!!

    https://zenn.dev/yuki2020/articles/00242351b3b3aa

  • Web3Auth

    Web3Authは、Web3アプリケーションの認証プロセスを簡単にするサービスです。
    Ethereumブロックチェーンを利用してユーザー認証を行い、サーバーレスで安全な方法を提供します。

    Metamaskのように従来の面倒な秘密鍵保管を行う必要なくユーザーが資産の管理を行えるセルフカストディウォレットの作成を可能にするサービスです。

    なんだか難しいことを書きましたが端的に言うと 「MetamaskがなくてもWeb3の世界にアクセスできるUXが提供できるようになります!!」

    現在、新規ユーザーをWeb3の世界に案内するためにウォレットなどについての知識については自己学習が基本的に必要となりますが、これだとマス受けはまず難しいでしょう。

    Web2アプリでメガヒットしているものはどれも直感的に分かりやすいUXを提供してします。それを可能にするのがWeb3Authです!!

    下記記事などでもまとめられているので気になる方はそちらもご覧ください!!

    https://newsletter.woorth.io/p/20230922

  • Biconomy

    上述したAAを実装するための便利なSDKやAPIを提供しているAA特化のインフラプロバイダーです!!
    他に存在するプレイヤーとしてはStackUp社やBlocknative社が有名ですね。

    ペイマスターなどAAを実装するために必須となる要素がすぐに使えるので開発効率がものすごく上がります!!
    ペイマスターに預ける暗号資産が必要なので、開発時はfaucetで沢山集めて預けると良いと思います。

    ちなみにこんな感じで各ネットワーク毎にペイマスターが設定でき、ガス代用の暗号資産を追加でdepositすることもできます!!

    スクリーンショット 2023-12-17 11.12.45.png

    気になる方はぜひ下記ページにアクセスしてみてください。

    https://www.biconomy.io/

Web3AuthとBiconomyを利用したAAアプリのシステムアーキテクチャ

Tipsの解説も終わったのでいよいよ本題です。
今回のテーマであるWeb3AuthとBiconomyを組み合わせるとどんなアーキテクチャになるのかということですが、ポンチ絵を用意してきました。

Web3AuthとBiconomy.drawio.png

アクセス初回時には下記のような流れになります。
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の世界にアクセスすることができるようになっています。

スクリーンショット 2023-12-17 13.18.53.png
スクリーンショット 2023-12-17 13.19.09.png

mock15.png

mock16.png

連打ゲームなので基本的にはユーザーがやることはボタンを押すだけです!!

ここまでシンプルなUI/UXにすることができればもっと多くの人がこのWeb3の世界に入りやすくなると思います!!

まとめ

いかがでしたでしょうか?

2023年はあらゆるハッカソンでAAが使われていたと思います。特に後半はAlchemyやStackUp、Biconomyの機能が拡充されて非常に開発しやすくなりました。

皆さんもこの記事で紹介した実装例を参考にぜひハッカソンでAAアプリを作ってみてください!!

来年以降のハッカソンではAIの要素も取り入れていきたいですね・・。

参考文献

  1. Web3Auth - Web3アプリケーションの認証をシンプル化するサービス
  2. web3authとは?使い方やethers.js/web3.jsとの連携まで紹介
  3. A web3 researcher's soliloquy - [9.22] Web3Authを完全に理解しよう
  4. Account Abstraction(ERC4337)を、具体的な処理を追ってしっかりと理解してみましょう。
  5. Account Abstraction とは何か? ブロックチェーンの体験を変えるERC4337について
  6. ERC-4337 UserOps Explorer
  7. Blocknative HP
  8. Blocknative Docs
  9. Biconomy HP

Discussion