Account Abstraction入門:EOAとスマートアカウントのUXを体験
Account Abstractionとは
ひとことで:アカウントのふるまいをスマートコントラクトで定義できるようにする考え方/仕組みです。これにより、従来はExternally Owned Account(EOA)前提から、認証・実行・支払い・権限・復旧をアプリ側のロジックで安全にコントロールできるようになります。
何が変わるのか
- 署名と実行の分離:ユーザーの署名手段(既存ウォレット(EOA)/パスキー/鍵を分けて共同で管理する方式 など)と、実際の実行・支払いを分けられます。
- 取引の単位が変わる:通常のトランザクション(Tx)の代わりに UserOperation(UO) を出します。UO は Bundler にまとめられて EntryPoint に渡され、共通のルールで検証・実行されます。
- 支払いの抽象化:Paymasterを使えばガスをアプリ側で肩代わりできます。ユーザーは ETH がゼロでもDappsの利用ができます。
主な登場人物(ざっくり)
- スマートアカウント:アカウントアブストラクションを利用して作られた、スマートコントラクトベースのアカウント。マルチオーナーやセッションキー等を実装できる
- UserOperation:やりたい処理の申請書
- Bundler:申請書を束ねて実行する配達員
- EntryPoint:受付・検証・実行・精算を取り仕切る共通コントラクト
- Paymaster:条件を満たす操作のガスを肩代わりをする
誤解しやすい点
現時点では EOA が不要になるわけではありません。 既存の EOA をオーナーにしたり、Web2 風のログイン(メール/パスキー)で アプリが補助する鍵を使ったりと、共存が現実的です。
Account Abstractionがあると良いこと
Web3のUXはいまだに重たいと思います。ウォレット拡張の導入、シードフレーズ管理、ガス購入やブリッジなど、いずれもアプリの外側で発生し、開発者がUXを主導しにくいのが現状です。さらに EOA は「1 本の鍵=全権」という構造のため、細かな権限付与や安全な復旧を苦手とします。
ERC-4337(Account Abstraction)は、コンセンサスの変更なしにアカウントそのものをコントラクトで表現できるようにする標準です。実行要求を UserOperation として送り、Bundler がまとめて EntryPoint に投入し、必要に応じて Paymaster がガスを肩代わりします。この流れにより、開発者は実行・課金・権限の設計をプロダクト側に取り戻せます。
その結果、たとえガス代不要、メール/パスキー等の Web2 認証と直結した即利用、セッションキーによるこの関数だけ・24 時間だけといった権限委任、ソーシャル/パスキー復旧など、ネイティブアプリに近い体験を実現できます。
EOA vs スマートアカウント:ギャップ早見表
観点 | EOA | スマートアカウント |
---|---|---|
実行 | 署名 → トランザクションを直接送信 | UserOperationで実行(ポリシー適用可) |
ガス支払い | 送信者が負担 | Paymaster が肩代わり可能 |
復旧 | シードフレーズのみ | ソーシャル / パスキー / マルチオーナー |
権限制御 | 全権一任 | 関数・金額・期限を絞るセッションキー |
UX | ダイアログ多め、複雑 | ワンクリック体験まで寄せられる |
簡単なDAppsでEOAとスマートアカウントでUX比較
ガス代が発生する処理を EOA と スマートアカウント 両方で実行して差を見てみます
リポジトリ:
Alchemy 事前準備
-
Create New Appで
https://eth-sepolia.g.alchemy.com/v2/<ALCHEMY_API_KEY>
を取得 -
Gas Manager でPolicy作成
一旦こういう風に設定はしました
policyId
を取得します -
Smart Walletsでconfigの設定
認証の設定はSNS認証はclient IDが必要になってくるので使用せず、emailだけ使うようにしています emailのredirect URLやApply whitelisted originにはローカル環境で確認する場合は
http://localhost:3000
を入力してくださいAPI Key
を取得します
Hardhatでの環境構築とテストネットへのデプロイ
※ Node.jsはv22で実行しております。
Hardhat初期設定
hardhatの初期設定を行います
mkdir aa-test && cd aa-test
# What do you want to do?で`Create a TypeScript project (with Viem)`を選択し、あとはenter押して進めば簡単に作成できます。
npx hardhat init
コントラクトの作成
contractsディレクトリにPostBoard.sol
を作成します Lock.sol
は不要なので削除して大丈夫です
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract PostBoard {
event PostCreated(address indexed author, string cid);
function createPost(string calldata cid) external {
require(bytes(cid).length != 0, "empty cid");
emit PostCreated(msg.sender, cid);
}
}
.env配置
Alchemyのアプリ作成した時に取得できるsepoliaのURLとユーザーのテストアカウントの秘密鍵を入れます
SEPOLIA_URL=https://eth-sepolia.g.alchemy.com/v2/<ALCHEMY_API_KEY>
PRIVATE_KEY=0x<YOUR_TEST_PRIVATE_KEY>
hardhat.config.ts にネットワーク追記
デプロイするために必要な最小限の構成を書きます
import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox-viem';
import * as dotenv from 'dotenv';
dotenv.config();
const config: HardhatUserConfig = {
solidity: {
version: '0.8.28',
},
networks: {
sepolia: {
url: process.env.SEPOLIA_URL || '',
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 11155111,
},
},
};
export default config;
Ignitionモジュール作成
ignition/modules/PostBoard.tsを作成します Lock.ts
は不要なので削除して大丈夫です
import { buildModule } from "@nomicfoundation/hardhat-ignition";
export default buildModule("PostBoardModule", (m) => {
const postBoard = m.contract("PostBoard");
return { postBoard };
});
デプロイ実行
※デプロイ時はガス代がかかる為、sepolia ETHが多少必要になりますので注意してください(0.01 Sepolia ETHあれば安心だと思います)
npx hardhat ignition deploy ./ignition/modules/PostBoard.ts --network sepolia
# => PostBoardModule#PostBoard 0x... (0x...はフロントエンド環境のNEXT_PUBLIC_POST_BOARDに設定するので控えておいてください)
フロントエンド環境構築(Next.js App Router)
こちらの記事に従い、フロントエンド環境の構築とSmart Walletsの設定をしていきます。
next.jsの初期設定と必要なライブラリのインストール
まず、next.jsの初期設定特にこだわりなければ、全部enter押していけば良いと思います。
完了したら、必要なライブラリのインストールも行います
npx create-next-app@latest frontend --ts
npm install @account-kit/react @account-kit/infra @tanstack/react-query viem
.env.localの配置
Alchemy 事前準備で取得したAPI Key、Policy Id、sepoliaデプロイ時に取得したアドレスを入れます。
NEXT_PUBLIC_ALCHEMY_API_KEY=xxxxx
NEXT_PUBLIC_GAS_POLICY_ID=policy-xxxxxx
NEXT_PUBLIC_POST_BOARD=0xYourPostBoardAddress
tailwindの設定
tailwind.config.ts
にwithAccountKitUiを適用します。これで認証モーダルなどが適切にテーマ適用されます。
import { withAccountKitUi } from "@account-kit/react/tailwind";
export default withAccountKitUi(
{
// Your existing Tailwind config (if already using Tailwind).
// If using Tailwind v4, this will likely be left empty.
},
{
// AccountKit UI theme customizations
},
);
**app/globals.css
**にtailwindのconfigを読み込みます
@import "tailwindcss";
@config '../../tailwind.config.ts';
ABIの配置(PostBoardAbi をフロントへ移動)
コントラクトをデプロイ後に生成された ABI(artifacts/contracts/PostBoard.sol/PostBoard.json
)はfrontend/src/abi/PostBoardAbi.json
に配置します。
app/providers.tsx
にSmart Walletsのconfigの設定とtanstack queryの設定を入れたproviderを作成する
createConfig
に policyId
を渡すと、条件一致時に Gas Manager がスポンサーになりガス代を代わりに支払ってくれます。
'use client';
import {
AlchemyAccountProvider,
createConfig,
cookieStorage,
} from '@account-kit/react';
import { sepolia, alchemy } from '@account-kit/infra';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const config = createConfig(
{
transport: alchemy({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_API_KEY! }),
chain: sepolia,
policyId: process.env.NEXT_PUBLIC_GAS_POLICY_ID,
ssr: true,
storage: cookieStorage,
enablePopupOauth: true,
},
{
auth: {
// メアドとEOAによる認証
sections: [[{ type: 'email' }], [{ type: 'external_wallets' }]],
},
}
);
export function Providers({ children }: { children: React.ReactNode }) {
const qc = new QueryClient();
return (
<QueryClientProvider client={qc}>
<AlchemyAccountProvider config={config} queryClient={qc}>
{children}
</AlchemyAccountProvider>
</QueryClientProvider>
);
}
app/layout.tsx
にProviderを入れる
import './globals.css';
import { Providers } from './providers';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'AA test',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
これでフロントエンドの環境構築が終わったのでコントラクトを呼ぶ処理を書いていきます。
コントラクトの内容を送信する投稿ボタンの作成
frontend/src/app/components/PostButton.tsx
※ useSendUserOperation
は接続ウォレット種別をhooks側で判定します。(EOAではガス肩代わり等のアカウントアブストラクションの便利機能は適用されません)
import {
useSmartAccountClient,
useSendUserOperation,
} from '@account-kit/react';
import { encodeFunctionData } from 'viem';
import PostBoardAbi from '@/abi/PostBoardAbi.json';
// PostBoardコントラクトのアドレス(環境変数から取得)
const POST_BOARD = process.env.NEXT_PUBLIC_POST_BOARD as `0x${string}`;
/**
* 投稿を作成するボタンコンポーネント
*/
export function PostButton({ cid }: { cid: string }) {
// PostBoardコントラクトのcreatePost関数の呼び出しデータをエンコード
const data = encodeFunctionData({
abi: PostBoardAbi,
functionName: 'createPost',
// IPFSのCIDを引数として渡す
args: [cid],
});
// LightAccountタイプのSmart Accountクライアントを取得
const { client, isLoadingClient } = useSmartAccountClient({
type: 'LightAccount',
});
// UserOperationまたは通常のトランザクションを送信するためのhook
// アカウントタイプに応じて適切な送信方法を自動選択
const { sendUserOperation, isSendingUserOperation } = useSendUserOperation({
client,
onSuccess: () => {
// トランザクション成功時のコールバック
alert(`User operation sent!`);
},
});
return (
<button
// クライアント読み込み中または送信中は無効化
disabled={isSendingUserOperation || isLoadingClient}
onClick={() =>
// PostBoardコントラクトのcreatePost関数を実行
sendUserOperation({
uo: {
target: POST_BOARD, // 呼び出し先コントラクトアドレス
data, // エンコードされた関数呼び出しデータ
value: BigInt(0), // 送金額(0 ETH)
},
})
}
className="akui-btn akui-btn-primary mt-4 disabled:akui-btn-disabled disabled:cursor-not-allowed disabled:opacity-50"
>
{isLoadingClient
? 'ローディング中...'
: isSendingUserOperation
? '投稿中...'
: '投稿'}{' '}
</button>
);
}
ページの作成
app/page.tsx
'use client';
import {
useAuthModal,
useLogout,
useSignerStatus,
useUser,
} from '@account-kit/react';
import { PostButton } from './components/PostButton';
export default function Home() {
const user = useUser();
const { openAuthModal } = useAuthModal();
const signerStatus = useSignerStatus();
const { logout } = useLogout();
return (
<main className="flex min-h-screen flex-col items-center p-24 gap-4 justify-center text-center">
{signerStatus.isInitializing ? (
<>Loading...</>
) : user ? (
<div className="flex flex-col gap-2 p-2">
<p className="text-xl font-bold">ログイン中</p>
{user.email ? user.email : user.address}
<button
className="akui-btn akui-btn-primary mt-6"
onClick={() => logout()}
>
ログアウト
</button>
{/* cidは仮のものです。実際のアプリでは適切なCIDを使用してください。 */}
<PostButton cid="demo:cid:hello-world" />
</div>
) : (
<button
className="akui-btn akui-btn-primary"
type="button"
onClick={openAuthModal}
>
ログイン
</button>
)}
</main>
);
}
これでDAppsを作成することができました。
UXの比較
ここからEOAとスマートアカウントのUXの比較をやっていきます。
ログインボタンを押下すると、alchemyが用意している認証ダイアログが表示されます。メールアドレスかEOAで認証できます。EOAは現在利用できるウォレットを提示してくれます。
メールアドレスでログインした時 6桁のコードを入れると認証されます。
EOA(Metamask)でログインした時 ウォレットの接続確認ボタンを押下すると認証できます
ここからがUXが特に変わってくるところです。
EOAの場合
投稿ボタンを押下すると、ウォレット(ex.MetaMask)のトランザクションの要求画面に飛びます。
確認ボタンを押下することによって承認され、トランザクションを送信します。この時ユーザーはガス代を支払う必要があります。
なので十分なSepoliaETHを所有していないユーザーはトランザクションを送信することはできません。
スマートアカウントの場合
投稿ボタンをワンクリックするだけで完了します。Sepolia環境では、ポリシー条件に一致すれば Gas Manager(Paymaster)がガス代を肩代わりするため、ユーザーはガスを支払う必要がありません。(本番環境ではクレジットカード等でAlchemyのGas Managerに課金して精算する運用になるようです。)
PostButton.tsx の sendUserOperation() が呼ばれると内部ではこのようなことを行なっています
- SDK (useSendUserOperation): UserOperation 組み立て
-
Paymaster (Account Kit/Gas Manager):
paymasterAndData
でガス肩代わり -
Bundler (Account Kit の設定先):
eth_sendUserOperation
で送信 - EntryPoint (ERC-4337標準): 検証・実行・ガス精算の中央制御
- LightAccount: 署名検証とトランザクション実行
まとめ
実際に最小の DApp を作成してみて、毎回MetaMaskのポップアップが出ないことや、手元に ETH がなくても操作できる体験は想像以上に便利だなと実感しました。
今回はテスト環境でしたのでガスポリシーは最小構成に留めましたが、本番では設定を誤ると Paymasterの負担が膨らむリスクがあるんじゃないかなと思います。今後、セキュリティ周りにも目を向けていきたいです。
ここまで読んでいただき、ありがとうございました。
Discussion