// AereModularAccount SDK demo — session keys + social recovery on AERE chain 2800. // // READ-ONLY / DRY-RUN by default: it queries the live chain via eth_call and // prints the calldata that write flows WOULD send, but sends no transactions. // Set AERE_SEND=1 to actually broadcast (you must also wire a signer/provider // that can sign; the default fetch provider below only serves eth_call). // // Run: // cd sdk-js && npm run build # produces ./dist // node examples/modular-account-demo.mjs // // The clients are dependency-free (no ethers/viem). This demo uses Node's global // fetch to satisfy the tiny EIP-1193 `request` surface. import { ModularAccountClient, SessionKeyClient, SocialRecoveryClient, AERE_MODULAR_ADDRESSES, MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, SELECTOR_NATIVE_TRANSFER, RECOVERY_DELAY_SECONDS, } from '../dist/account/index.js'; const RPC = process.env.AERE_RPC || 'https://rpc.aere.network'; const DO_SEND = process.env.AERE_SEND === '1'; // Minimal EIP-1193 provider over JSON-RPC (read-only unless you add signing). let rpcId = 1; const provider = { async request({ method, params = [] }) { const res = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: rpcId++, method, params }), }); const j = await res.json(); if (j.error) throw new Error(`${method}: ${JSON.stringify(j.error)}`); return j.result; }, }; // The sample account proven live on 2026-07-10 (root owner = the deployer). const ROOT_OWNER = '0xbeB33D20dFBBD49eC7AC1F617667f1f02dfd6465'; const SALT = 1783688121732n; const PING = '0x6191CC59961F2C652aE1879ff731aE1349986036'; // AerePingCounter const PING_SELECTOR = '0x5c36b186'; // ping() const mac = new ModularAccountClient(provider, AERE_MODULAR_ADDRESSES); const sk = new SessionKeyClient(provider, AERE_MODULAR_ADDRESSES.sessionKeyValidator); const sr = new SocialRecoveryClient(provider, AERE_MODULAR_ADDRESSES.socialRecoveryModule); const line = (s = '') => console.log(s); // ───────────────────────────── Flow 1: Modular account ───────────────────── line('=== 1. Modular account (CREATE2 + reads + root userOp) ==='); const account = mac.getAddress(ROOT_OWNER, SALT); // local CREATE2 mirror line(`counterfactual address : ${account}`); line(`on-chain factory says : ${await mac.getAddressOnChain(ROOT_OWNER, SALT)}`); line(`deployed? : ${await mac.isDeployed(account)}`); line(`rootOwner : ${await mac.rootOwnerOf(account)}`); line(`entryPoint : ${await mac.entryPointOf(account)}`); line(`EntryPoint deposit : ${await mac.depositOf(account)} wei`); // Build + hash a root-owner userOp that calls ping() (no submit). const callData = mac.encodeExecute(PING, 0n, PING_SELECTOR); const rootOp = await mac.buildUserOp({ sender: account, callData }); line(`root userOp nonce : ${rootOp.nonce}`); line(`userOpHash (local) : ${mac.userOpHash(rootOp)}`); line(`userOpHash (on-chain) : ${await mac.userOpHashOnChain(rootOp)} <- must match`); line('to submit: signRootUserOp(rootOp, eip1193PersonalSign(walletProvider, ROOT_OWNER)) then sendHandleOps(bundler, [op], bundler)'); if (DO_SEND) line('(AERE_SEND set, but this demo provider cannot sign — wire a wallet provider first)'); // ───────────────────────────── Flow 2: Session keys ──────────────────────── line(); line('=== 2. Session key (scope + local check + on-chain read) ==='); const sessionKey = '0x0EE48ec9e2EE095Ce81e351457893EAAf8CB2CEC'; const scope = { key: sessionKey, validAfter: 0, validUntil: 1893456000, // 2030-01-01 maxValuePerOp: 1_000_000_000_000_000n, // 0.001 AERE permissions: [{ target: PING, selector: PING_SELECTOR }], }; line(`install initData : ${sk.encodeInstallInitData(scope).slice(0, 74)}...`); line(`enableSession calldata : ${sk.encodeEnableSession(scope).slice(0, 74)}...`); line(' (register by: account.installModule(1, validator, initData) OR'); line(' account.execute(validator, 0, enableSessionCalldata) as the root owner)'); // Local scope check BEFORE paying to submit. const inScope = sk.checkCallLocally(scope, { target: PING, value: 0n, data: PING_SELECTOR }); const badSel = sk.checkCallLocally(scope, { target: PING, value: 0n, data: '0xdeadbeef' }); const badVal = sk.checkCallLocally(scope, { target: PING, value: 2n * scope.maxValuePerOp, data: PING_SELECTOR }); line(`in-scope ping() : ${JSON.stringify(inScope)}`); line(`out-of-scope selector : ${JSON.stringify(badSel)}`); line(`over-value transfer : ${JSON.stringify(badVal)}`); line(`native-transfer sentinel selector = ${SELECTOR_NATIVE_TRANSFER}`); const onChainSession = await sk.getSession(account, sessionKey); line(`on-chain session : ${JSON.stringify(onChainSession, (k, v) => (typeof v === 'bigint' ? v.toString() : v))}`); // Build a session-signed userOp (asserts scope, then leaves 0x01 signature to attach). const { op: sessOp, userOpHash: sessHash } = await sk.buildSessionUserOp( mac, account, { target: PING, value: 0n, data: PING_SELECTOR }, { scope }, ); line(`session userOpHash : ${sessHash}`); line('to submit: sk.signSessionUserOp(mac, sessOp, sessionKeySigner) -> 0x01||validator||sig, then mac.sendHandleOps(bundler, [op], bundler)'); void sessOp; // ───────────────────────────── Flow 3: Social recovery ───────────────────── line(); line('=== 3. Social recovery (M-of-N guardians, 48h timelock) ==='); line(`recovery installed? : ${await mac.isModuleInstalled(account, MODULE_TYPE_EXECUTOR, AERE_MODULAR_ADDRESSES.socialRecoveryModule)}`); const guardians = await sr.guardiansOf(account); line(`guardians : ${JSON.stringify(guardians)}`); line(`threshold : ${await sr.thresholdOf(account)}`); line(`recovery delay : ${RECOVERY_DELAY_SECONDS}s (48h)`); const active = await sr.getActiveRecovery(account); line(`active recovery : ${JSON.stringify(active)}`); if (active.active) { const remaining = active.executeAfter - Math.floor(Date.now() / 1000); line(` -> executable in ~${Math.max(0, remaining)}s`); } const NEW_OWNER = '0xFa1Eb227Bd1F05104bE0bB21a3f2094238E0d6D2'; line('install initData : ' + sr.encodeInstallInitData(guardians, 2).slice(0, 74) + '...'); line('guardian1 initiate : ' + sr.encodeInitiateRecovery(account, NEW_OWNER)); line('guardian2 support : ' + sr.encodeSupportRecovery(account)); line('any guardian execute : ' + sr.encodeExecuteRecovery(account) + ' (after threshold + 48h)'); line('owner cancel : ' + sr.encodeCancelRecovery(account)); line('add guardian (owner) : account.execute(module, 0, ' + sr.encodeAddGuardian(NEW_OWNER).slice(0, 42) + '...)'); if (DO_SEND) { line('\n[AERE_SEND] guardian/owner flows would broadcast now via sr.sendInitiateRecovery(...) etc.'); } else { line('\nDRY-RUN complete — no transactions sent. Set AERE_SEND=1 (and a signing provider) to broadcast.'); }