sdk-js/src/compliance/NewComplianceClients.ts
Liviu 15d9f6e64d feat(sdk): R5+R6 surfaces — CoreBook client + sAERE drip + Compliance challenge bond
New & expanded clients reflecting all R4/R5/R6 contract hardening:

- corebook/AereCoreBookClient.ts (NEW):
  - Side+TimeInForce enums; pre-computed selectors for place/placeProtected/
    placeWithSlippage/placeWithSlippageProtected/iocFill/cancel/flushFees/
    sweepDust/claim
  - read* helpers for bestBid, bestAsk, pendingSinkFee, TAKER_FEE_BPS,
    PRICE_TICK, LOT_SIZE, QUOTE_DECIMALS_FACTOR, BASE_DECIMALS_FACTOR,
    PRICE_SCALE, totalClaimableQuote/Base, per-user claimable lookups
  - computeQuote(price, qty) helper mirrors on-chain formula
  - recommendedSlippage(side, limit, qty, bps) computes maxAvgPriceWei for
    real sandwich protection — discourages naive iocFill
  - Docstrings explicitly state which entrypoint to use for crossing vs
    resting orders (R6 HIGH-5/MED-9 enforcement)

- saere/sAEREClient.ts (R5/R6):
  - atomicState() — single-call snapshot of {bal, totalAssets, undist,
    totalSupply, rewardRate, periodFinish, lastObservedBalance} eliminating
    cross-block drift in fuzz/dashboard reads
  - readUndistributed(), readRewardRate(), readPeriodFinish()
  - encodeSync() — permissionless drip refresh

- compliance/NewComplianceClients.ts (R5/R6):
  - proposeAssociationRoot + challengeAssociationRoot (bond required) +
    dismissChallenge entrypoints
  - challengeBond(), maxDismissalsPerRoot(), dismissCount(root),
    challenger(root), isKnownRoot(root), getLastRoot(), nextLeafIndex()
  - removed publishDepositRoot (R5: roots are on-chain Merkle, not pushed)
  - static computeActualLeaf helper note: leaf = keccak256(commitment, sender)
    so SDK consumers build ZK proofs against the bound leaf

- index.ts: export corebook surface

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 12:56:44 +03:00

261 lines
11 KiB
TypeScript

/**
* 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<unknown>;
}
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<string> {
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<string> {
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<boolean> {
const r = await call(this.provider, this.address, ATT_SEL.isValidNow + encB(attestationId));
return BigInt(r) === 1n;
}
async subjectCurrentlyAttested(subject: string, schemaId: string): Promise<boolean> {
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<string> {
return await call(this.provider, this.address, ATT_SEL.latestFor + encB(subject) + encB(schemaId));
}
async isAuthorisedAttestor(schemaId: string, attestor: string): Promise<boolean> {
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<string> {
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<string> {
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<string> {
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<string> {
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<bigint> {
return BigInt(await call(this.provider, this.address, POOL_SEL.DENOMINATION));
}
async depositCount(): Promise<bigint> {
return BigInt(await call(this.provider, this.address, POOL_SEL.depositCount));
}
async latestDepositRoot(): Promise<string> {
return await call(this.provider, this.address, POOL_SEL.latestDepositRoot);
}
async isAssociationRootValid(root: string): Promise<boolean> {
return BigInt(await call(this.provider, this.address, POOL_SEL.isAssociationRootValid + encB(root))) === 1n;
}
async isSpent(nullifierHash: string): Promise<boolean> {
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<string> {
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<string> {
return send(this.provider, from, this.address, POOL_SEL.publishAssociationRoot + encB(root));
}
setComplianceProvider(from: string, provider: string, allowed: boolean): Promise<string> {
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<string> {
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<string> {
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<string> {
return send(this.provider, from, this.address, POOL_SEL.dismissAssociationRootChallenge + encB(root));
}
async challengeBond(): Promise<bigint> {
return BigInt(await call(this.provider, this.address, POOL_SEL.CHALLENGE_BOND));
}
async maxDismissalsPerRoot(): Promise<number> {
return Number(BigInt(await call(this.provider, this.address, POOL_SEL.MAX_DISMISSALS_PER_ROOT)));
}
async dismissCount(root: string): Promise<number> {
return Number(BigInt(await call(this.provider, this.address, POOL_SEL.associationRootDismissCount + encB(root))));
}
async challenger(root: string): Promise<string> {
const r = await call(this.provider, this.address, POOL_SEL.associationRootChallenger + encB(root));
return '0x' + r.slice(-40);
}
async isKnownRoot(root: string): Promise<boolean> {
return BigInt(await call(this.provider, this.address, POOL_SEL.isKnownRoot + encB(root))) === 1n;
}
async getLastRoot(): Promise<string> {
return await call(this.provider, this.address, POOL_SEL.getLastRoot);
}
async nextLeafIndex(): Promise<number> {
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<string> {
// 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);
}
}