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.
133 lines
7.1 KiB
JavaScript
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.');
|
|
}
|