sdk-js/examples/modular-account-demo.mjs
Liviu 6d6c18ae41 SDK 0.16.12: the address book and typed clients catch up with the live chain, and client mentions get the same discretion as everywhere else
The published repository served the 0.3.0 line from May while the install
page already pointed here. This carries the uncommitted line: 35+ typed
contract clients over the canonical address book, the compliance surface,
and the hybrid PQC wallet helpers.
2026-08-15 14:01:27 +03:00

133 lines
7.1 KiB
JavaScript

// 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.');
}