/** * NewComplianceClients — SDK for the 2026-06-08 ship batch. * Covers: AereAttestationGateway, AereCompliancePool. * * Pattern matches sibling clients in @aere/sdk-js — dependency-free * EIP-1193 wrapper, precomputed selectors. */ export interface RpcProvider { request(args: { method: string; params: unknown[] }): Promise; } const pad32 = (h: string) => h.toLowerCase().replace(/^0x/, '').padStart(64, '0'); const encU = (n: bigint) => pad32(n.toString(16)); const encA = (a: string) => pad32(a.replace(/^0x/, '')); const encB = (b: string) => pad32(b.replace(/^0x/, '')); async function call(p: RpcProvider, to: string, data: string): Promise { return (await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] })) as string; } async function send(p: RpcProvider, from: string, to: string, data: string): Promise { return (await p.request({ method: 'eth_sendTransaction', params: [{ from, to, data, value: '0x0' }] })) as string; } function encString(s: string): { body: string; byteLen: number } { const b = Buffer.from(s, 'utf8'); const padded = Buffer.alloc(Math.ceil(b.length / 32) * 32); b.copy(padded); return { body: encU(BigInt(b.length)) + padded.toString('hex'), byteLen: padded.length + 32 }; } function encBytes(hex: string): { body: string; byteLen: number } { const clean = hex.replace(/^0x/, ''); const len = clean.length / 2; const padded = Buffer.alloc(Math.ceil(len / 32) * 32); Buffer.from(clean, 'hex').copy(padded); return { body: encU(BigInt(len)) + padded.toString('hex'), byteLen: padded.length + 32 }; } /* ========================= AereAttestationGateway ========================= */ const ATT_SEL = { FOUNDATION: '0x65df2e51', MAX_INHERITANCE_DEPTH: '0xadf515af', isAuthorisedAttestor: '0x331735f0', latestFor: '0xfba8fcb9', isValidNow: '0xcf65b2b5', subjectCurrentlyAttested: '0x57e74a61', publishSchema: '0xce3f3aa3', setAttestor: '0xe65b98ba', attest: '0xc35b1106', revoke: '0xb75c7dc6', } as const; export class AereAttestationGatewayClient { constructor(public readonly provider: RpcProvider, public readonly address: string) {} async isValidNow(attestationId: string): Promise { const r = await call(this.provider, this.address, ATT_SEL.isValidNow + encB(attestationId)); return BigInt(r) === 1n; } async subjectCurrentlyAttested(subject: string, schemaId: string): Promise { const data = ATT_SEL.subjectCurrentlyAttested + encB(subject) + encB(schemaId); return BigInt(await call(this.provider, this.address, data)) === 1n; } async latestFor(subject: string, schemaId: string): Promise { return await call(this.provider, this.address, ATT_SEL.latestFor + encB(subject) + encB(schemaId)); } async isAuthorisedAttestor(schemaId: string, attestor: string): Promise { const data = ATT_SEL.isAuthorisedAttestor + encB(schemaId) + encA(attestor); return BigInt(await call(this.provider, this.address, data)) === 1n; } /** Foundation-only. */ publishSchema(from: string, schemaId: string, specHash: string, name: string, semver: string, specUri: string): Promise { const n = encString(name), sv = encString(semver), su = encString(specUri); // head: schemaId(32) + specHash(32) + offset_n + offset_sv + offset_su = 5 slots const offN = 5n * 32n; const offSv = offN + BigInt(n.byteLen); const offSu = offSv + BigInt(sv.byteLen); const head = encB(schemaId) + encB(specHash) + encU(offN) + encU(offSv) + encU(offSu); return send(this.provider, from, this.address, ATT_SEL.publishSchema + head + n.body + sv.body + su.body); } setAttestor(from: string, schemaId: string, attestor: string, authorised: boolean): Promise { const data = ATT_SEL.setAttestor + encB(schemaId) + encA(attestor) + pad32(authorised ? '1' : '0'); return send(this.provider, from, this.address, data); } /** Authorised-attestor-only. parentId = 0x00...00 for no parent. */ attest( from: string, schemaId: string, subject: string, parentId: string, payloadHash: string, validFrom: bigint, validUntil: bigint, payloadUri: string ): Promise { const p = encString(payloadUri); // 7 head slots + string tail const head = encB(schemaId) + encB(subject) + encB(parentId) + encB(payloadHash) + encU(validFrom) + encU(validUntil) + encU(7n * 32n); return send(this.provider, from, this.address, ATT_SEL.attest + head + p.body); } revoke(from: string, attestationId: string): Promise { return send(this.provider, from, this.address, ATT_SEL.revoke + encB(attestationId)); } } /* ============================= AereCompliancePool ============================ */ const POOL_SEL = { TOKEN: '0x82bfefc8', DENOMINATION: '0x8b01429d', FOUNDATION: '0x65df2e51', VERIFIER: '0x08c84e70', POOL_ID: '0xe0d7d0e9', latestDepositRoot: '0x4e574c55', depositCount: '0x2dfdf0b5', isComplianceProvider: '0xff3d4ba1', complianceProviders: '0x05195996', isAssociationRootValid: '0x992d8d62', spentNullifiers: '0x1c70406a', associationRootPublishedAt: '0x68c46b72', deposit: '0xca0c3ad5', setComplianceProvider: '0xecc19f6c', publishAssociationRoot: '0x9f92a5b1', withdraw: '0xb7433691', // R5/R6 additions proposeAssociationRoot: '0x73fc3b6a', // proposeAssociationRoot(bytes32) challengeAssociationRoot: '0x45cb9085', // challengeAssociationRoot(bytes32,string) — bond required dismissAssociationRootChallenge: '0x793819b9', associationRootProposedAt: '0x68c46b72', associationRootChallenged: '0x8a76c7c8', associationRootDismissCount: '0x6c7560c8', associationRootChallenger: '0xbb84a4c8', CHALLENGE_BOND: '0x9790ce71', MAX_DISMISSALS_PER_ROOT: '0x9c9eea4f', CHALLENGE_BOND_BURN: '0x9f3098ff', BOND_TOKEN: '0x10ee36c5', isKnownRoot: '0x2b7ac3f3', getLastRoot: '0x4cf088d9', ROOT_HISTORY_SIZE: '0xb0d3e9b4', TREE_DEPTH: '0x7d9f6c6f', nextLeafIndex: '0x3b15aabf', ASSOCIATION_ROOT_CHALLENGE_WINDOW: '0x37dd9e6c', } as const; export class AereCompliancePoolClient { constructor(public readonly provider: RpcProvider, public readonly address: string) {} async denomination(): Promise { return BigInt(await call(this.provider, this.address, POOL_SEL.DENOMINATION)); } async depositCount(): Promise { return BigInt(await call(this.provider, this.address, POOL_SEL.depositCount)); } async latestDepositRoot(): Promise { return await call(this.provider, this.address, POOL_SEL.latestDepositRoot); } async isAssociationRootValid(root: string): Promise { return BigInt(await call(this.provider, this.address, POOL_SEL.isAssociationRootValid + encB(root))) === 1n; } async isSpent(nullifierHash: string): Promise { return BigInt(await call(this.provider, this.address, POOL_SEL.spentNullifiers + encB(nullifierHash))) === 1n; } /** User deposit. Caller must have approved TOKEN to this address for DENOMINATION first. */ deposit(from: string, commitment: string, travelRuleHash: string, complianceProvider: string, travelRuleUri: string): Promise { const u = encString(travelRuleUri); const head = encB(commitment) + encB(travelRuleHash) + encA(complianceProvider) + encU(4n * 32n); return send(this.provider, from, this.address, POOL_SEL.deposit + head + u.body); } publishAssociationRoot(from: string, root: string): Promise { return send(this.provider, from, this.address, POOL_SEL.publishAssociationRoot + encB(root)); } setComplianceProvider(from: string, provider: string, allowed: boolean): Promise { return send(this.provider, from, this.address, POOL_SEL.setComplianceProvider + encA(provider) + pad32(allowed ? '1' : '0')); } /* R5/R6 additions — challenge-bond + on-chain Merkle tree views. */ /** Foundation proposes a new association root. Anyone can publishAssociationRoot * after the 24h challenge window expires + no active challenge. */ proposeAssociationRoot(from: string, root: string): Promise { return send(this.provider, from, this.address, POOL_SEL.proposeAssociationRoot + encB(root)); } /** Challenge a proposed root. Caller MUST have approved CHALLENGE_BOND of * BOND_TOKEN (= same TOKEN as DENOMINATION) to the pool first. If * Foundation dismisses the challenge, bond is burned. */ challengeAssociationRoot(from: string, root: string, reason: string): Promise { const r = encString(reason); // head: root(32) + offset(32) = 2 slots const head = encB(root) + encU(2n * 32n); return send(this.provider, from, this.address, POOL_SEL.challengeAssociationRoot + head + r.body); } /** Foundation dismisses a challenge — burns bond, resets 24h window. */ dismissChallenge(from: string, root: string): Promise { return send(this.provider, from, this.address, POOL_SEL.dismissAssociationRootChallenge + encB(root)); } async challengeBond(): Promise { return BigInt(await call(this.provider, this.address, POOL_SEL.CHALLENGE_BOND)); } async maxDismissalsPerRoot(): Promise { return Number(BigInt(await call(this.provider, this.address, POOL_SEL.MAX_DISMISSALS_PER_ROOT))); } async dismissCount(root: string): Promise { return Number(BigInt(await call(this.provider, this.address, POOL_SEL.associationRootDismissCount + encB(root)))); } async challenger(root: string): Promise { const r = await call(this.provider, this.address, POOL_SEL.associationRootChallenger + encB(root)); return '0x' + r.slice(-40); } async isKnownRoot(root: string): Promise { return BigInt(await call(this.provider, this.address, POOL_SEL.isKnownRoot + encB(root))) === 1n; } async getLastRoot(): Promise { return await call(this.provider, this.address, POOL_SEL.getLastRoot); } async nextLeafIndex(): Promise { return Number(BigInt(await call(this.provider, this.address, POOL_SEL.nextLeafIndex))); } /** * Compute the actual leaf as the contract does (R6 fix): leaf = * keccak256(commitment || depositor). SDK consumers MUST build their ZK * proof against this leaf, not against the raw commitment. */ static computeActualLeaf(commitment: string, depositor: string): string { // simple wrapper — ethers/viem caller computes keccak256 over the // packed encoding. Pure helper returns the input format the verifier // public-input slot expects. return JSON.stringify({ commitment, depositor, note: 'leaf = keccak256(commitment || depositor) — compute via your hashing lib' }); } /** Withdraw via zk proof. Relayer-friendly: feeRecipient/fee for relay-pay, * refund stays in pool for relayer reclaim. */ withdraw( from: string, proof: string, depositRoot: string, associationRoot: string, nullifierHash: string, recipient: string, feeRecipient: string, fee: bigint, refund: bigint ): Promise { // 8 head slots: proof_offset, depositRoot, associationRoot, nullifierHash, // recipient, feeRecipient, fee, refund const offProof = 8n * 32n; const p = encBytes(proof); const head = encU(offProof) + encB(depositRoot) + encB(associationRoot) + encB(nullifierHash) + encA(recipient) + encA(feeRecipient) + encU(fee) + encU(refund); return send(this.provider, from, this.address, POOL_SEL.withdraw + head + p.body); } }