# Test smart wallets (AA / smart contract accounts) > [🇬🇧 English](./smart-wallet-aa.md) • [🇯🇵 日本語](../../ja/cookbook/smart-wallet-aa.md) Use `WalletConfig.isContractAccount` added in `@kiwa-test/dapp` v0.3 to test ERC-4337 / EIP-1271 smart contract accounts end-to-end. ## Problem Regular EOA wallets (MetaMask / Rabby) sign with ECDSA via `personal_sign` / `eth_signTypedData_v4`. **Smart contract accounts (AA)** behave differently: - Signatures are generated by the owner EOA, but verification must go through the smart account's `isValidSignature(hash, signature)` (EIP-1271). - Transactions are not sent via `eth_sendTransaction` directly; they go through the smart account's `execute(target, value, data)` (in real ERC-4337, via `eth_sendUserOperation`). - `eth_accounts` should return the **smart account address**, not the owner EOA. kiwa handles these differences transparently at the RPC layer so dApp code (wagmi / viem) does not need special handling. ## Solution pattern ### Step 1: Deploy the smart account in prepare-env.ts ```ts // tests/prepare-env.ts import { runE2EPrepareEnv, loadForgeArtifact } from '@kiwa-test/dapp'; await runE2EPrepareEnv({ envFile: '.env.local', port: 8551, deploy: async ({ wallet, publicClient }) => { // Deploy EntryPoint v0.7 + SimpleAccountFactory + SimpleAccount const entryPoint = await deployContract(/* ... */); const factory = await deployContract({ args: [entryPoint] }); // Create a smart account from the owner EOA const smartAccount = await wallet.writeContract({ address: factory, abi: FACTORY_ABI, functionName: 'createAccount', args: [OWNER_EOA, SALT], }); return { NEXT_PUBLIC_ENTRYPOINT: entryPoint, NEXT_PUBLIC_SMART_ACCOUNT: smartAccount, }; }, }); ``` ### Step 2: Declare `isContractAccount` in the fixture ```ts // tests/fixture.ts import { dappE2eTest } from '@kiwa-test/dapp'; import type { Address } from 'viem'; const SMART_ACCOUNT_ADDRESS = process.env.NEXT_PUBLIC_SMART_ACCOUNT as Address; export const test = dappE2eTest.extend({ wallets: [ { name: 'Simple AA', rdns: 'eth.aa-simple', icon: 'data:image/svg+xml;base64,...', privateKey: OWNER_EOA_PRIVATE_KEY, isContractAccount: true, contractAccountAddress: SMART_ACCOUNT_ADDRESS, }, ], }); ``` kiwa automatically reroutes these RPCs: | RPC | EOA (default) | smart account (`isContractAccount=true`) | |---|---|---| | `eth_accounts` | owner EOA address | smart account address | | `eth_requestAccounts` | same | same | | `personal_sign` | sign with owner EOA | sign with owner EOA + verify via `isValidSignature` before returning | | `eth_signTypedData_v4` | same | same (via EIP-712 hash) | | `eth_sendTransaction` | direct from owner EOA | through smart account's `execute(target, value, data)` | ### Step 3: Write your tests ```ts import { test, expect } from './fixture'; test('T-AA-001 eth_accounts returns the smart account address', async ({ page, dappE2e }) => { await page.goto('/'); await page.getByTestId('connect-button').click(); const accounts = await page.evaluate(async () => { return (window as any).ethereum.request({ method: 'eth_accounts' }); }); expect(accounts[0].toLowerCase()).toBe(SMART_ACCOUNT_ADDRESS.toLowerCase()); }); test('T-AA-002 personal_sign signatures pass isValidSignature', async ({ page, dappE2e }) => { const { signature, message } = await page.evaluate(async () => { const accounts = await (window as any).ethereum.request({ method: 'eth_accounts' }); const sig = await (window as any).ethereum.request({ method: 'personal_sign', params: ['hello AA', accounts[0]], }); return { signature: sig, message: 'hello AA' }; }); // kiwa has already validated via isValidSignature internally expect(signature).toMatch(/^0x[0-9a-f]+$/i); }); ``` ## Full ERC-4337 example `examples/nextjs-aa-erc4337/` ships a complete EntryPoint v0.7 + SimpleAccountFactory + in-process UserOperation bundler stub. Key tests: - T-AA37-002 `EntryPoint.depositTo` gas funding + deploy + execute in a single UserOperation - T-AA37-003 UserOperation signature is generated by owner EOA and passes `validateUserOp` - T-AA37-004 UserOperation with invalid signature reverts - T-AA37-005 UserOperation with a stale nonce reverts - T-AA37-006 dApp UI triggers `sendUserOperation` and `MockTarget.counter` increments ## thirdweb / Safe / Biconomy integration (roadmap) SDK-bound integrations land in v0.4+ (Phase D-3): - **thirdweb `inAppWallet`** — smart wallets via Email / Passkey - **Safe (Gnosis Safe)** — multi-sig threshold signatures - **Biconomy / ZeroDev / Alchemy AA SDK** — managed bundler / paymaster Likely shape: accept external SDKs via `peerDependencies` and ship them as subpackages (`@kiwa-test/aa-thirdweb`, `@kiwa-test/aa-safe`, ...). ## Avoiding false positives Watch out for `adversarial-pitfalls.md` **#3 (partial access-control checks)**: - Cover not just the owner path but also **guardian / module / fallback handler signature paths** - Always test the case where `validateUserOp` **returns 0 (sigFailed)** for mismatched signatures - After a recovery flow, verify that signatures from the **old owner are rejected by `isValidSignature`** ## See also - [API: WalletConfig.isContractAccount](../api/dapp-e2e-test.md#walletconfig) - [Example: nextjs-aa-erc4337](https://github.com/cardene777/kiwa/tree/main/examples/nextjs-aa-erc4337) - [Example: nextjs-aa-smart-account (simplified)](https://github.com/cardene777/kiwa/tree/main/examples/nextjs-aa-smart-account)