Compare commits

...

7 Commits

Author SHA1 Message Date
Liviu
b45682d578 feat(corebook): CoreBookClient + AereCoreBookClient selector fixes + AERE_COREBOOK markets gate
- CoreBookClient: full read/write surface for AereCoreBookV0 (placeGTC/IOC/
  withSlippage/Protected, cancel, claim base+quote, flushFees; bestBid/bestAsk/
  marketInfo/depth/openOrdersOf via Lens), price math mirrors (_computeQuote,
  _lockBuyFunds incl. 256-wei buffer), orderId derivation (pre-incremented
  nonce keccak), event decode for OrderPlaced/Filled/OrderCancelled/Claimed.
  Verified 63/63 against ethers v6 ground truth.
- AereCoreBookClient (pre-existing): fixed 11 wrong read selectors (bestBid,
  bestAsk, pendingSinkFee, TAKER_FEE_BPS, PRICE_TICK, LOT_SIZE, decimals
  factors, PRICE_SCALE, cancel-delay constants) - every read would have
  thrown before.
- addresses.ts: AERE_COREBOOK = { markets: {} } - founder gate, no market
  listed until explicit approval.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:44:18 +03:00
Liviu
094688d1d1 feat(addresses): 21 audit-pass-stack contracts deployed 2026-06-09
sAERE, AereSink, BugBountyVault, Sanctions/TravelRule/Forensic/ZKScreen/
AIProof/Attestation/BestEx, SettlementHub, CoinbaseSplitterV2, Agent +
AERE402Facilitator (CREATE-predicted pair), Delegate7702 + DelegationRegistry,
InsuranceFund, NavOracle, StateChannels, RaaSFactory.

Deployed via Foundation MetaMask through aere.network/deploy.html (standalone
HTML deploy page after React AdminDeploy SPA had MetaMask-mobile issues).

Pending USDC.e bridge bootstrap: CoreBookV0, LendingMarket x 6,
CompliancePool, SpokePool, Hyperlane Warp Routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 21:22:33 +03:00
Liviu
fbc6d4f68a 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
Liviu
99abfff98a feat(sdk): SDK clients for the 2026-06-08 ship batch
Six new clients (dependency-free EIP-1193 wrappers, precomputed
4-byte selectors, ABI tuple/string encoders inline):

- AereAgentBondClient        — bondOf, isBonded, postBond, requestWithdraw,
                               withdraw, slash; oracle-only on slash
- AereAIReputationClient     — scoreOf, statsOf, attest (delta -1/0/+1)
- AereInferNetClient         — modelEpochCount, registerModel, commitEpoch;
                               EU AI Act Article 12 provider surface
- AereAgentMemoryVaultClient — grant, revoke, write, canReadNow,
                               permissionOf; user-owned namespace
- AereAttestationGatewayClient — publishSchema, setAttestor, attest,
                                 revoke, isValidNow, subjectCurrentlyAttested
- AereCompliancePoolClient   — deposit, withdraw, publishAssociationRoot,
                               setComplianceProvider; Privacy Pools surface

All exported from the package barrel. Zero TS errors.
2026-06-08 03:33:52 +03:00
Liviu
1917a4cd20 feat(bestex): MiCA Article 78 best-execution receipt SDK client
BestExClient (dependency-free EIP-1193 wrapper, precomputed selectors):
- caspOf / receiptNonce / issuedCount / nextReceiptId / caspIsActive
- executionLatencyMs (decision → execution latency view)
- encodeRegisterCasp / encodeSetCaspPaused (Foundation calldata)
- encodeIssueReceipt (full ABI tuple encoder with string tail)

Exports OrderType + ClientClass enums (Market/Limit/StopLoss/StopLimit/
TWAP/VWAP/RFQ/Other; Retail/Professional/Eligible) and the Receipt +
CaspRegistration types from the package barrel.

Matches the contract shipped 2026-06-08 (172/172 tests). Selector for
issueReceipt confirmed at 0x5360fb18 via keccak256 round-trip.
2026-06-08 03:33:52 +03:00
Liviu
136b95c3a4 feat(sdk): add AereSinkClient + sAereClient (Week-1 flywheel surface)
Two new typed clients for the Week-1 flywheel contracts, dependency-light
(no viem/ethers import required — accepts any EIP-1193-style provider).

src/sink/AereSinkClient.ts
  - readConfig(): all 8 immutable parameters in one batched eth_call sequence
    (AERE token, BURN_VAULT, SAERE_VAULT, DEX_ROUTER, 4 bps values, slippage cap).
  - readSplitPercentages(): convenience that returns burn/buyback/staker
    as human-readable percentages.
  - encodeFlush(token, amount): calldata for the integrator-side flush call.
  - encodeSweepDust(token): calldata for the permissionless dust-sweep.

src/saere/sAEREClient.ts
  - readVaultState(): live snapshot — underlying address, totalAssets,
    totalSupply, pricePerShare, decimals.
  - balanceOf(user): per-user sAERE share balance.
  - convertToAssets / convertToShares: round-trip conversion helpers.
  - encodeDeposit / encodeWithdraw / encodeRedeem: ERC-4626 write calldata.

Both clients precompute 4-byte function selectors at module load so the
SDK doesn't depend on a keccak helper. ABI-encoded address and uint
arguments built manually so consumers can use this with either a
high-level library (viem, ethers) or raw fetch against rpc.aere.network.

Re-exported from src/index.ts under 'export * from sink/saere' so the
SDK keeps a single import path.
2026-06-08 03:33:52 +03:00
AERE Foundation
88626e43ae v0.16.1 — Tier 1.14–1.16 additions, Tier 1.17 reverted (AereUSDC removed)
Tier 1.11 sub-second blocks (no new contracts), Tier 1.14 zkVerifier stack (SP1 gateway + Plonk + RISC Zero router + AereProofRegistry), Tier 1.15 passkey wallets v1, Tier 1.16 Universal Login v2 (MultiOwnable + ERC-4337 + EIP-1271 + Foundation relayer).

Tier 1.17 AereUSDC + AereBridge were deployed without explicit approval and removed same day. AERE remains the only token on chain 2800.
2026-06-01 14:58:41 +03:00
27 changed files with 3545 additions and 3 deletions

View File

@ -0,0 +1,85 @@
// agent-402-client.js
//
// Drop-in example: agent-side AERE402 client. Calls a paid endpoint,
// receives 402, signs the quote via the agent's signing key, retries.
//
// Run:
// yarn add ethers
// AGENT_PRIVATE_KEY=0x... AGENT_ID=1234 ENDPOINT=http://localhost:3000/v1/inference node agent-402-client.js
//
// Prereq: the agent's signing key (AGENT_PRIVATE_KEY) must already be
// registered on AereAgent with sufficient prepaid balance + un-exceeded
// rate limits.
const { ethers } = require('ethers');
const AGENT_PK = process.env.AGENT_PRIVATE_KEY;
const AGENT_ID = process.env.AGENT_ID;
const ENDPOINT = process.env.ENDPOINT ?? 'http://localhost:3000/v1/inference';
if (!AGENT_PK || !AGENT_ID) {
console.error('AGENT_PRIVATE_KEY + AGENT_ID env required');
process.exit(1);
}
const signer = new ethers.Wallet(AGENT_PK);
(async () => {
console.log(`agent ${AGENT_ID} calling ${ENDPOINT}...`);
// 1. Unpaid request.
const r1 = await fetch(ENDPOINT);
if (r1.status !== 402) {
console.log(`unexpected status ${r1.status} on first call`);
console.log(await r1.text());
return;
}
const quoteJson = r1.headers.get('x-aere402-quote');
if (!quoteJson) throw new Error('server did not return X-AERE402-Quote');
const quote = JSON.parse(quoteJson);
console.log(' received quote:', quote);
// 2. Build EIP-712 domain + types.
const domain = {
name: 'AERE402Facilitator',
version: '1',
chainId: quote.chainId,
verifyingContract: quote.facilitator,
};
const types = {
PaymentAuth: [
{ name: 'agentId', type: 'uint256' },
{ name: 'token', type: 'address' },
{ name: 'payee', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'resourceId', type: 'bytes32' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
const value = {
agentId: BigInt(AGENT_ID),
token: quote.token,
payee: quote.payee,
amount: BigInt(quote.amount),
resourceId: quote.resourceId,
nonce: BigInt(quote.nonce),
deadline: BigInt(quote.deadline),
};
// 3. Sign.
const signature = await signer.signTypedData(domain, types, value);
console.log(` signed: ${signature.slice(0, 18)}...`);
// 4. Retry with the signature.
const r2 = await fetch(ENDPOINT, {
headers: {
'X-AERE402-Sig': signature,
'X-AERE402-AgentId': AGENT_ID,
'X-AERE402-Quote': quoteJson,
},
});
console.log(` retry status: ${r2.status}`);
console.log(` tx hash: ${r2.headers.get('x-aere402-txhash')}`);
console.log(' body:', await r2.json());
})();

View File

@ -0,0 +1,84 @@
// express-402-paid-endpoint.js
//
// Drop-in example: a paid `/v1/inference` endpoint that charges $0.0125
// in USDC.e per call via AERE402 settlement on Chain 2800.
//
// Run:
// yarn add express @aere/sdk-js ethers
// FACILITATOR=0x... PAYEE=0x... PROVIDER_PRIVATE_KEY=0x... node express-402-paid-endpoint.js
//
// Then the agent-side does:
// 1. curl http://localhost:3000/v1/inference
// → 402 with X-AERE402-Quote JSON header
// 2. agent EIP-712-signs the quote using its registered signing key
// 3. curl -H "X-AERE402-Sig: 0x.." -H "X-AERE402-AgentId: 1234" \
// -H "X-AERE402-Quote: <original-quote-json>" \
// http://localhost:3000/v1/inference
// → 200 with the actual inference output, X-AERE402-TxHash header set
const express = require('express');
const { ethers } = require('ethers');
const { createAere402Middleware } = require('@aere/sdk-js');
const FACILITATOR = process.env.FACILITATOR;
const USDC_E = process.env.USDC_E ?? '0x0000000000000000000000000000000000000000';
const PAYEE = process.env.PAYEE;
const PROVIDER_PK = process.env.PROVIDER_PRIVATE_KEY;
const RPC_URL = process.env.RPC_URL ?? 'https://rpc.aere.network';
const CHAIN_ID = parseInt(process.env.CHAIN_ID ?? '2800', 10);
if (!FACILITATOR || !PAYEE || !PROVIDER_PK) {
console.error('FACILITATOR, PAYEE, and PROVIDER_PRIVATE_KEY env vars required');
process.exit(1);
}
const provider = new ethers.JsonRpcProvider(RPC_URL);
const providerWallet = new ethers.Wallet(PROVIDER_PK, provider);
// Real `settle()` ABI from AERE402Facilitator. Pulled minimal — just what we call.
const FAC_ABI = [
'function settle(uint256 agentId, address token, address payee, uint256 amount, bytes32 resourceId, uint256 nonce, uint256 deadline, bytes signature) external',
];
const facContract = new ethers.Contract(FACILITATOR, FAC_ABI, providerWallet);
const charge = createAere402Middleware({
facilitator: FACILITATOR,
chainId: CHAIN_ID,
token: USDC_E,
payee: PAYEE,
pricer: () => 12500n, // $0.0125 in USDC.e (6 decimals)
submit: async (auth, signature) => {
const tx = await facContract.settle(
BigInt(auth.agentId),
auth.token,
auth.payee,
BigInt(auth.amount),
auth.resourceId,
BigInt(auth.nonce),
BigInt(auth.deadline),
signature,
);
const rcpt = await tx.wait();
return rcpt.hash;
},
});
const app = express();
app.get('/v1/inference', charge, (req, res) => {
// The middleware paid us — we can read req.aere402 for receipt info.
const { txHash, auth } = req.aere402;
console.log(`paid call: agent=${auth.agentId} amount=${auth.amount} tx=${txHash}`);
res.json({
output: 'Pretend this is an LLM completion.',
aere402: { txHash, amountCharged: auth.amount, token: auth.token },
});
});
const PORT = parseInt(process.env.PORT ?? '3000', 10);
app.listen(PORT, () => {
console.log(`paid endpoint running at http://localhost:${PORT}/v1/inference`);
console.log(`facilitator: ${FACILITATOR}`);
console.log(`payee: ${PAYEE}`);
console.log(`provider wallet: ${providerWallet.address}`);
});

View File

@ -1,6 +1,6 @@
{
"name": "@aere/sdk",
"version": "0.3.0",
"version": "0.16.1",
"description": "Official AERE Network SDK — typed ethers v6 client for the 16 contracts deployed on AERE L1 (chain ID 2800). WAERE, oracle, identity, faucet, card-escrow, delegated + locked staking, governance, AMM factory, bridge, NFT + marketplace, mining subscriptions.",
"type": "module",
"main": "./dist/index.js",

View File

@ -1,6 +1,6 @@
/**
* Canonical AERE Network mainnet contract addresses (chain 2800).
* Last updated: 2026-05-31 · SDK v0.5.0
* Last updated: 2026-06-01 · SDK v0.16.1 (Tier 1.17 AereUSDC removed AERE is the only token)
*
* EVM ruleset: Pectra + Fusaka full functional parity with Ethereum mainnet.
* Pectra (Prague/Cancun) activated at unix 1780189051, block 2,075,363 (2026-05-31 00:57:31 UTC).
@ -60,6 +60,37 @@ export const AERE_MAINNET = {
AereStaking: '0xAbDb01d9A4f41792129b2654Fb6DDB9689360DEc', // delegated, 8% APY, 7-day unbonding
AereConsensus: '0xF8bDDad4aDACF9d38711e8f9aFC8a2697aBF0d47',
// ── Audit-pass-stack (deployed 2026-06-09 via Foundation MetaMask) ────────
sAERE: '0xA2125bE9C6fd4196D9F94757Df18B3a2A5e650b0', // R6-hardened ERC-4626 with 7-day drip
AereSink: '0x69581B86A48161b067Ff4E01544780625B231676', // Immutable 3-bucket router 15/40/45
AereBugBountyVault: '0x253fDCb248649396CBDaD320F81869A570d69cD3', // 5% max payout
AereSanctionsRegistry: '0xb7d235718D99560F6EA4Fc5eAea2F8a306A3Cacf',
AereTravelRuleHashRegistry: '0xcF0E2e010E6e4506672019b1e570874AEeBC4c84',
AereStateChannels: '0x64488eda27fA5b55A277ac8D386E169DC78cd7b6', // 24h challenge window
AereRaaSFactory: '0x8C1b0018ab8C4299a75621a4BdD3cF26971B26Cd', // 1000 AERE bond, 10% sink share, 6h window
ChainalysisOracleWrapper: '0x1B7Be82C80f368f75Cb3807B1bc05E86A498f85c',
AereForensicEventRegistry: '0x4a7526A068e5DDE9788f6571E4A99095b14C6fff',
AereZKScreen: '0xE9da9c5F40c2CDfda368885832C59609286e04ee', // SP1Gateway-backed
AereAIProof: '0xFf92c669AbF4C1DAE31eBFCC017764036d9D97e6',
AereAttestationGateway: '0x9bdacA8dfF39Fc688e8D3c4bbA13bCFC0580c325',
AereBestExReceipt: '0x3c80BD6fa0d9a2274c7fBD5943B5EC03313D92d7', // MiCA Article 78
AereSettlementHub: '0x2a02fD80c16293D2B5D8a295F31D1a6E6a582c02',
AereCoinbaseSplitterV2: '0x8C1A48eFA57b66fEE743A00E3899c29ad3Fd27b4',
AereAgent: '0xE96396B4b596B3A74e4195Be12aADd5257863536',
AERE402Facilitator: '0xbA6e6700D629a5E3C885778a42885a944CA84E56',
AereDelegate7702: '0x5673D92080efbd0987402E9335c14200d0a5EaeF',
AereDelegationRegistry: '0x6c25c07D134713b6C2F8E19D807423f022903D63',
AereInsuranceFund: '0x5Ab95C549c2A2b07913Df7edD4a16fd108B7CAAC', // 7-day cooldown
AereNavOracle: '0xC8D12E44f10b03477330b35115b432750831fEBD',
// ── PENDING (blocked on Hyperlane USDC.e bootstrap post-MEXC payment) ────
// AereCoreBookV0 — per-market deploy, needs BASE + QUOTE=USDC.e
// AereLendingMarket × 6 — needs USDC.e + bridged RWAs
// AereCompliancePool — needs USDC.e as TOKEN
// AereSpokePool — needs USDC.e
// AereHypERC20Synthetic/Collateral — Hyperlane Warp Routes (Ethereum-side gas required)
// RWATransferAdapter — needs Foundation NAV aggregators
// AereCompliancePoolSP1Verifier — needs VKEY from cargo-prove build
// ── dApp contracts (deployed 2026-05-22, owner = Foundation) ──
AereSwapFactory: '0xf0a8df7BDc25721892475B21271e52D77B0e84DC', // V2 AMM factory
AereSwapRouter: '0x7526B2E5526EfA84018378b60F2844Dad77523D8', // V2-style periphery (add/remove liquidity + swap)
@ -112,6 +143,39 @@ export const AERE_MAINNET = {
AereFeeMonetization: '0x6b62DC6cC974F779354c953F41b64a7aB994dd98', // Developer fee-share NFTs + 5% Foundation treasury slice
// NOTE: AERE is the network's only native token. No stablecoin issuance.
// ── Tier-1.14 zk-verifier stack (deployed 2026-05-31) ──────────
// Multi-prover zk verification. Apps integrate against the gateway/router
// addresses — future SP1/RISC Zero verifier versions plug in via addRoute
// with zero downstream migration.
//
// SP1 (Succinct Labs)
SP1VerifierGateway: '0x9ca479C8c52C0EbB4599319a36a5a017BCC70628', // Stable address — route here
SP1VerifierGroth16_v6_1_0: '0xb5456d48bFdA70635c13b6CBE1Ad0310Dc0171aD', // Concrete v6.1.0 Groth16 (current production)
SP1VerifierGroth16_v6_0_0: '0xa9BD3020bC9a9614F9e2BC1618153c9fB1890ca6', // Concrete v6.0.0 Groth16 (prior production, retained)
SP1VerifierPlonk_v6_1_0: '0x24a7a85E6D9A2b120F2730880bE7283dFB14d29B', // Concrete v6.1.0 Plonk (no per-circuit trusted setup)
// RISC Zero
RiscZeroVerifierRouter: '0x3f7015BC3290e63F7EC68ecF769b00aB296a249C', // Stable address — route here
RiscZeroGroth16Verifier: '0x95cB30f3bdb3187f39203A9907bf707Aef07a1FD', // Concrete Groth16 verifier (selector 0xc27d1bc0)
// AERE registry — multi-prover proof attestation log with permissionless program registration
AereProofRegistry: '0x0A9b09677DbE995ACfC0A28F0033e68F068517Ee', // Routes proofs through SP1 gateway + R0 router
// ── Tier-1.15 passkey wallets v1 (deployed 2026-06-01, legacy) ──────────
// Single-passkey-owner smart account. Retained for the original demo account.
// For new wallets use AerePasskeyAccountFactoryV2 below.
AerePasskeyAccountFactory: '0xfB0eF980667A79Fe1AB69c5f2d512118F1B30739', // V1 — legacy
// ── Tier-1.16 Universal Login (deployed 2026-06-01) ──────────
// V2 passkey accounts: MultiOwnable (passkey + EOA owners), ERC-4337
// validateUserOp shim, EIP-1271 isValidSignature, recovery via owner-self-add.
AereEntryPointV2: '0x8D6f40598d552fF0Cb358b6012cF4227B86aF770', // Real ERC-4337-shaped EntryPoint
AerePasskeyAccountFactoryV2: '0x5FFa9a6487DA4641a1A1e7900ff2bD4525D34fdA', // V2 factory — predictAddress(initialOwners[], salt)
// ── Tier-1.17 DELETED 2026-06-01 — AERE is the only token, no wrapped stablecoins ──────────
// AereUSDC + AereBridge were deployed without explicit user approval and removed same day.
// Bridge watcher container archived at /opt/aere-bridge-watcher.killed-2026-06-01 on aere-infra.
// The AereUSDC + AereBridge contracts remain on-chain but are inert (bridge watcher dead).
// Foundation will pause+renounce AereUSDC.MINTER_ROLE when Foundation key is available.
// ── Deferred to future phases (not yet deployed) ──────────
// AereSwapRouter, AereYieldFarm,
// AereVesting, AereInsurancePool, AereLaunchpad, AereAirdrop, AereMultiSig,
@ -125,3 +189,16 @@ export type AereContractName = keyof Omit<
typeof AERE_MAINNET,
'chainId' | 'chainIdHex' | 'rpc' | 'ws' | 'explorer' | 'indexer'
>;
// ── AereCoreBookV0 — native CLOB (per-market deploys) ─────────────────────
// One AereCoreBookV0 contract per (base, quote) market. NOTHING is deployed
// or listed yet: market listing is founder-gated, and the trade UI must fall
// back to demo mode while this map is empty. Keys are human market symbols
// ("WAERE/USDC.e"), values are the per-market AereCoreBookV0 addresses.
export const AERE_COREBOOK = {
markets: {
// no markets listed — founder gate
} as Record<string, `0x${string}`>,
} as const;
export type CoreBookMarketSymbol = keyof typeof AERE_COREBOOK.markets;

View File

@ -0,0 +1,203 @@
/**
* AERE402Middleware Express/Connect-compatible middleware for charging
* AI agents via the AERE402 HTTP 402 protocol.
*
* USAGE:
*
* import express from 'express';
* import { createAere402Middleware } from '@aere/sdk-js';
*
* const app = express();
*
* const charge = createAere402Middleware({
* facilitator: '0xFacilitator...',
* chainId: 2800,
* token: '0xUSDC.e...',
* payee: '0xMyProviderWallet...',
* pricer: (req) => 12500n, // 12500 USDC.e ($0.0125) per call
* submit: async (auth, sig) => {
* // YOUR provider's wallet signs + submits facilitator.settle(...)
* // Returns the txHash. See examples/express-402-paid-endpoint.js
* return submitSettleViaMyWallet(auth, sig);
* },
* });
*
* app.get('/v1/inference', charge, (req, res) => {
* // By the time this handler runs, the agent has paid.
* res.json({ output: runInference(req.body) });
* });
*
* FLOW handled by this middleware:
* 1. No X-AERE402-Sig header respond 402 with quote (X-AERE402-Quote)
* 2. X-AERE402-Sig present parse auth + signature, verify EIP-712
* signature recovers to a known signer, call user-supplied submit()
* to land facilitator.settle on-chain, attach result to req, call next()
* 3. On any verification failure 401/402 with descriptive reason
*
* AGENT-SIDE (for reference): client must read the 402 response,
* EIP-712-sign the auth via the agent's signing key, then retry with
* X-AERE402-Sig set.
*/
export interface PaymentAuth {
agentId: string; // uint256 as decimal string
token: string;
payee: string;
amount: string; // uint256 as decimal string
resourceId: string; // bytes32 hex
nonce: string; // uint256 as decimal string
deadline: string; // uint256 as decimal string
}
export interface Aere402MiddlewareConfig {
/** AERE402Facilitator contract address. */
facilitator: string;
/** Chain ID (2800 for AERE mainnet). */
chainId: number;
/** Payment token (e.g. USDC.e). */
token: string;
/** Provider's receiving address. */
payee: string;
/** Function to compute the price per request. Receives the Express req. */
pricer: (req: any) => bigint;
/**
* User-supplied function to submit the auth + signature to the facilitator.
* Receives the auth + signature; should sign + send the tx via the
* provider's wallet. Returns the on-chain tx hash.
*/
submit: (auth: PaymentAuth, signature: string) => Promise<string>;
/**
* Function to derive a resourceId (bytes32) for the request. Defaults to
* keccak256-equivalent of `${req.method} ${req.path}`. Override if you
* want finer-grained resource pricing.
*/
resourceIdFor?: (req: any) => string;
/** Quote validity in seconds. Default 300 (5 min). */
deadlineSeconds?: number;
/** Nonce generator. Default: monotonic per-process counter starting at Date now / 1000. */
nextNonce?: () => string;
}
const DEFAULT_DEADLINE = 300;
let _nonceCounter = BigInt(Math.floor(Date.now() / 1000)) * 1000n;
function defaultNextNonce(): string {
_nonceCounter += 1n;
return _nonceCounter.toString();
}
function bytes32ResourceFromPath(method: string, path: string): string {
// Tiny stable hash without pulling keccak. Hex of ASCII path padded/truncated to 64.
// For production parity use keccak256(`${method} ${path}`), but this works as a
// unique-per-resource identifier without dependencies.
const raw = `${method} ${path}`;
const buf = Buffer.from(raw, 'utf8').toString('hex').slice(0, 64).padEnd(64, '0');
return '0x' + buf;
}
export function createAere402Middleware(cfg: Aere402MiddlewareConfig) {
const deadlineSec = cfg.deadlineSeconds ?? DEFAULT_DEADLINE;
const nextNonce = cfg.nextNonce ?? defaultNextNonce;
const resourceIdFor = cfg.resourceIdFor ?? ((req: any) => bytes32ResourceFromPath(req.method ?? 'GET', req.path ?? ''));
return async function aere402(req: any, res: any, next: (err?: any) => void) {
try {
const sigHeader = req.headers?.['x-aere402-sig'];
const agentIdHeader = req.headers?.['x-aere402-agentid'];
if (!sigHeader || !agentIdHeader) {
// Step 1: emit 402 with quote.
const amount = cfg.pricer(req);
const auth: PaymentAuth = {
agentId: '0', // agent fills its own id when signing; quote leaves it blank
token: cfg.token,
payee: cfg.payee,
amount: amount.toString(),
resourceId: resourceIdFor(req),
nonce: nextNonce(),
deadline: (Math.floor(Date.now() / 1000) + deadlineSec).toString(),
};
res.statusCode = 402;
res.setHeader('X-AERE402-Quote', JSON.stringify({
facilitator: cfg.facilitator,
chainId: cfg.chainId,
...auth,
}));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: 'Payment Required',
quote: { facilitator: cfg.facilitator, chainId: cfg.chainId, ...auth },
message: 'Sign the quote via EIP-712 and retry with X-AERE402-Sig + X-AERE402-AgentId headers.',
}));
return;
}
// Step 2: parse + reconstruct the auth and submit.
const quoteHeader = req.headers?.['x-aere402-quote'];
if (!quoteHeader) {
res.statusCode = 400;
res.end('Missing X-AERE402-Quote header on retry.');
return;
}
let quote: PaymentAuth;
try { quote = JSON.parse(quoteHeader as string); }
catch { res.statusCode = 400; res.end('Invalid X-AERE402-Quote JSON.'); return; }
// Override agentId from header (the signer's identity).
const auth: PaymentAuth = { ...quote, agentId: String(agentIdHeader) };
// Deadline guard server-side too.
if (Number(auth.deadline) < Math.floor(Date.now() / 1000)) {
res.statusCode = 408;
res.end('Quote expired. Request a fresh 402.');
return;
}
// Step 3: submit on-chain via user-supplied callback.
let txHash: string;
try {
txHash = await cfg.submit(auth, String(sigHeader));
} catch (e: any) {
res.statusCode = 402;
res.end(`Settlement failed: ${e?.message ?? 'unknown'}`);
return;
}
// Attach settlement info to the request for the downstream handler.
(req as any).aere402 = { auth, signature: sigHeader, txHash };
res.setHeader('X-AERE402-TxHash', txHash);
next();
} catch (e) {
next(e);
}
};
}
/**
* Standalone helper: build the EIP-712 typed-data object the AGENT-SIDE
* wallet must sign. Same shape as buildAuthTypedData in AereAgentClient.ts.
* Re-exported here so providers can validate before charging.
*/
export function buildAere402TypedData(facilitator: string, chainId: number, auth: PaymentAuth): Record<string, unknown> {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
PaymentAuth: [
{ name: 'agentId', type: 'uint256' },
{ name: 'token', type: 'address' },
{ name: 'payee', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'resourceId', type: 'bytes32' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'PaymentAuth',
domain: { name: 'AERE402Facilitator', version: '1', chainId, verifyingContract: facilitator },
message: auth,
};
}

View File

@ -0,0 +1,205 @@
// AereAgentClient — dependency-light EIP-1193 wrapper for the AereAgent
// machine-account registry. Used by operators (humans / orgs) running an
// AI agent on Chain 2800.
export interface Eip1193Provider {
request(args: { method: string; params?: unknown[] }): Promise<any>;
}
// 4-byte selectors (precomputed; never call eth_sign just to derive them).
const SEL = {
registerAgent: '0xa30b3acc', // registerAgent(address,bytes32,bytes32,uint128,uint128,uint128,string)
setSigner: '0x6c19e783', // setSigner(uint256,address)
setRateLimits: '0x44dec0e2', // setRateLimits(uint256,uint128,uint128,uint128)
setPaused: '0x8456db15', // setPaused(uint256,bool)
topUp: '0xc35a8e8b', // topUp(uint256,address,uint256)
withdraw: '0x069a89ec', // withdraw(uint256,address,uint256,address)
signerOf: '0x9f9eb5b1', // signerOf(uint256) view returns (address)
balances: '0xa7263000', // balances(uint256,address) view returns (uint256)
} as const;
function pad32Hex(hex: string): string {
const h = hex.toLowerCase().replace(/^0x/, '');
return h.padStart(64, '0');
}
function encUint(n: bigint): string {
return pad32Hex(n.toString(16));
}
function encAddr(addr: string): string {
return pad32Hex(addr.replace(/^0x/, ''));
}
function encBytes32(b: string): string {
return pad32Hex(b.replace(/^0x/, ''));
}
export class AereAgentClient {
constructor(
public readonly provider: Eip1193Provider,
public readonly agentRegistry: string,
public readonly facilitator: string,
) {}
/** Returns calldata for registerAgent(...). */
encodeRegisterAgent(opts: {
signer: string;
passkeyPubX?: string;
passkeyPubY?: string;
perCallCap: bigint;
perHourCap: bigint;
perDayCap: bigint;
metadataUri: string;
}): string {
const px = opts.passkeyPubX ?? '0x' + '0'.repeat(64);
const py = opts.passkeyPubY ?? '0x' + '0'.repeat(64);
const meta = Buffer.from(opts.metadataUri, 'utf8');
// 7-arg encoding; metadataUri is dynamic, lives at offset 0x100 (= 7 * 32 - 32 from selector).
// Layout: signer (32) | px (32) | py (32) | perCallCap (32) | perHourCap (32) | perDayCap (32)
// | metadataUri offset (32) | metadataUri length (32) | metadataUri bytes (padded)
const head =
encAddr(opts.signer) +
encBytes32(px) +
encBytes32(py) +
encUint(opts.perCallCap) +
encUint(opts.perHourCap) +
encUint(opts.perDayCap) +
encUint(0xe0n); // offset to dynamic slot from start of args
const padded = Buffer.alloc(Math.ceil(meta.length / 32) * 32);
meta.copy(padded);
const dyn = encUint(BigInt(meta.length)) + padded.toString('hex');
return SEL.registerAgent + head + dyn;
}
async sendRegisterAgent(from: string, opts: Parameters<AereAgentClient['encodeRegisterAgent']>[0]): Promise<string> {
const data = this.encodeRegisterAgent(opts);
return this.provider.request({
method: 'eth_sendTransaction',
params: [{ from, to: this.agentRegistry, data }],
});
}
async sendTopUp(from: string, agentId: bigint, token: string, amount: bigint): Promise<string> {
const data = SEL.topUp + encUint(agentId) + encAddr(token) + encUint(amount);
return this.provider.request({
method: 'eth_sendTransaction',
params: [{ from, to: this.agentRegistry, data }],
});
}
async sendSetPaused(from: string, agentId: bigint, paused: boolean): Promise<string> {
const data = SEL.setPaused + encUint(agentId) + encUint(paused ? 1n : 0n);
return this.provider.request({
method: 'eth_sendTransaction',
params: [{ from, to: this.agentRegistry, data }],
});
}
async getSigner(agentId: bigint): Promise<string> {
const data = SEL.signerOf + encUint(agentId);
const res: string = await this.provider.request({
method: 'eth_call',
params: [{ to: this.agentRegistry, data }, 'latest'],
});
return '0x' + res.slice(-40);
}
async getBalance(agentId: bigint, token: string): Promise<bigint> {
const data = SEL.balances + encUint(agentId) + encAddr(token);
const res: string = await this.provider.request({
method: 'eth_call',
params: [{ to: this.agentRegistry, data }, 'latest'],
});
return BigInt(res);
}
}
/* ---------------------------------------------------------------------- *
* AERE402 payment authorisation helpers (EIP-712 typed data).
*
* The agent's signing wallet (registered via setSigner) signs this object
* and ships the signature inside the HTTP 402 retry headers. The provider
* POSTs (auth, signature) to AERE402Facilitator.settle().
* ---------------------------------------------------------------------- */
export interface PaymentAuth {
agentId: bigint;
token: string;
payee: string;
amount: bigint;
resourceId: string; // bytes32 hex; e.g. keccak256("GET /v1/inference")
nonce: bigint;
deadline: bigint; // unix seconds
}
export function buildAuthTypedData(opts: {
chainId: number;
facilitator: string;
auth: PaymentAuth;
}): Record<string, unknown> {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
PaymentAuth: [
{ name: 'agentId', type: 'uint256' },
{ name: 'token', type: 'address' },
{ name: 'payee', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'resourceId', type: 'bytes32' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'PaymentAuth',
domain: {
name: 'AERE402Facilitator',
version: '1',
chainId: opts.chainId,
verifyingContract: opts.facilitator,
},
message: {
agentId: opts.auth.agentId.toString(),
token: opts.auth.token,
payee: opts.auth.payee,
amount: opts.auth.amount.toString(),
resourceId: opts.auth.resourceId,
nonce: opts.auth.nonce.toString(),
deadline: opts.auth.deadline.toString(),
},
};
}
const SEL_SETTLE = '0x0c0a40c3'; // settle(uint256,address,address,uint256,bytes32,uint256,uint256,bytes)
export class AERE402SettlementClient {
constructor(public readonly provider: Eip1193Provider, public readonly facilitator: string) {}
/** Encode and submit settle(...) from the provider's address. */
async sendSettle(from: string, auth: PaymentAuth, signature: string): Promise<string> {
const sig = signature.replace(/^0x/, '');
const sigPadded = Buffer.alloc(Math.ceil(sig.length / 2 / 32) * 32);
Buffer.from(sig, 'hex').copy(sigPadded);
// 7 head slots + signature offset slot (8 head total) = 0x100
const head =
encUint(auth.agentId) +
encAddr(auth.token) +
encAddr(auth.payee) +
encUint(auth.amount) +
encBytes32(auth.resourceId) +
encUint(auth.nonce) +
encUint(auth.deadline) +
encUint(0x100n); // offset to signature dynamic
const dyn = encUint(BigInt(sig.length / 2)) + sigPadded.toString('hex');
const data = SEL_SETTLE + head + dyn;
return this.provider.request({
method: 'eth_sendTransaction',
params: [{ from, to: this.facilitator, data }],
});
}
}

View File

@ -0,0 +1,227 @@
/**
* NewAgenticClients SDK clients for the 2026-06-08 ship batch.
* Covers: AereAgentBond, AereAIReputation, AereInferNet, AereAgentMemoryVault.
*
* Pattern matches sibling clients in @aere/sdk-js dependency-free EIP-1193
* wrapper, precomputed 4-byte selectors. Callers supply provider + signer.
*/
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): { offset: bigint; body: string } {
const b = Buffer.from(s, 'utf8');
const padded = Buffer.alloc(Math.ceil(b.length / 32) * 32);
b.copy(padded);
return { offset: BigInt(b.length), body: encU(BigInt(b.length)) + padded.toString('hex') };
}
/* ============================== AereAgentBond ============================== */
const BOND_SEL = {
AERE: '0xe3b11fda',
SINK: '0xae9fc205',
WITHDRAWAL_COOLDOWN: '0xfeedab00',
bondOf: '0xb416623b',
isBonded: '0x52bd7947',
isOracle: '0xa97e5c93',
oracles: '0x2857373a',
totalSlashedToSink: '0x8ca95389',
postBond: '0x18b348af',
requestWithdraw: '0x1fad8338',
withdraw: '0x40e5e9aa',
slash: '0x6d68fdd7',
} as const;
export interface BondInfo {
amount: bigint; requestedAt: bigint; lifetimeSlashed: bigint; exists: boolean;
}
export class AereAgentBondClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async bondOf(operator: string, agentId: string): Promise<BondInfo> {
const res = await call(this.provider, this.address, BOND_SEL.bondOf + encA(operator) + encB(agentId));
const r = res.replace(/^0x/, '');
return {
amount: BigInt('0x' + r.slice(0, 64)),
requestedAt: BigInt('0x' + r.slice(64, 128)),
lifetimeSlashed: BigInt('0x' + r.slice(128, 192)),
exists: BigInt('0x' + r.slice(192, 256)) === 1n,
};
}
async isBonded(operator: string, agentId: string, minBond: bigint): Promise<boolean> {
const data = BOND_SEL.isBonded + encA(operator) + encB(agentId) + encU(minBond);
return BigInt(await call(this.provider, this.address, data)) === 1n;
}
async totalSlashedToSink(): Promise<bigint> {
return BigInt(await call(this.provider, this.address, BOND_SEL.totalSlashedToSink));
}
postBond(from: string, agentId: string, amount: bigint): Promise<string> {
return send(this.provider, from, this.address, BOND_SEL.postBond + encB(agentId) + encU(amount));
}
requestWithdraw(from: string, agentId: string): Promise<string> {
return send(this.provider, from, this.address, BOND_SEL.requestWithdraw + encB(agentId));
}
withdraw(from: string, operator: string, agentId: string): Promise<string> {
return send(this.provider, from, this.address, BOND_SEL.withdraw + encA(operator) + encB(agentId));
}
/** Oracle-only. reasonUri is a free-form ipfs:// or https:// link. */
slash(from: string, operator: string, agentId: string, amount: bigint, reasonHash: string, reasonUri: string): Promise<string> {
const head = encA(operator) + encB(agentId) + encU(amount) + encB(reasonHash) + encU(0xa0n);
const s = encString(reasonUri);
return send(this.provider, from, this.address, BOND_SEL.slash + head + s.body);
}
}
/* ============================ AereAIReputation ============================ */
const REP_SEL = {
BOND: '0xc1c1d218',
MAX_SCORE: '0x27ff6223',
SLASH_UNIT: '0xd50a3e76',
statsOf: '0xcf212c5f',
scoreOf: '0x7377fa36',
attest: '0x37637f60',
isAttestor: '0x2e2f4e24',
attestors: '0xe7eb466f',
} as const;
export interface AgentStats { positiveCount: bigint; disputeCount: bigint; lifetimeSlashed: bigint; }
export class AereAIReputationClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async scoreOf(operator: string, agentId: string): Promise<bigint> {
return BigInt(await call(this.provider, this.address, REP_SEL.scoreOf + encA(operator) + encB(agentId)));
}
async statsOf(operator: string, agentId: string): Promise<AgentStats> {
const res = (await call(this.provider, this.address, REP_SEL.statsOf + encA(operator) + encB(agentId))).replace(/^0x/, '');
return {
positiveCount: BigInt('0x' + res.slice(0, 64)),
disputeCount: BigInt('0x' + res.slice(64, 128)),
lifetimeSlashed: BigInt('0x' + res.slice(128, 192)),
};
}
/** delta ∈ {-1, 0, 1}. Attestor-only. */
attest(from: string, operator: string, agentId: string, delta: number, evidenceHash: string, evidenceUri: string): Promise<string> {
// int8 is sign-extended to 32 bytes; negatives become 0xFF...FF
const deltaWord = delta < 0 ? 'f'.repeat(64 - 2) + (256 + delta).toString(16).padStart(2, '0') : pad32(delta.toString(16));
const head = encA(operator) + encB(agentId) + deltaWord + encB(evidenceHash) + encU(0xa0n);
const s = encString(evidenceUri);
return send(this.provider, from, this.address, REP_SEL.attest + head + s.body);
}
}
/* ============================== AereInferNet ============================== */
const INFER_SEL = {
modelEpochCount: '0xf407005b',
modelOf: '0x1548a0e9',
epochOf: '0xc1a5ae12',
registerModel: '0x703722ac',
commitEpoch: '0x7f11fb10',
verifyInference: '0xa2a98a64',
} as const;
export class AereInferNetClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async modelEpochCount(provider_: string, modelId: string): Promise<bigint> {
return BigInt(await call(this.provider, this.address, INFER_SEL.modelEpochCount + encA(provider_) + encB(modelId)));
}
/** RiskTier: 0=Minimal, 1=Limited, 2=High, 3=Unacceptable. */
registerModel(
from: string, modelId: string, modelDigest: string, attestationDigest: string,
risk: number, modelName: string, attestationUri: string
): Promise<string> {
// head = modelId(32) + modelDigest(32) + attestationDigest(32) + risk(32) + offset_name(32) + offset_attUri(32)
// tail = nameLen+nameBody, attUriLen+attUriBody
const name = encString(modelName);
const att = encString(attestationUri);
const offsetName = 6n * 32n;
const nameBodyLen = name.body.length / 2; // including length word + padded
const offsetAtt = offsetName + BigInt(nameBodyLen);
const head =
encB(modelId) + encB(modelDigest) + encB(attestationDigest) +
pad32(risk.toString(16)) + encU(offsetName) + encU(offsetAtt);
return send(this.provider, from, this.address, INFER_SEL.registerModel + head + name.body + att.body);
}
commitEpoch(
from: string, modelId: string, epochId: bigint, root: string,
inferenceCount: bigint, metadataUri: string
): Promise<string> {
const m = encString(metadataUri);
const head = encB(modelId) + encU(epochId) + encB(root) + encU(inferenceCount) + encU(0xa0n);
return send(this.provider, from, this.address, INFER_SEL.commitEpoch + head + m.body);
}
}
/* =========================== AereAgentMemoryVault ========================== */
const MEM_SEL = {
permissionOf: '0xef928267',
canReadNow: '0x80b4e9ba',
headOf: '0xcc92a9ed',
nextVersion: '0xba7badeb',
grant: '0xbb9f078e',
revoke: '0x88e62721',
write: '0x2a7ae64c',
} as const;
export interface Permission { canRead: boolean; canWrite: boolean; validUntil: bigint; exists: boolean; }
export interface MemEntry { contentHash: string; blobUri: string; writer: string; version: bigint; writtenAt: bigint; }
export class AereAgentMemoryVaultClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async canReadNow(user: string, agent: string, slot: string): Promise<boolean> {
const data = MEM_SEL.canReadNow + encA(user) + encA(agent) + encB(slot);
return BigInt(await call(this.provider, this.address, data)) === 1n;
}
async permissionOf(user: string, agent: string, slot: string): Promise<Permission> {
const data = MEM_SEL.permissionOf + encA(user) + encA(agent) + encB(slot);
const r = (await call(this.provider, this.address, data)).replace(/^0x/, '');
return {
canRead: BigInt('0x' + r.slice(0, 64)) === 1n,
canWrite: BigInt('0x' + r.slice(64, 128)) === 1n,
validUntil: BigInt('0x' + r.slice(128, 192)),
exists: BigInt('0x' + r.slice(192, 256)) === 1n,
};
}
/** User grants per-slot permission. validUntil=0n means no expiry. */
grant(from: string, agent: string, slot: string, canRead: boolean, canWrite: boolean, validUntil: bigint): Promise<string> {
const data = MEM_SEL.grant + encA(agent) + encB(slot) +
pad32(canRead ? '1' : '0') + pad32(canWrite ? '1' : '0') + encU(validUntil);
return send(this.provider, from, this.address, data);
}
revoke(from: string, agent: string, slot: string): Promise<string> {
return send(this.provider, from, this.address, MEM_SEL.revoke + encA(agent) + encB(slot));
}
/** Agent writes memory entry. contentHash = SHA256(encrypted blob). */
write(from: string, user: string, slot: string, contentHash: string, blobUri: string): Promise<string> {
const u = encString(blobUri);
const head = encA(user) + encB(slot) + encB(contentHash) + encU(0x80n);
return send(this.provider, from, this.address, MEM_SEL.write + head + u.body);
}
}

18
src/agentic/index.ts Normal file
View File

@ -0,0 +1,18 @@
export { AereAgentClient, AERE402SettlementClient, buildAuthTypedData } from './AereAgentClient.js';
export type { Eip1193Provider, PaymentAuth as AgentPaymentAuth } from './AereAgentClient.js';
export { createAere402Middleware, buildAere402TypedData } from './AERE402Middleware.js';
export type { Aere402MiddlewareConfig, PaymentAuth } from './AERE402Middleware.js';
// 2026-06-08 ship batch — accountability + AI Act + portable memory.
export {
AereAgentBondClient,
AereAIReputationClient,
AereInferNetClient,
AereAgentMemoryVaultClient,
} from './NewAgenticClients.js';
export type {
BondInfo,
AgentStats,
Permission as MemoryVaultPermission,
MemEntry,
} from './NewAgenticClients.js';

227
src/bestex/BestExClient.ts Normal file
View File

@ -0,0 +1,227 @@
/**
* BestExClient typed read/write helpers for AereBestExReceipt
* (MiCA Article 78 best-execution receipts on-chain).
*
* Contract: aerenew/contracts/contracts/bestex/AereBestExReceipt.sol
* Audited: 7/7 internal tests passing, deployed 2026-06-08.
*
* Pattern matches LendingMarketClient: dependency-free EIP-1193 wrapper
* with precomputed 4-byte selectors. No ethers/viem import callers
* supply their own provider/signer.
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
export interface BestExConfig {
address: `0x${string}`;
provider: RpcProvider;
}
export enum OrderType {
Market = 0,
Limit = 1,
StopLoss = 2,
StopLimit = 3,
TWAP = 4,
VWAP = 5,
RFQ = 6,
Other = 7,
}
export enum ClientClass {
Retail = 0,
Professional = 1,
Eligible = 2,
}
export interface Receipt {
casp: `0x${string}`;
clientHash: `0x${string}`;
clientClass: ClientClass;
orderId: `0x${string}`;
orderType: OrderType;
instrumentSymbol: `0x${string}`;
requestedQty: bigint;
executedQty: bigint;
executedPrice: bigint;
executedVenue: `0x${string}`;
alternativesRoot: `0x${string}`;
algorithmRef: `0x${string}`;
decisionTsMs: bigint;
executionTsMs: bigint;
evidenceUri: string;
}
export interface CaspRegistration {
licenceRef: `0x${string}`;
registeredAt: bigint;
paused: boolean;
exists: boolean;
}
const SEL = {
FOUNDATION: '0x65df2e51',
caspOf: '0x0f5b6f91',
receiptNonce: '0x3700af14',
issuedCount: '0xa13eb74b',
nextReceiptId: '0xf8193baa',
caspIsActive: '0x83bb246c',
executionLatencyMs: '0x654deb2e',
registerCasp: '0x21231b18',
setCaspPaused: '0xa6ae9dbc',
} as const;
/* --------------------------- ABI helpers (no deps) -------------------------- */
const hex = (n: bigint, bytes = 32): string =>
n.toString(16).padStart(bytes * 2, '0');
const addrPad = (a: `0x${string}`): string => a.slice(2).toLowerCase().padStart(64, '0');
const bytes32Pad = (b: `0x${string}`): string => b.slice(2).toLowerCase().padStart(64, '0');
const boolPad = (b: boolean): string => (b ? '1'.padStart(64, '0') : '0'.padStart(64, '0'));
/** keccak256 substitute via the provider's web3_sha3 isn't always available;
* callers compute keccak themselves for clientHash/instrumentSymbol etc. */
/* --------------------------------- client --------------------------------- */
export class BestExClient {
private readonly addr: `0x${string}`;
private readonly provider: RpcProvider;
constructor(cfg: BestExConfig) {
this.addr = cfg.address;
this.provider = cfg.provider;
}
private async call(data: string): Promise<string> {
const r = await this.provider.request({
method: 'eth_call',
params: [{ to: this.addr, data }, 'latest'],
});
return r as string;
}
/* ------------------------------ views ------------------------------ */
async foundation(): Promise<`0x${string}`> {
const r = await this.call(SEL.FOUNDATION);
return ('0x' + r.slice(26)) as `0x${string}`;
}
async caspOf(casp: `0x${string}`): Promise<CaspRegistration> {
const r = await this.call(SEL.caspOf + addrPad(casp));
// returns (bytes32, uint64, bool, bool) — but solc packs into 2 slots:
// slot 1: licenceRef (32 bytes)
// slot 2: registeredAt (8) | paused (1) | exists (1) — right-aligned in 32-byte word
const raw = r.slice(2);
const licenceRef = ('0x' + raw.slice(0, 64)) as `0x${string}`;
const packed = raw.slice(64, 128);
// bytes from low-to-high: exists at byte 0, paused at byte 1, registeredAt 2..10
const exists = parseInt(packed.slice(62, 64), 16) !== 0;
const paused = parseInt(packed.slice(60, 62), 16) !== 0;
const registeredAt = BigInt('0x' + packed.slice(44, 60));
return { licenceRef, registeredAt, paused, exists };
}
async receiptNonce(casp: `0x${string}`): Promise<bigint> {
const r = await this.call(SEL.receiptNonce + addrPad(casp));
return BigInt(r);
}
async issuedCount(casp: `0x${string}`): Promise<bigint> {
const r = await this.call(SEL.issuedCount + addrPad(casp));
return BigInt(r);
}
async nextReceiptId(casp: `0x${string}`): Promise<`0x${string}`> {
return (await this.call(SEL.nextReceiptId + addrPad(casp))) as `0x${string}`;
}
async caspIsActive(casp: `0x${string}`): Promise<boolean> {
const r = await this.call(SEL.caspIsActive + addrPad(casp));
return parseInt(r.slice(-2), 16) !== 0;
}
async executionLatencyMs(receiptId: `0x${string}`): Promise<bigint> {
const r = await this.call(SEL.executionLatencyMs + bytes32Pad(receiptId));
return BigInt(r);
}
/* ------------------------------ writes ----------------------------- */
/** Build calldata for Foundation to register a CASP. */
encodeRegisterCasp(casp: `0x${string}`, licenceRef: `0x${string}`): `0x${string}` {
return (SEL.registerCasp + addrPad(casp) + bytes32Pad(licenceRef)) as `0x${string}`;
}
/** Build calldata for Foundation to (un)pause a CASP. */
encodeSetCaspPaused(casp: `0x${string}`, paused: boolean): `0x${string}` {
return (SEL.setCaspPaused + addrPad(casp) + boolPad(paused)) as `0x${string}`;
}
/**
* Build calldata for a CASP to mint a receipt. ABI-encode the tuple at the
* exact layout solc uses for `Receipt calldata r`.
*
* Layout of `Receipt`:
* offset 0x00 address casp
* offset 0x20 bytes32 clientHash
* offset 0x40 uint8 clientClass
* offset 0x60 bytes32 orderId
* offset 0x80 uint8 orderType
* offset 0xa0 bytes32 instrumentSymbol
* offset 0xc0 uint128 requestedQty
* offset 0xe0 uint128 executedQty
* offset 0x100 uint128 executedPrice
* offset 0x120 bytes32 executedVenue
* offset 0x140 bytes32 alternativesRoot
* offset 0x160 bytes32 algorithmRef
* offset 0x180 uint64 decisionTsMs
* offset 0x1a0 uint64 executionTsMs
* offset 0x1c0 string evidenceUri (offset tail)
*
* For a single tuple parameter solc encodes the outer head as a pointer
* (0x20) to the tuple body. So: selector || 0x20 || tuple-body || tail.
*/
encodeIssueReceipt(r: Receipt): `0x${string}` {
// selector for issueReceipt((address,bytes32,uint8,bytes32,uint8,bytes32,uint128,uint128,uint128,bytes32,bytes32,bytes32,uint64,uint64,string))
// Computed once in JS:
// keccak256("issueReceipt((address,bytes32,uint8,bytes32,uint8,bytes32,uint128,uint128,uint128,bytes32,bytes32,bytes32,uint64,uint64,string))").slice(0, 10)
// = 0x5360fb18
const selector = '0x5360fb18';
const head =
hex(0x20n) + // offset to tuple
addrPad(r.casp) +
bytes32Pad(r.clientHash) +
hex(BigInt(r.clientClass)) +
bytes32Pad(r.orderId) +
hex(BigInt(r.orderType)) +
bytes32Pad(r.instrumentSymbol) +
hex(r.requestedQty) +
hex(r.executedQty) +
hex(r.executedPrice) +
bytes32Pad(r.executedVenue) +
bytes32Pad(r.alternativesRoot) +
bytes32Pad(r.algorithmRef) +
hex(r.decisionTsMs) +
hex(r.executionTsMs) +
hex(0x1e0n); // string head: offset within tuple body to string tail
// string tail = 32-byte length + utf-8 bytes padded to 32
const uriBytes = new TextEncoder().encode(r.evidenceUri);
const lenWord = hex(BigInt(uriBytes.length));
const padded = Array.from(uriBytes).map((b) => b.toString(16).padStart(2, '0')).join('');
const padTo32 = (s: string) => s + '0'.repeat((64 - (s.length % 64)) % 64);
const tail = lenWord + padTo32(padded);
return (selector + head + tail) as `0x${string}`;
}
}
export default BestExClient;

2
src/bestex/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { BestExClient, OrderType, ClientClass } from './BestExClient.js';
export type { BestExConfig, Receipt, CaspRegistration, RpcProvider } from './BestExClient.js';

113
src/channels/index.ts Normal file
View File

@ -0,0 +1,113 @@
/**
* State Channels SDK AereStateChannels.
*
* Contract: aerenew/contracts/contracts/channels/AereStateChannels.sol
*
* Off-chain transactions: both parties hold a current `ChannelState` and
* exchange new ones (signed by both) on every payment. On dispute, EITHER
* party submits the latest state to start a 24h challenge window; the other
* can override with a higher-nonce state.
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
export interface ChannelState {
channelId: string; // bytes32
nonce: bigint;
balanceA: bigint;
balanceB: bigint;
htlcLockedFromA: bigint;
htlcLockedFromB: bigint;
lockHash: string; // bytes32 ('0x000...' if no HTLC active)
lockTimeout: bigint; // unix seconds
}
const pad32 = (h: string) => h.toLowerCase().replace(/^0x/, '').padStart(64, '0');
const encUint = (n: bigint) => pad32(n.toString(16));
const encAddr = (a: string) => pad32(a.replace(/^0x/, ''));
const encBytes32 = (b: string) => pad32(b.replace(/^0x/, ''));
async function ethCall(p: RpcProvider, to: string, data: string): Promise<string> {
return await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] }) as string;
}
async function ethSend(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 encChannelState(s: ChannelState): string {
return encBytes32(s.channelId) + encUint(s.nonce) + encUint(s.balanceA) + encUint(s.balanceB) +
encUint(s.htlcLockedFromA) + encUint(s.htlcLockedFromB) +
encBytes32(s.lockHash) + encUint(s.lockTimeout);
}
function encDynBytes(hex: string): { offset: bigint; encoded: string } {
const clean = hex.replace(/^0x/, '');
const bytes = Buffer.from(clean, 'hex');
const padded = Buffer.alloc(Math.ceil(bytes.length / 32) * 32);
bytes.copy(padded);
return { offset: 0n, encoded: encUint(BigInt(bytes.length)) + padded.toString('hex') };
}
const SEL = {
CHALLENGE_WINDOW: '0xe9333e75',
channels: '0xb29a8140', // channels(bytes32)
open: '0x73c0d3a9', // open(address,address,address,uint256,uint256)
cooperativeClose: '0xa9fbe4b6', // cooperativeClose(ChannelState,bytes,bytes)
startChallenge: '0xd1e26000', // startChallenge(ChannelState,bytes,bytes)
overrideChallenge: '0x44b04beb', // overrideChallenge(ChannelState,bytes,bytes)
finalize: '0x6e3e98bf', // finalize(bytes32)
settleHtlc: '0xa84f3c0e', // settleHtlc(bytes32,bytes)
withdraw: '0x51cff8d9', // withdraw(bytes32)
} as const;
export class AereStateChannelsClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async open(from: string, partyA: string, partyB: string, token: string, depositA: bigint, depositB: bigint): Promise<string> {
const data = SEL.open + encAddr(partyA) + encAddr(partyB) + encAddr(token) + encUint(depositA) + encUint(depositB);
return ethSend(this.provider, from, this.address, data);
}
async finalize(from: string, channelId: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.finalize + encBytes32(channelId));
}
async settleHtlc(from: string, channelId: string, preimage: string): Promise<string> {
const dyn = encDynBytes(preimage);
const data = SEL.settleHtlc + encBytes32(channelId) + encUint(64n) + dyn.encoded;
return ethSend(this.provider, from, this.address, data);
}
async withdraw(from: string, channelId: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.withdraw + encBytes32(channelId));
}
/** Helper: encode the EIP-712 typed-data object for off-chain signing. */
buildStateTypedData(chainId: number, state: ChannelState): Record<string, unknown> {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
ChannelState: [
{ name: 'channelId', type: 'bytes32' },
{ name: 'nonce', type: 'uint256' },
{ name: 'balanceA', type: 'uint256' },
{ name: 'balanceB', type: 'uint256' },
{ name: 'htlcLockedFromA', type: 'uint256' },
{ name: 'htlcLockedFromB', type: 'uint256' },
{ name: 'lockHash', type: 'bytes32' },
{ name: 'lockTimeout', type: 'uint256' },
],
},
primaryType: 'ChannelState',
domain: { name: 'AereStateChannels', version: '1', chainId, verifyingContract: this.address },
message: state,
};
}
}

View File

@ -0,0 +1,260 @@
/**
* 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);
}
}

185
src/compliance/index.ts Normal file
View File

@ -0,0 +1,185 @@
/**
* Compliance SDK clients AereProof v0 stack + ZKScreen + AIProof.
*
* Contracts:
* aerenew/contracts/contracts/compliance/AereSanctionsRegistry.sol
* aerenew/contracts/contracts/compliance/ChainalysisOracleWrapper.sol
* aerenew/contracts/contracts/compliance/AereTravelRuleHashRegistry.sol
* aerenew/contracts/contracts/compliance/AereForensicEventRegistry.sol
* aerenew/contracts/contracts/compliance/AereZKScreen.sol
* aerenew/contracts/contracts/compliance/AereAIProof.sol
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
const pad32 = (h: string) => h.toLowerCase().replace(/^0x/, '').padStart(64, '0');
const encUint = (n: bigint) => pad32(n.toString(16));
const encAddr = (a: string) => pad32(a.replace(/^0x/, ''));
const encBytes32 = (b: string) => pad32(b.replace(/^0x/, ''));
async function ethCall(p: RpcProvider, to: string, data: string): Promise<string> {
return await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] }) as string;
}
async function ethSend(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 encodeDynamicBytes(hex: string): { offset: string; body: string } {
const clean = hex.replace(/^0x/, '');
const bytes = Buffer.from(clean, 'hex');
const padded = Buffer.alloc(Math.ceil(bytes.length / 32) * 32);
bytes.copy(padded);
return { offset: encUint(BigInt(bytes.length)), body: padded.toString('hex') };
}
/* ===================== AereSanctionsRegistry ====================== */
const SANCT_SEL = {
currentRoot: '0x6f29ed46', // currentRoot()
isSanctioned: '0x7ad8e02b', // isSanctioned(address,uint16,bytes32[])
commitRoot: '0x9b30b9ae', // commitRoot(bytes32,uint16,string) — placeholder
} as const;
export class AereSanctionsRegistryClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async currentRoot(): Promise<string> {
return await ethCall(this.provider, this.address, SANCT_SEL.currentRoot);
}
}
/* ===================== ChainalysisOracleWrapper =================== */
const CHAIN_SEL = {
isSanctioned: '0xdf592f7d', // isSanctioned(address)
upstream: '0x6c0360eb', // upstream()
locked: '0xcf30901', // locked()
} as const;
export class ChainalysisOracleWrapperClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
/** Returns true iff `addr` is sanctioned (fails closed — reverts also return true). */
async isSanctioned(addr: string): Promise<boolean> {
try {
const res = await ethCall(this.provider, this.address, CHAIN_SEL.isSanctioned + encAddr(addr));
return BigInt(res) === 1n;
} catch {
return true; // fails closed
}
}
}
/* ===================== AereTravelRuleHashRegistry ================= */
const TRR_SEL = {
committedHashes: '0x6c5e6e95', // committedHashes(bytes32)
status: '0x200d2ed2', // status(bytes32)
} as const;
export class AereTravelRuleHashRegistryClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
/** State enum: 0=Unknown, 1=Committed, 2=Acknowledged, 3=Disputed. */
async status(hash: string): Promise<number> {
const res = await ethCall(this.provider, this.address, TRR_SEL.status + encBytes32(hash));
return Number(BigInt(res));
}
}
/* ===================== AereForensicEventRegistry ================== */
const FORENSIC_SEL = {
subjectAlertCount: '0x7c1d3d0a', // subjectAlertCount(address)
recentConfirmedAlertCount: '0x44e3a39d', // recentConfirmedAlertCount(address,uint8,uint256)
nextBotId: '0xf4cae09e', // nextBotId()
nextAlertId: '0xa0d49d3a', // nextAlertId()
} as const;
export class AereForensicEventRegistryClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async subjectAlertCount(subject: string): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, FORENSIC_SEL.subjectAlertCount + encAddr(subject)));
}
/** Returns count of CONFIRMED alerts of `minSeverity` or higher within last `windowSeconds`. */
async recentConfirmedAlertCount(subject: string, minSeverity: number, windowSeconds: bigint): Promise<bigint> {
const data = FORENSIC_SEL.recentConfirmedAlertCount + encAddr(subject) +
pad32(minSeverity.toString(16)) + encUint(windowSeconds);
return BigInt(await ethCall(this.provider, this.address, data));
}
}
/* ===================== AereZKScreen =============================== */
const ZK_SEL = {
isCleared: '0xb6a5d7de', // isCleared(address,bytes32,uint256)
clearedAt: '0xc5f4dec0', // clearedAt(address,bytes32)
submitProof: '0x77a91da7', // submitProof(bytes32,bytes,bytes)
} as const;
export class AereZKScreenClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async isCleared(user: string, programVKey: string, minTimestamp: bigint): Promise<boolean> {
const data = ZK_SEL.isCleared + encAddr(user) + encBytes32(programVKey) + encUint(minTimestamp);
const res = await ethCall(this.provider, this.address, data);
return BigInt(res) === 1n;
}
async clearedAt(user: string, programVKey: string): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, ZK_SEL.clearedAt + encAddr(user) + encBytes32(programVKey)));
}
/** Submit an SP1 proof. Caller must equal the `user` encoded in publicValues. */
async submitProof(from: string, programVKey: string, publicValues: string, proof: string): Promise<string> {
const pv = encodeDynamicBytes(publicValues);
const pr = encodeDynamicBytes(proof);
// 3-arg encoding: vKey(32) + offset_pv(32) + offset_proof(32) + len_pv(32) + body_pv + len_proof(32) + body_proof
const pvBodyLen = Math.ceil(publicValues.replace(/^0x/, '').length / 64) * 32; // bytes after length
const offsetPv = 3n * 32n;
const offsetProof = offsetPv + 32n + BigInt(pvBodyLen);
const data = ZK_SEL.submitProof +
encBytes32(programVKey) +
encUint(offsetPv) +
encUint(offsetProof) +
pv.offset + pv.body +
pr.offset + pr.body;
return ethSend(this.provider, from, this.address, data);
}
}
/* ===================== AereAIProof ================================ */
const AI_SEL = {
models: '0x88e98b22', // models(bytes32)
attestationCount: '0x8b2c3a99', // attestationCount(bytes32)
registerModel: '0xa9b5a9eb', // registerModel(bytes32,address,string)
anchor: '0x5b9babe0', // anchor(bytes32,bytes32,bytes32,bytes32,uint256,uint256,bytes)
} as const;
export class AereAIProofClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async attestationCount(modelId: string): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, AI_SEL.attestationCount + encBytes32(modelId)));
}
async registerModel(from: string, modelId: string, signer: string, description: string): Promise<string> {
const desc = Buffer.from(description, 'utf8');
const padded = Buffer.alloc(Math.ceil(desc.length / 32) * 32);
desc.copy(padded);
const head = encBytes32(modelId) + encAddr(signer) + encUint(0x60n); // offset to string
const dyn = encUint(BigInt(desc.length)) + padded.toString('hex');
return ethSend(this.provider, from, this.address, AI_SEL.registerModel + head + dyn);
}
}
// 2026-06-08 ship batch — multi-regulator attestation + privacy pools.
export {
AereAttestationGatewayClient,
AereCompliancePoolClient,
} from './NewComplianceClients.js';

View File

@ -0,0 +1,220 @@
/**
* AereCoreBookClient typed helpers for AereCoreBookV0 native CLOB.
*
* Contract: aerenew/contracts/contracts/corebook/AereCoreBookV0.sol
*
* Post-R6 hardening notes:
* - `place()` is the GTC limit entrypoint; refuses CROSSING orders (use
* iocFill / placeWithSlippage for those).
* - `iocFill(side, worstPrice, qty)` naive IOC sweep capped at worstPrice.
* - `placeWithSlippage(side, price, qty, tif, minBaseFilled, maxQuoteSpent,
* maxAvgPriceWei)` — REAL slippage protection. SDKs MUST prefer this.
* - `placeProtected(...)` adds a per-order cancelDelayBlocks so mempool
* races cannot front-run a maker's cancel.
* - BUY taker pays `principal + maxFee + 256-wei buffer`; maker receives
* FULL `quoteAmount` (no fee skim).
* - On transfer-revert (USDC blacklist, ERC777 hook) the recipient gets a
* claimable credit. Pull via `claim(wantBase)`.
* - When the book is fully empty, `sweepDust()` forwards leftover QUOTE
* (excluding pendingSinkFee + totalClaimableQuote) to AereSink.
*/
import type { RpcProvider } from '../sink/AereSinkClient.js';
export interface AereCoreBookConfig {
address: `0x${string}`;
provider: RpcProvider;
}
export type Side = 0 | 1; // BUY=0, SELL=1
export type TimeInForce = 0 | 1; // GTC=0, IOC=1
/* --------------------------------- selectors --------------------------------- */
const SEL = {
// entrypoints
place: '0x8121866a', // place(uint8,uint128,uint128,uint8)
placeProtected: '0x8af26684', // placeProtected(uint8,uint128,uint128,uint8,uint16)
placeWithSlippage: '0xf4526bbc', // placeWithSlippage(uint8,uint128,uint128,uint8,uint128,uint256,uint128)
placeWithSlippageProtected: '0xc0efac90', // placeWithSlippageProtected(uint8,uint128,uint128,uint8,uint128,uint256,uint16,uint128)
iocFill: '0x5bbb981a', // iocFill(uint8,uint128,uint128)
cancel: '0x81649d06', // cancel(uint128)
flushFees: '0x313d8cb9', // flushFees()
sweepDust: '0xa53df2e2', // sweepDust()
claim: '0x2d81a78e', // claim(bool)
// reads (selectors recomputed 2026-06-11 against AereCoreBookV0.sol — the
// previous values for bestBid/bestAsk/pendingSinkFee + immutables were wrong)
bestBid: '0x20c1614b', // bestBid()
bestAsk: '0xcffb7d15', // bestAsk()
pendingSinkFee: '0x20a0e28c', // pendingSinkFee()
totalClaimableQuote: '0x17554eb7', // totalClaimableQuote()
totalClaimableBase: '0x200d51bc', // totalClaimableBase()
claimableQuote: '0x2d9cf134', // claimableQuote(address)
claimableBase: '0x22046da1', // claimableBase(address)
TAKER_FEE_BPS: '0x2057bfa0', // TAKER_FEE_BPS()
PRICE_TICK: '0xaffabe45', // PRICE_TICK()
LOT_SIZE: '0x7fee67a2', // LOT_SIZE()
QUOTE_DECIMALS_FACTOR: '0x3da2ad02', // QUOTE_DECIMALS_FACTOR()
BASE_DECIMALS_FACTOR: '0x43878833', // BASE_DECIMALS_FACTOR()
PRICE_SCALE: '0xc33f59d3', // PRICE_SCALE()
MAX_CANCEL_DELAY_BLOCKS: '0xef8045a2', // MAX_CANCEL_DELAY_BLOCKS()
DEFAULT_CANCEL_DELAY_BLOCKS: '0x3d81cedd', // DEFAULT_CANCEL_DELAY_BLOCKS()
} as const;
/* ------------------------------ encode helpers ------------------------------ */
function padUint(value: bigint | number, bytes: number = 32): string {
const hex = BigInt(value).toString(16);
return hex.padStart(bytes * 2, '0');
}
function padAddress(addr: `0x${string}`): string {
return addr.slice(2).toLowerCase().padStart(64, '0');
}
function hexToBigInt(hex: string): bigint {
if (!/^0x[0-9a-fA-F]+$/.test(hex)) throw new Error(`CoreBookClient: bad hex: ${hex}`);
return BigInt(hex);
}
/* --------------------------------- client --------------------------------- */
export class AereCoreBookClient {
readonly address: `0x${string}`;
private readonly provider: RpcProvider;
constructor(cfg: AereCoreBookConfig) {
this.address = cfg.address;
this.provider = cfg.provider;
}
/* ----------------------------- read methods ----------------------------- */
async readBestBid(): Promise<bigint> { return hexToBigInt(await this._call(SEL.bestBid)); }
async readBestAsk(): Promise<bigint> { return hexToBigInt(await this._call(SEL.bestAsk)); }
async readPendingSinkFee(): Promise<bigint> { return hexToBigInt(await this._call(SEL.pendingSinkFee)); }
async readTakerFeeBps(): Promise<bigint> { return hexToBigInt(await this._call(SEL.TAKER_FEE_BPS)); }
async readPriceTick(): Promise<bigint> { return hexToBigInt(await this._call(SEL.PRICE_TICK)); }
async readLotSize(): Promise<bigint> { return hexToBigInt(await this._call(SEL.LOT_SIZE)); }
async readQuoteFactor(): Promise<bigint> { return hexToBigInt(await this._call(SEL.QUOTE_DECIMALS_FACTOR)); }
async readBaseFactor(): Promise<bigint> { return hexToBigInt(await this._call(SEL.BASE_DECIMALS_FACTOR)); }
async readPriceScale(): Promise<bigint> { return hexToBigInt(await this._call(SEL.PRICE_SCALE)); }
async readTotalClaimableQuote(): Promise<bigint> { return hexToBigInt(await this._call(SEL.totalClaimableQuote)); }
async readTotalClaimableBase(): Promise<bigint> { return hexToBigInt(await this._call(SEL.totalClaimableBase)); }
async readClaimableQuote(user: `0x${string}`): Promise<bigint> {
return hexToBigInt(await this._call(SEL.claimableQuote + padAddress(user)));
}
async readClaimableBase(user: `0x${string}`): Promise<bigint> {
return hexToBigInt(await this._call(SEL.claimableBase + padAddress(user)));
}
/** Compute quote = price * qty * QUOTE_FACTOR / (PRICE_SCALE * BASE_FACTOR). */
async computeQuote(price: bigint, qty: bigint): Promise<bigint> {
const [quoteFactor, baseFactor, priceScale] = await Promise.all([
this.readQuoteFactor(), this.readBaseFactor(), this.readPriceScale(),
]);
return (price * qty * quoteFactor) / (priceScale * baseFactor);
}
/* -------------------------- write calldata builders -------------------------- */
/**
* GTC limit entrypoint REFUSES crossing orders. For crossing use
* `iocFill` or `placeWithSlippage`. Applies DEFAULT_CANCEL_DELAY_BLOCKS=2.
*/
encodePlace(side: Side, price: bigint, quantity: bigint, tif: TimeInForce): `0x${string}` {
return (SEL.place
+ padUint(side, 32) + padUint(price, 32) + padUint(quantity, 32) + padUint(tif, 32)) as `0x${string}`;
}
/** GTC + custom cancel-delay. Use placeWithSlippageProtected for crossing. */
encodePlaceProtected(side: Side, price: bigint, quantity: bigint, tif: TimeInForce, cancelDelayBlocks: number): `0x${string}` {
return (SEL.placeProtected
+ padUint(side, 32) + padUint(price, 32) + padUint(quantity, 32)
+ padUint(tif, 32) + padUint(cancelDelayBlocks, 32)) as `0x${string}`;
}
/**
* IOC sweep capped at worstPrice. NO avg-price protection use
* placeWithSlippage with explicit maxAvgPriceWei for surgical-sandwich
* protection.
*/
encodeIocFill(side: Side, worstPrice: bigint, quantity: bigint): `0x${string}` {
return (SEL.iocFill
+ padUint(side, 32) + padUint(worstPrice, 32) + padUint(quantity, 32)) as `0x${string}`;
}
/**
* Recommended entrypoint for any crossing order. Caller MUST set
* `maxAvgPriceWei` strictly tighter than `price` to get real sandwich
* protection (e.g. price*0.995 for 50bps band).
*/
encodePlaceWithSlippage(
side: Side, price: bigint, quantity: bigint, tif: TimeInForce,
minBaseFilled: bigint, maxQuoteSpent: bigint, maxAvgPriceWei: bigint,
): `0x${string}` {
return (SEL.placeWithSlippage
+ padUint(side, 32) + padUint(price, 32) + padUint(quantity, 32) + padUint(tif, 32)
+ padUint(minBaseFilled, 32) + padUint(maxQuoteSpent, 32) + padUint(maxAvgPriceWei, 32)) as `0x${string}`;
}
/** Full protection: crossing + slippage + cancel-delay. */
encodePlaceWithSlippageProtected(
side: Side, price: bigint, quantity: bigint, tif: TimeInForce,
minBaseFilled: bigint, maxQuoteSpent: bigint, cancelDelayBlocks: number, maxAvgPriceWei: bigint,
): `0x${string}` {
return (SEL.placeWithSlippageProtected
+ padUint(side, 32) + padUint(price, 32) + padUint(quantity, 32) + padUint(tif, 32)
+ padUint(minBaseFilled, 32) + padUint(maxQuoteSpent, 32)
+ padUint(cancelDelayBlocks, 32) + padUint(maxAvgPriceWei, 32)) as `0x${string}`;
}
encodeCancel(orderId: bigint): `0x${string}` {
return (SEL.cancel + padUint(orderId, 32)) as `0x${string}`;
}
encodeFlushFees(): `0x${string}` { return SEL.flushFees as `0x${string}`; }
/** Permissionless dust sweep — ONLY succeeds when book is fully empty. */
encodeSweepDust(): `0x${string}` { return SEL.sweepDust as `0x${string}`; }
/**
* Pull accumulated claimable balance after a failed transfer.
* @param wantBase true claim BASE side; false claim QUOTE side.
*/
encodeClaim(wantBase: boolean): `0x${string}` {
return (SEL.claim + padUint(wantBase ? 1 : 0, 32)) as `0x${string}`;
}
/* --------------------------- helper: recommended bands --------------------------- */
/**
* Returns recommended slippage parameters for a crossing order. The caller
* provides desired band (default 50bps); we compute maxAvgPriceWei such
* that the average fill cannot exceed it.
*/
recommendedSlippage(
side: Side, limitPrice: bigint, quantity: bigint, bpsBand: number = 50,
): { minBaseFilled: bigint; maxQuoteSpent: bigint; maxAvgPriceWei: bigint } {
const band = BigInt(bpsBand);
const maxAvg = side === 0
? (limitPrice * (10_000n - band)) / 10_000n
: (limitPrice * (10_000n + band)) / 10_000n;
return {
minBaseFilled: 0n,
maxQuoteSpent: (1n << 256n) - 1n,
maxAvgPriceWei: maxAvg,
};
}
/* ----------------------------- internals ------------------------------ */
private async _call(data: string): Promise<string> {
const result = await this.provider.request({
method: 'eth_call',
params: [{ to: this.address, data }, 'latest'],
});
if (typeof result !== 'string') throw new Error('CoreBookClient: bad eth_call');
return result;
}
}

View File

@ -0,0 +1,724 @@
/**
* CoreBookClient full typed read/write client + off-chain lens for
* AereCoreBookV0 (native CLOB, one contract per market).
*
* Contract: aerenew/contracts/contracts/corebook/AereCoreBookV0.sol
* (R6-hardened, 289/289 tests passing source of truth for all math here).
*
* Pattern: dependency-free EIP-1193 wrapper with precomputed 4-byte
* selectors. Read methods just `eth_call`; write methods build calldata
* + `eth_sendTransaction`. Caller handles signer + gas estimation.
*
* Market conventions (mirrors the contract exactly):
* - Side: 0 = BUY (quote base), 1 = SELL (base quote).
* - TimeInForce: 0 = GTC, 1 = IOC.
* - price is uint128, quote-per-base scaled by PRICE_SCALE = 1e18.
* Verified against `_computeQuote`:
* quote = price * qty * QUOTE_DECIMALS_FACTOR / (1e18 * BASE_DECIMALS_FACTOR)
* For human price P (quote units per base unit) the decimals factors
* cancel, so priceWei = P * 1e18 regardless of token decimals.
* - quantity is uint128 in base smallest units, multiple of LOT_SIZE.
* - BUY locks quote principal + max taker fee + 256-wei buffer
* (`_lockBuyFunds`: MAX_OPEN_ORDERS_PER_MAKER covers worst-case per-fill
* fee-ceiling rounding). SELL locks base. ERC-20 approve required first.
* - `place()` REFUSES crossing orders (CrossingRequiresSlippage) and applies
* a default 2-block cancel delay. Crossing flow must go through
* `iocFill` / `placeWithSlippage(Protected)`.
* - orderId = uint128(keccak256(abi.encode(maker, makerNonce))) the lens
* derives historical ids client-side (see `deriveOrderId`).
*/
import type { RpcProvider } from '../sink/AereSinkClient.js';
import type { Side, TimeInForce } from './AereCoreBookClient.js';
export interface CoreBookClientConfig {
address: `0x${string}`;
provider: RpcProvider;
}
/* ------------------------------- constants ------------------------------- */
/** Contract PRICE_SCALE — price uint128 is quote-per-base scaled by 1e18. */
export const COREBOOK_PRICE_SCALE = 10n ** 18n;
/** `bestAsk()` sentinel when the ask side is empty (type(uint128).max). */
export const COREBOOK_ASK_EMPTY = (1n << 128n) - 1n;
/** `bestBid()` sentinel when the bid side is empty. */
export const COREBOOK_BID_EMPTY = 0n;
/** `_lockBuyFunds` flat buffer = MAX_OPEN_ORDERS_PER_MAKER (256 wei of quote). */
export const COREBOOK_BUY_LOCK_BUFFER = 256n;
/** Contract caps for cancel-delay (blocks). */
export const COREBOOK_DEFAULT_CANCEL_DELAY_BLOCKS = 2;
export const COREBOOK_MAX_CANCEL_DELAY_BLOCKS = 1200;
const UINT128_MAX = (1n << 128n) - 1n;
const UINT256_MAX = (1n << 256n) - 1n;
/* ------------------------------- selectors ------------------------------- */
const SEL = {
// entrypoints
place: '0x8121866a', // place(uint8,uint128,uint128,uint8)
placeProtected: '0x8af26684', // placeProtected(uint8,uint128,uint128,uint8,uint16)
iocFill: '0x5bbb981a', // iocFill(uint8,uint128,uint128)
placeWithSlippage: '0xf4526bbc', // placeWithSlippage(uint8,uint128,uint128,uint8,uint128,uint256,uint128)
placeWithSlippageProtected: '0xc0efac90', // placeWithSlippageProtected(uint8,uint128,uint128,uint8,uint128,uint256,uint16,uint128)
cancel: '0x81649d06', // cancel(uint128)
claim: '0x2d81a78e', // claim(bool)
flushFees: '0x313d8cb9', // flushFees()
sweepDust: '0xa53df2e2', // sweepDust()
// book state
bestBid: '0x20c1614b', // bestBid()
bestAsk: '0xcffb7d15', // bestAsk()
pendingSinkFee: '0x20a0e28c', // pendingSinkFee()
getOrder: '0x117d4128', // getOrder(uint128)
bidHeadAtPrice: '0xf056e2d7', // bidHeadAtPrice(uint128)
askHeadAtPrice: '0xec29c07b', // askHeadAtPrice(uint128)
nextPopulatedAsk: '0xcce549e0', // nextPopulatedAsk(uint128)
nextPopulatedBid: '0xe3340be7', // nextPopulatedBid(uint128)
claimableQuote: '0x2d9cf134', // claimableQuote(address)
claimableBase: '0x22046da1', // claimableBase(address)
makerNonce: '0xcd370bb2', // makerNonce(address)
openOrderCount: '0x639babfb', // openOrderCount(address)
// immutables
BASE: '0xec342ad0', // BASE()
QUOTE: '0x9c579839', // QUOTE()
SINK: '0xae9fc205', // SINK()
PRICE_TICK: '0xaffabe45', // PRICE_TICK()
LOT_SIZE: '0x7fee67a2', // LOT_SIZE()
TAKER_FEE_BPS: '0x2057bfa0', // TAKER_FEE_BPS()
QUOTE_DECIMALS_FACTOR: '0x3da2ad02', // QUOTE_DECIMALS_FACTOR()
BASE_DECIMALS_FACTOR: '0x43878833', // BASE_DECIMALS_FACTOR()
// ERC-20 (approve flow)
allowance: '0xdd62ed3e', // allowance(address,address)
approve: '0x095ea7b3', // approve(address,uint256)
balanceOf: '0x70a08231', // balanceOf(address)
} as const;
/* ----------------------------- event topic0 ------------------------------ */
// keccak256 of the exact event signature strings from AereCoreBookV0.sol
// (enums are uint8 in the ABI signature).
/** OrderPlaced(uint128,address,uint8,uint128,uint128) */
export const TOPIC_ORDER_PLACED =
'0x2229a00bfbf976283c0743a5a848fbf6e1a6843ac1eb8a9744a624a68b57632c';
/** OrderCancelled(uint128,address,uint128) */
export const TOPIC_ORDER_CANCELLED =
'0x5264d0657a58acd302a6fb1f840ac461d0347aae6be5019868b66ee71b1b8cab';
/** Filled(uint128,uint128,address,address,uint8,uint128,uint128,uint256) */
export const TOPIC_FILLED =
'0x39440616edbc0f927b9a918e409c20c622cd6e4570e90f80b679f6181e4f70f7';
/** Claimed(address,address,uint256) */
export const TOPIC_CLAIMED =
'0xf7a40077ff7a04c7e61f6f26fb13774259ddf1b6bce9ecf26a8276cdd3992683';
/* ----------------------------- encode helpers ---------------------------- */
function pad32(hex: string): string {
const h = hex.toLowerCase().replace(/^0x/, '');
return h.padStart(64, '0');
}
function encUint(n: bigint | number): string {
const v = BigInt(n);
if (v < 0n || v > UINT256_MAX) throw new Error(`CoreBookClient: uint out of range: ${v}`);
return pad32(v.toString(16));
}
function encAddr(addr: string): string { return pad32(addr.replace(/^0x/, '')); }
function word(data: string, i: number): bigint {
const h = data.replace(/^0x/, '');
const w = h.slice(i * 64, i * 64 + 64);
if (w.length !== 64) throw new Error('CoreBookClient: short returndata');
return BigInt('0x' + w);
}
function addrWord(data: string, i: number): string {
const h = data.replace(/^0x/, '');
return '0x' + h.slice(i * 64 + 24, i * 64 + 64);
}
function topicAddr(topic: string): string { return '0x' + topic.slice(26).toLowerCase(); }
async function ethCall(p: RpcProvider, to: string, data: string): Promise<string> {
const r = await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] });
if (typeof r !== 'string') throw new Error('CoreBookClient: bad eth_call result');
return r;
}
async function ethSend(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;
}
/* --------------------- keccak-256 (vanilla, no deps) --------------------- */
// Needed ONLY for client-side orderId derivation:
// orderId = uint128(keccak256(abi.encode(maker, nonce)))
// (AereCoreBookV0._nextOrderId — nonce is PRE-incremented, so the first
// order of a maker uses nonce 1). Verified against the canonical Keccak-256
// test vector keccak256("") = 0xc5d2...a456 and ethers.keccak256.
const KECCAK_RC: bigint[] = [
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
];
// rotation offsets r[x][y], flattened as [x + 5y]
const KECCAK_ROT: number[] = [
0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14,
];
const M64 = (1n << 64n) - 1n;
function rotl64(v: bigint, n: number): bigint {
if (n === 0) return v;
return ((v << BigInt(n)) | (v >> BigInt(64 - n))) & M64;
}
function keccakF1600(s: bigint[]): void {
for (let r = 0; r < 24; r++) {
// theta
const c: bigint[] = new Array(5);
for (let x = 0; x < 5; x++) c[x] = s[x] ^ s[x + 5] ^ s[x + 10] ^ s[x + 15] ^ s[x + 20];
for (let x = 0; x < 5; x++) {
const d = c[(x + 4) % 5] ^ rotl64(c[(x + 1) % 5], 1);
for (let y = 0; y < 25; y += 5) s[x + y] ^= d;
}
// rho + pi
const b: bigint[] = new Array(25);
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(s[x + 5 * y], KECCAK_ROT[x + 5 * y]);
}
}
// chi
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 25; y += 5) {
s[x + y] = b[x + y] ^ ((~b[((x + 1) % 5) + y] & M64) & b[((x + 2) % 5) + y]);
}
}
// iota
s[0] ^= KECCAK_RC[r];
}
}
function keccak256Bytes(input: Uint8Array): Uint8Array {
const rate = 136; // 1088-bit rate for keccak-256
const s: bigint[] = new Array(25).fill(0n);
const padLen = rate - (input.length % rate);
const padded = new Uint8Array(input.length + padLen);
padded.set(input);
padded[input.length] = 0x01; // keccak (NOT sha3 0x06) domain padding
padded[padded.length - 1] |= 0x80;
for (let off = 0; off < padded.length; off += rate) {
for (let i = 0; i < rate / 8; i++) {
let lane = 0n;
for (let b2 = 7; b2 >= 0; b2--) lane = (lane << 8n) | BigInt(padded[off + i * 8 + b2]);
s[i] ^= lane;
}
keccakF1600(s);
}
const out = new Uint8Array(32);
for (let i = 0; i < 4; i++) {
let lane = s[i];
for (let b2 = 0; b2 < 8; b2++) { out[i * 8 + b2] = Number(lane & 0xffn); lane >>= 8n; }
}
return out;
}
function hexToBytes(hexNo0x: string): Uint8Array {
const out = new Uint8Array(hexNo0x.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hexNo0x.slice(i * 2, i * 2 + 2), 16);
return out;
}
/**
* Derive a CoreBook orderId exactly like `_nextOrderId`:
* orderId = uint128(keccak256(abi.encode(maker, nonce)))
* The contract pre-increments, so a maker's first order uses nonce = 1 and
* the latest used nonce equals `makerNonce(maker)`.
*/
export function deriveOrderId(maker: string, nonce: bigint): bigint {
const hash = keccak256Bytes(hexToBytes(encAddr(maker) + encUint(nonce)));
let v = 0n;
for (const b of hash.subarray(16)) v = (v << 8n) | BigInt(b); // low 128 bits
return v;
}
/* --------------------------- price / qty helpers -------------------------- */
/**
* Convert a human decimal price string ("63000", "0.0125") into the uint128
* price the book expects: quote-per-base scaled by PRICE_SCALE = 1e18.
*
* Verified against `_computeQuote`:
* quote = price * qty * 10^quoteDecimals / (1e18 * 10^baseDecimals)
* For qty = 10^baseDecimals (one whole base unit) this yields exactly
* P * 10^quoteDecimals i.e. the decimals factors CANCEL and
* priceWei = P * 1e18 independent of token decimals. The decimals params
* are kept in the signature for call-site self-documentation + validation.
*/
export function humanPriceToWei(price: string, quoteDecimals: number, baseDecimals: number): bigint {
if (!Number.isInteger(quoteDecimals) || !Number.isInteger(baseDecimals)
|| quoteDecimals < 0 || baseDecimals < 0 || quoteDecimals > 77 || baseDecimals > 77) {
throw new Error('CoreBookClient: bad token decimals');
}
const m = /^(\d+)(?:\.(\d+))?$/.exec(price.trim());
if (!m) throw new Error(`CoreBookClient: bad price string: ${price}`);
const frac = (m[2] ?? '');
if (frac.length > 18) throw new Error('CoreBookClient: price has more than 18 decimal places');
return BigInt(m[1]) * COREBOOK_PRICE_SCALE + BigInt(frac.padEnd(18, '0') || '0');
}
/** Inverse of humanPriceToWei — render a 1e18-scaled price as a decimal string. */
export function weiPriceToHuman(priceWei: bigint): string {
const whole = priceWei / COREBOOK_PRICE_SCALE;
const frac = (priceWei % COREBOOK_PRICE_SCALE).toString().padStart(18, '0').replace(/0+$/, '');
return frac.length > 0 ? `${whole}.${frac}` : whole.toString();
}
/** Snap a price to a multiple of PRICE_TICK (default rounds down). */
export function snapToTick(price: bigint, priceTick: bigint, mode: 'down' | 'up' | 'nearest' = 'down'): bigint {
if (priceTick <= 0n) throw new Error('CoreBookClient: priceTick must be > 0');
const rem = price % priceTick;
if (rem === 0n) return price;
if (mode === 'up') return price + (priceTick - rem);
if (mode === 'nearest') return rem * 2n >= priceTick ? price + (priceTick - rem) : price - rem;
return price - rem;
}
/** Snap a base quantity to a multiple of LOT_SIZE (default rounds down). */
export function snapToLot(quantity: bigint, lotSize: bigint, mode: 'down' | 'up' | 'nearest' = 'down'): bigint {
return snapToTick(quantity, lotSize, mode);
}
/**
* Exact mirror of `_computeQuote` (floor division):
* quote = price * qty * quoteFactor / (PRICE_SCALE * baseFactor)
* Factors are 10**decimals (read them from `marketInfo()`).
*/
export function computeQuote(priceWei: bigint, quantity: bigint, quoteFactor: bigint, baseFactor: bigint): bigint {
return (priceWei * quantity * quoteFactor) / (COREBOOK_PRICE_SCALE * baseFactor);
}
/**
* Exact mirror of `_lockBuyFunds` the QUOTE amount a BUY order must have
* approved + available:
* principal = _computeQuote(price, qty) (must be > 0)
* feeReserve = ceil(principal * takerFeeBps / 1e4) ((p*bps + 9999) / 10000)
* locked = principal + feeReserve + 256
* The flat 256-wei buffer is MAX_OPEN_ORDERS_PER_MAKER covers worst-case
* per-fill fee-ceiling rounding across up to 256 maker fills (AUDIT FIX R6
* CRITICAL #2). Unspent lock is refunded in the same transaction.
*/
export function computeMaxBuyLock(
priceWei: bigint, quantity: bigint, takerFeeBps: number | bigint,
quoteFactor: bigint, baseFactor: bigint,
): bigint {
const principal = computeQuote(priceWei, quantity, quoteFactor, baseFactor);
if (principal === 0n) throw new Error('CoreBookClient: BUY lock principal is zero (qty*price below min quote unit)');
const feeReserve = (principal * BigInt(takerFeeBps) + 9999n) / 10_000n;
return principal + feeReserve + COREBOOK_BUY_LOCK_BUFFER;
}
/* --------------------------------- types --------------------------------- */
export interface CoreBookMarketInfo {
base: string;
quote: string;
sink: string;
priceTick: bigint;
lotSize: bigint;
takerFeeBps: number;
quoteDecimalsFactor: bigint;
baseDecimalsFactor: bigint;
}
/** Decoded `getOrder` view (field order matches the Order struct). */
export interface CoreBookOrder {
maker: string;
price: bigint;
quantity: bigint;
placedAt: bigint;
placedAtBlock: bigint;
side: Side;
exists: boolean;
cancelDelayBlocks: number;
nextAtPrice: bigint;
prevAtPrice: bigint;
}
export interface CoreBookOpenOrder extends CoreBookOrder {
orderId: bigint;
}
export interface CoreBookDepthLevel {
price: bigint;
quantity: bigint; // sum of resting base quantity at this level
orderCount: number;
truncated: boolean; // true if maxOrdersPerLevel cap stopped the walk early
}
export interface CoreBookDepth {
bids: CoreBookDepthLevel[]; // best (highest) bid first
asks: CoreBookDepthLevel[]; // best (lowest) ask first
}
/** Minimal eth_getTransactionReceipt log shape. */
export interface CoreBookLog {
address: string;
topics: string[];
data: string;
transactionHash?: string;
logIndex?: string | number;
}
export interface CoreBookEventBase {
address: string;
transactionHash?: string;
logIndex?: number;
}
export interface CoreBookOrderPlacedEvent extends CoreBookEventBase {
type: 'OrderPlaced';
orderId: bigint; maker: string; side: Side; price: bigint; quantity: bigint;
}
export interface CoreBookOrderCancelledEvent extends CoreBookEventBase {
type: 'OrderCancelled';
orderId: bigint; maker: string; unfilled: bigint;
}
export interface CoreBookFilledEvent extends CoreBookEventBase {
type: 'Filled';
takerOrderId: bigint; makerOrderId: bigint; taker: string; maker: string;
takerSide: Side; price: bigint; quantity: bigint; takerFee: bigint;
}
export interface CoreBookClaimedEvent extends CoreBookEventBase {
type: 'Claimed';
who: string; token: string; amount: bigint;
}
export type CoreBookEvent =
| CoreBookOrderPlacedEvent
| CoreBookOrderCancelledEvent
| CoreBookFilledEvent
| CoreBookClaimedEvent;
/* ----------------------------- event decoding ---------------------------- */
function logMeta(log: CoreBookLog): CoreBookEventBase {
const meta: CoreBookEventBase = { address: log.address.toLowerCase() };
if (log.transactionHash !== undefined) meta.transactionHash = log.transactionHash;
if (log.logIndex !== undefined) meta.logIndex = Number(BigInt(log.logIndex));
return meta;
}
/**
* Decode AereCoreBookV0 logs from a transaction receipt (manual topic match
* unknown topics are skipped). Pass `book` to filter to one market address.
*/
export function decodeCoreBookLogs(logs: CoreBookLog[], book?: string): CoreBookEvent[] {
const out: CoreBookEvent[] = [];
const filter = book?.toLowerCase();
for (const log of logs) {
if (filter && log.address.toLowerCase() !== filter) continue;
const t0 = log.topics[0]?.toLowerCase();
if (t0 === TOPIC_ORDER_PLACED) {
// OrderPlaced(uint128 indexed orderId, address indexed maker, uint8 side, uint128 price, uint128 quantity)
out.push({
...logMeta(log), type: 'OrderPlaced',
orderId: BigInt(log.topics[1]),
maker: topicAddr(log.topics[2]),
side: Number(word(log.data, 0)) as Side,
price: word(log.data, 1),
quantity: word(log.data, 2),
});
} else if (t0 === TOPIC_ORDER_CANCELLED) {
// OrderCancelled(uint128 indexed orderId, address indexed maker, uint128 unfilled)
out.push({
...logMeta(log), type: 'OrderCancelled',
orderId: BigInt(log.topics[1]),
maker: topicAddr(log.topics[2]),
unfilled: word(log.data, 0),
});
} else if (t0 === TOPIC_FILLED) {
// Filled(uint128 indexed takerOrderId, uint128 indexed makerOrderId, address indexed taker,
// address maker, uint8 takerSide, uint128 price, uint128 quantity, uint256 takerFee)
out.push({
...logMeta(log), type: 'Filled',
takerOrderId: BigInt(log.topics[1]),
makerOrderId: BigInt(log.topics[2]),
taker: topicAddr(log.topics[3]),
maker: addrWord(log.data, 0),
takerSide: Number(word(log.data, 1)) as Side,
price: word(log.data, 2),
quantity: word(log.data, 3),
takerFee: word(log.data, 4),
});
} else if (t0 === TOPIC_CLAIMED) {
// Claimed(address indexed who, address indexed token, uint256 amount)
out.push({
...logMeta(log), type: 'Claimed',
who: topicAddr(log.topics[1]),
token: topicAddr(log.topics[2]),
amount: word(log.data, 0),
});
}
}
return out;
}
/* --------------------------------- client -------------------------------- */
export class CoreBookClient {
readonly address: `0x${string}`;
readonly provider: RpcProvider;
constructor(cfg: CoreBookClientConfig) {
this.address = cfg.address;
this.provider = cfg.provider;
}
/* ---- market info (immutables) ----------------------------------------- */
async marketInfo(): Promise<CoreBookMarketInfo> {
const [base, quote, sink, tick, lot, fee, qf, bf] = await Promise.all([
ethCall(this.provider, this.address, SEL.BASE),
ethCall(this.provider, this.address, SEL.QUOTE),
ethCall(this.provider, this.address, SEL.SINK),
ethCall(this.provider, this.address, SEL.PRICE_TICK),
ethCall(this.provider, this.address, SEL.LOT_SIZE),
ethCall(this.provider, this.address, SEL.TAKER_FEE_BPS),
ethCall(this.provider, this.address, SEL.QUOTE_DECIMALS_FACTOR),
ethCall(this.provider, this.address, SEL.BASE_DECIMALS_FACTOR),
]);
return {
base: addrWord(base, 0), quote: addrWord(quote, 0), sink: addrWord(sink, 0),
priceTick: word(tick, 0), lotSize: word(lot, 0),
takerFeeBps: Number(word(fee, 0)),
quoteDecimalsFactor: word(qf, 0), baseDecimalsFactor: word(bf, 0),
};
}
/* ---- book state -------------------------------------------------------- */
/** Highest resting BUY price (0 = bid side empty). */
async bestBid(): Promise<bigint> { return word(await ethCall(this.provider, this.address, SEL.bestBid), 0); }
/** Lowest resting SELL price (COREBOOK_ASK_EMPTY sentinel = ask side empty). */
async bestAsk(): Promise<bigint> { return word(await ethCall(this.provider, this.address, SEL.bestAsk), 0); }
async pendingSinkFee(): Promise<bigint> { return word(await ethCall(this.provider, this.address, SEL.pendingSinkFee), 0); }
async makerNonce(maker: string): Promise<bigint> {
return word(await ethCall(this.provider, this.address, SEL.makerNonce + encAddr(maker)), 0);
}
async openOrderCount(maker: string): Promise<bigint> {
return word(await ethCall(this.provider, this.address, SEL.openOrderCount + encAddr(maker)), 0);
}
/** Failed-transfer credits pulled via claimBase/claimQuote. */
async claimableOf(user: string): Promise<{ base: bigint; quote: bigint }> {
const [b, q] = await Promise.all([
ethCall(this.provider, this.address, SEL.claimableBase + encAddr(user)),
ethCall(this.provider, this.address, SEL.claimableQuote + encAddr(user)),
]);
return { base: word(b, 0), quote: word(q, 0) };
}
async getOrder(orderId: bigint): Promise<CoreBookOrder> {
const d = await ethCall(this.provider, this.address, SEL.getOrder + encUint(orderId));
// Order struct = 10 static fields → 10 inline words.
return {
maker: addrWord(d, 0),
price: word(d, 1),
quantity: word(d, 2),
placedAt: word(d, 3),
placedAtBlock: word(d, 4),
side: Number(word(d, 5)) as Side,
exists: word(d, 6) === 1n,
cancelDelayBlocks: Number(word(d, 7)),
nextAtPrice: word(d, 8),
prevAtPrice: word(d, 9),
};
}
/* ---- lens: depth + open orders (pure view composition) ----------------- */
/**
* Aggregate the top `levels` price levels per side by walking the
* populated-level linked lists + per-level FIFO order lists. View-only;
* cost is one eth_call per pointer hop + per order, so keep `levels`
* UI-sized (e.g. 10-25).
*/
async depth(levels: number, opts: { maxOrdersPerLevel?: number } = {}): Promise<CoreBookDepth> {
const cap = opts.maxOrdersPerLevel ?? 256;
const [bb, ba] = await Promise.all([this.bestBid(), this.bestAsk()]);
const asks: CoreBookDepthLevel[] = [];
const bids: CoreBookDepthLevel[] = [];
let ap = ba;
while (ap !== COREBOOK_ASK_EMPTY && ap !== 0n && asks.length < levels) {
asks.push(await this._levelDepth(false, ap, cap));
ap = await this._readPriceMap(SEL.nextPopulatedAsk, ap); // 0 = end of list
}
let bp = bb;
while (bp !== COREBOOK_BID_EMPTY && bids.length < levels) {
bids.push(await this._levelDepth(true, bp, cap));
bp = await this._readPriceMap(SEL.nextPopulatedBid, bp); // 0 = end of list
}
return { bids, asks };
}
/**
* Enumerate a maker's resting orders WITHOUT an enumeration view on the
* contract: derive orderIds from makerNonce (latest first, see
* `deriveOrderId`) and probe `getOrder` until `openOrderCount` orders are
* found or `maxScan` nonces have been checked (high-churn makers may need
* a larger budget).
*/
async openOrdersOf(maker: string, opts: { maxScan?: number } = {}): Promise<CoreBookOpenOrder[]> {
const [nonce, open] = await Promise.all([this.makerNonce(maker), this.openOrderCount(maker)]);
if (open === 0n || nonce === 0n) return [];
const maxScan = BigInt(opts.maxScan ?? 4096);
const makerLc = maker.toLowerCase();
const found: CoreBookOpenOrder[] = [];
const BATCH = 16;
let scanned = 0n;
let n = nonce;
while (n >= 1n && scanned < maxScan && BigInt(found.length) < open) {
const batch: bigint[] = [];
while (batch.length < BATCH && n >= 1n && scanned < maxScan) { batch.push(n); n -= 1n; scanned += 1n; }
const orders = await Promise.all(batch.map(async (nn) => {
const orderId = deriveOrderId(maker, nn);
return { orderId, order: await this.getOrder(orderId) };
}));
for (const { orderId, order } of orders) {
if (order.exists && order.maker.toLowerCase() === makerLc) {
found.push({ orderId, ...order });
if (BigInt(found.length) >= open) break;
}
}
}
return found;
}
/* ---- writes (caller signs via EIP-1193 from) ---------------------------- */
/**
* GTC limit order via `place` (default 2-block cancel delay) or
* `placeProtected` when `cancelDelayBlocks` is given. REVERTS with
* CrossingRequiresSlippage if the order would cross route crossing
* orders through placeIOC / placeWithSlippage instead.
*/
async placeGTC(from: string, side: Side, price: bigint, quantity: bigint, cancelDelayBlocks?: number): Promise<string> {
if (cancelDelayBlocks === undefined) {
return ethSend(this.provider, from, this.address,
SEL.place + encUint(side) + encUint(price) + encUint(quantity) + encUint(0 /* GTC */));
}
if (cancelDelayBlocks < 0 || cancelDelayBlocks > COREBOOK_MAX_CANCEL_DELAY_BLOCKS) {
throw new Error('CoreBookClient: cancelDelayBlocks out of range (0-1200)');
}
return ethSend(this.provider, from, this.address,
SEL.placeProtected + encUint(side) + encUint(price) + encUint(quantity)
+ encUint(0 /* GTC */) + encUint(cancelDelayBlocks));
}
/**
* IOC sweep via `iocFill` fills whatever crosses up to `worstPrice`,
* discards the rest. Average price is only capped at worstPrice; for a
* tighter band use placeWithSlippage with explicit maxAvgPriceWei.
*/
async placeIOC(from: string, side: Side, worstPrice: bigint, quantity: bigint): Promise<string> {
return ethSend(this.provider, from, this.address,
SEL.iocFill + encUint(side) + encUint(worstPrice) + encUint(quantity));
}
/**
* Recommended entrypoint for crossing orders. Pass maxQuoteSpent =
* COREBOOK_UINT256_MAX-style no-cap via (1n<<256n)-1n if unused; set
* maxAvgPriceWei = 0n to disable the average-price band.
*/
async placeWithSlippage(
from: string, side: Side, price: bigint, quantity: bigint, tif: TimeInForce,
minBaseFilled: bigint, maxQuoteSpent: bigint, maxAvgPriceWei: bigint,
): Promise<string> {
return ethSend(this.provider, from, this.address,
SEL.placeWithSlippage + encUint(side) + encUint(price) + encUint(quantity) + encUint(tif)
+ encUint(minBaseFilled) + encUint(maxQuoteSpent) + encUint(maxAvgPriceWei));
}
/** Full protection: slippage bounds + per-order cancel delay. */
async placeWithSlippageProtected(
from: string, side: Side, price: bigint, quantity: bigint, tif: TimeInForce,
minBaseFilled: bigint, maxQuoteSpent: bigint, cancelDelayBlocks: number, maxAvgPriceWei: bigint,
): Promise<string> {
if (cancelDelayBlocks < 0 || cancelDelayBlocks > COREBOOK_MAX_CANCEL_DELAY_BLOCKS) {
throw new Error('CoreBookClient: cancelDelayBlocks out of range (0-1200)');
}
return ethSend(this.provider, from, this.address,
SEL.placeWithSlippageProtected + encUint(side) + encUint(price) + encUint(quantity) + encUint(tif)
+ encUint(minBaseFilled) + encUint(maxQuoteSpent) + encUint(cancelDelayBlocks) + encUint(maxAvgPriceWei));
}
async cancel(from: string, orderId: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.cancel + encUint(orderId));
}
/** Pull failed-transfer BASE credits (claim(true)). */
async claimBase(from: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.claim + encUint(1));
}
/** Pull failed-transfer QUOTE credits (claim(false)). */
async claimQuote(from: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.claim + encUint(0));
}
/** Permissionless — forwards pendingSinkFee to AereSink. */
async flushFees(from: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.flushFees);
}
/* ---- ERC-20 approve flow ------------------------------------------------ */
/** Current allowance granted by `owner` to THIS book for `token`. */
async allowanceForBook(token: string, owner: string): Promise<bigint> {
const d = await ethCall(this.provider, token, SEL.allowance + encAddr(owner) + encAddr(this.address));
return word(d, 0);
}
/** ERC-20 balance helper (base/quote funding checks before placing). */
async balanceOf(token: string, owner: string): Promise<bigint> {
return word(await ethCall(this.provider, token, SEL.balanceOf + encAddr(owner)), 0);
}
/** approve(book, amount) on `token` from `from`. */
async approveForBook(token: string, from: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, token, SEL.approve + encAddr(this.address) + encUint(amount));
}
/**
* Checks allowance and approves only when insufficient. Returns the
* approve tx hash, or null when the existing allowance already covers
* `amount`. For BUY orders pass `computeMaxBuyLock(...)` as the amount;
* for SELL orders pass the base quantity.
*/
async ensureApprovalForBook(token: string, from: string, amount: bigint): Promise<string | null> {
const current = await this.allowanceForBook(token, from);
if (current >= amount) return null;
return this.approveForBook(token, from, amount);
}
/* ---- internals ---------------------------------------------------------- */
private async _readPriceMap(selector: string, price: bigint): Promise<bigint> {
return word(await ethCall(this.provider, this.address, selector + encUint(price)), 0);
}
private async _levelDepth(isBid: boolean, price: bigint, cap: number): Promise<CoreBookDepthLevel> {
const headSel = isBid ? SEL.bidHeadAtPrice : SEL.askHeadAtPrice;
let id = await this._readPriceMap(headSel, price);
let quantity = 0n;
let orderCount = 0;
while (id !== 0n && orderCount < cap) {
const o = await this.getOrder(id);
if (!o.exists) break; // transient inconsistency guard
quantity += o.quantity;
orderCount += 1;
id = o.nextAtPrice;
}
return { price, quantity, orderCount, truncated: id !== 0n && orderCount >= cap };
}
}

6
src/corebook/index.ts Normal file
View File

@ -0,0 +1,6 @@
// AereCoreBookV0 — native CLOB, one contract per (base, quote) market.
// CoreBookClient is the full read/write client + off-chain lens (depth,
// open-order enumeration, event decoding, price/qty/lock math helpers).
// AereCoreBookClient is the lighter calldata-builder variant.
export * from './CoreBookClient.js';
export * from './AereCoreBookClient.js';

View File

@ -1,6 +1,57 @@
export { AereClient, type AereClientOptions } from './client.js';
export { AERE_MAINNET, type AereContractName } from './addresses.js';
export { AERE_MAINNET, AERE_COREBOOK, type AereContractName, type CoreBookMarketSymbol } from './addresses.js';
export {
ERC20_ABI, WAERE_ABI, STAKING_V2_ABI, STAKING_V1_ABI, LENDING_ABI, STABLE_ABI,
IDENTITY_ABI, BRIDGE_ABI, FAUCET_ABI, SWAP_FACTORY_ABI, SWAP_PAIR_ABI, LIGHTNING_CHANNELS_ABI,
} from './abis.js';
// Week-1 flywheel clients — sAERE receipt vault + AereSink immutable router.
// Ship Q3 2026 (post audit contests).
export * from './sink/index.js';
export * from './saere/index.js';
// AereCoreBookV0 — native CLOB (R6-hardened: claim/sweepDust/protected entrypoints).
export * from './corebook/index.js';
// Wave-1 lending — isolated single-pair money markets + BadDebt insurance.
export { LendingMarketClient, AereInsuranceFundClient, RWATransferAdapterClient } from './lending/index.js';
export type { LendingMarketConfig } from './lending/index.js';
// Month 2-4 — Rollup-as-a-Service registry.
export { AereRaaSFactoryClient, AereRollupSettlementClient } from './raas/index.js';
// AERE402 agentic settlement — registry + facilitator + Express middleware.
export { AereAgentClient, AERE402SettlementClient, buildAuthTypedData,
createAere402Middleware, buildAere402TypedData } from './agentic/index.js';
export type { Aere402MiddlewareConfig, PaymentAuth, AgentPaymentAuth, Eip1193Provider } from './agentic/index.js';
// Compliance primitives — AereProof v0 + ZKScreen + AIProof.
export { AereSanctionsRegistryClient, ChainalysisOracleWrapperClient,
AereTravelRuleHashRegistryClient, AereForensicEventRegistryClient,
AereZKScreenClient, AereAIProofClient } from './compliance/index.js';
// AereStateChannels — bidirectional payment channels + HTLC (Lightning-EVM).
export { AereStateChannelsClient } from './channels/index.js';
export type { ChannelState } from './channels/index.js';
// MiCA Article 78 best-execution receipts — on-chain CASP commitments.
export { BestExClient, OrderType, ClientClass } from './bestex/index.js';
export type { BestExConfig, Receipt as BestExReceipt, CaspRegistration } from './bestex/index.js';
// 2026-06-08 ship batch — agentic accountability + EU AI Act + portable memory.
export {
AereAgentBondClient,
AereAIReputationClient,
AereInferNetClient,
AereAgentMemoryVaultClient,
} from './agentic/index.js';
export type {
BondInfo,
AgentStats,
MemoryVaultPermission,
MemEntry,
} from './agentic/index.js';
export {
AereAttestationGatewayClient,
AereCompliancePoolClient,
} from './compliance/index.js';

View File

@ -0,0 +1,71 @@
/**
* InsuranceFundClient AereInsuranceFund helpers.
* Permissionless donate; Foundation-only coverBadDebt.
*
* Contract: aerenew/contracts/contracts/lending/AereInsuranceFund.sol
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
const pad32 = (h: string) => h.toLowerCase().replace(/^0x/, '').padStart(64, '0');
const encUint = (n: bigint) => pad32(n.toString(16));
const encAddr = (a: string) => pad32(a.replace(/^0x/, ''));
async function ethCall(p: RpcProvider, to: string, data: string): Promise<string> {
return await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] }) as string;
}
async function ethSend(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;
}
const SEL = {
FOUNDATION: '0x4d09f93e', // FOUNDATION()
COOLDOWN_SECONDS: '0xb47d5cf5', // COOLDOWN_SECONDS()
markets: '0x2c43cab8', // markets(address)
balances: '0x27e235e3', // balances(address)
coverageRemaining: '0x1d29df0e', // coverageRemaining(address)
cooldownReady: '0x3a09a73e', // cooldownReady(address)
registerMarket: '0xc6da18d4', // registerMarket(address,uint256)
donate: '0xf14fcbc8', // donate(address,uint256)
coverBadDebt: '0xa90c1e1c', // coverBadDebt(address,address,uint256)
} as const;
export class AereInsuranceFundClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async getFoundation(): Promise<string> {
return '0x' + (await ethCall(this.provider, this.address, SEL.FOUNDATION)).slice(-40);
}
async getCooldownSeconds(): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, SEL.COOLDOWN_SECONDS));
}
async balanceOf(token: string): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, SEL.balances + encAddr(token)));
}
async coverageRemaining(market: string): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, SEL.coverageRemaining + encAddr(market)));
}
async cooldownReady(market: string): Promise<boolean> {
const r = await ethCall(this.provider, this.address, SEL.cooldownReady + encAddr(market));
return BigInt(r) === 1n;
}
/**
* Permissionless donate. Caller must approve `token` for `amount` on the fund first.
*/
async donate(from: string, token: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.donate + encAddr(token) + encUint(amount));
}
/** Foundation-only: register a market with a lifetime coverage cap. */
async registerMarket(from: string, market: string, lifetimeCap: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.registerMarket + encAddr(market) + encUint(lifetimeCap));
}
/** Foundation-only: pay down `amount` of `borrower`'s debt on `market`. */
async coverBadDebt(from: string, market: string, borrower: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.coverBadDebt + encAddr(market) + encAddr(borrower) + encUint(amount));
}
}

View File

@ -0,0 +1,166 @@
/**
* LendingMarketClient typed read/write helpers for AereLendingMarket
* (Wave 1 isolated single-pair money markets).
*
* Contract: aerenew/contracts/contracts/lending/AereLendingMarket.sol
* Audited 2026-06-07 (5 HIGH + 1 MED bugs fixed pre-deploy).
*
* Pattern: dependency-free EIP-1193 wrapper with precomputed 4-byte
* selectors. Read methods just `eth_call`; write methods build calldata
* + `eth_sendTransaction`. Caller handles signer + gas estimation.
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
export interface LendingMarketConfig {
address: `0x${string}`;
provider: RpcProvider;
}
const SEL = {
// immutable views
COLLATERAL: '0xfb4cd02f', // COLLATERAL()
DEBT_TOKEN: '0xea5b5ddf', // DEBT_TOKEN()
SINK: '0xf4f3b200', // SINK()
ORACLE: '0x7dc0d1d0', // ORACLE()
LTV_BPS: '0xcbc4cdc7', // LTV_BPS()
LIQ_THRESHOLD_BPS: '0xc0c5294d', // LIQ_THRESHOLD_BPS()
LIQ_BONUS_BPS: '0x3a86d9b3', // LIQ_BONUS_BPS()
BORROW_FEE_BPS: '0x47bf8b54', // BORROW_FEE_BPS()
MARKET_DEBT_CAP: '0xcca5e09e', // MARKET_DEBT_CAP()
COLLATERAL_DECIMALS: '0x99e9f4ad', // COLLATERAL_DECIMALS()
DEBT_DECIMALS: '0x4d9e7826', // DEBT_DECIMALS()
// state views
collateralOf: '0x8aef7ddf', // collateralOf(address)
debtOf: '0xcc4d2f49', // debtOf(address)
supplierScaled: '0x9d7d6f0d', // supplierScaled(address)
supplierBalance: '0x9d9f3ff6', // supplierBalance(address)
debtBalance: '0x33ed81e4', // debtBalance(address)
borrowIndex: '0xaa5af0fd', // borrowIndex()
supplyIndex: '0xb02f7d5e', // supplyIndex()
totalBorrowsScaled: '0xc0c44d6b', // totalBorrowsScaled()
totalSuppliedScaled: '0x7b0b3da7', // totalSuppliedScaled()
totalBorrowsCurrent: '0xc26c9116', // totalBorrowsCurrent()
totalSuppliedCurrent: '0x57e3c7fc', // totalSuppliedCurrent()
pendingSinkFee: '0x99c50fcb', // pendingSinkFee()
borrowRatePerSecond: '0x9e6dadce', // borrowRatePerSecond()
borrowPaused: '0x42b6e3bf', // borrowPaused()
depositPaused: '0x6a5db0fa', // depositPaused()
healthFactor: '0xe8b9c2dd', // healthFactor(address)
// write
accrueInterest: '0x59601e8a', // accrueInterest()
flushSinkFee: '0x71a5ba73', // flushSinkFee()
supply: '0xf2b9fdb8', // supply(uint256)
redeem: '0xdb006a75', // redeem(uint256)
depositCollateral: '0x80aebec5', // depositCollateral(uint256)
withdrawCollateral: '0xeb0aaa4b', // withdrawCollateral(uint256)
borrow: '0xc5ebeaec', // borrow(uint256)
repay: '0x0e752702', // repay(uint256)
repayOnBehalfOf: '0x8f1a3bf0', // repayOnBehalfOf(address,uint256)
liquidate: '0xf5e3c462', // liquidate(address,uint256)
} as const;
/* ----------------------------- helpers ---------------------------------- */
function pad32(hex: string): string {
const h = hex.toLowerCase().replace(/^0x/, '');
return h.padStart(64, '0');
}
function encUint(n: bigint): string { return pad32(n.toString(16)); }
function encAddr(addr: string): string { return pad32(addr.replace(/^0x/, '')); }
async function ethCall(p: RpcProvider, to: string, data: string): Promise<string> {
return await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] }) as string;
}
async function ethSend(p: RpcProvider, from: string, to: string, data: string, value = '0x0'): Promise<string> {
return await p.request({ method: 'eth_sendTransaction', params: [{ from, to, data, value }] }) as string;
}
/* --------------------------- client ------------------------------------- */
export class LendingMarketClient {
readonly address: `0x${string}`;
readonly provider: RpcProvider;
constructor(cfg: LendingMarketConfig) {
this.address = cfg.address;
this.provider = cfg.provider;
}
/* ---- immutable risk params (cached one-shot reads) ------------------- */
async getCollateral(): Promise<string> { return '0x' + (await ethCall(this.provider, this.address, SEL.COLLATERAL)).slice(-40); }
async getDebtToken(): Promise<string> { return '0x' + (await ethCall(this.provider, this.address, SEL.DEBT_TOKEN)).slice(-40); }
async getOracle(): Promise<string> { return '0x' + (await ethCall(this.provider, this.address, SEL.ORACLE)).slice(-40); }
async getSink(): Promise<string> { return '0x' + (await ethCall(this.provider, this.address, SEL.SINK)).slice(-40); }
async getLtvBps(): Promise<number> { return Number(BigInt(await ethCall(this.provider, this.address, SEL.LTV_BPS))); }
async getLiqThresholdBps(): Promise<number> { return Number(BigInt(await ethCall(this.provider, this.address, SEL.LIQ_THRESHOLD_BPS))); }
async getLiqBonusBps(): Promise<number> { return Number(BigInt(await ethCall(this.provider, this.address, SEL.LIQ_BONUS_BPS))); }
async getBorrowFeeBps(): Promise<number> { return Number(BigInt(await ethCall(this.provider, this.address, SEL.BORROW_FEE_BPS))); }
async getMarketDebtCap(): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.MARKET_DEBT_CAP)); }
/* ---- live state ------------------------------------------------------- */
async totalBorrowsCurrent(): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.totalBorrowsCurrent)); }
async totalSuppliedCurrent(): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.totalSuppliedCurrent)); }
async pendingSinkFee(): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.pendingSinkFee)); }
async borrowRatePerSecond(): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.borrowRatePerSecond)); }
async borrowPaused(): Promise<boolean> { return BigInt(await ethCall(this.provider, this.address, SEL.borrowPaused)) === 1n; }
async depositPaused(): Promise<boolean> { return BigInt(await ethCall(this.provider, this.address, SEL.depositPaused)) === 1n; }
/* ---- per-user views --------------------------------------------------- */
async collateralOf(user: string): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.collateralOf + encAddr(user))); }
async debtBalance(user: string): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.debtBalance + encAddr(user))); }
async supplierBalance(user: string): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.supplierBalance + encAddr(user))); }
async healthFactor(user: string): Promise<bigint> { return BigInt(await ethCall(this.provider, this.address, SEL.healthFactor + encAddr(user))); }
/* ---- write methods ---------------------------------------------------- */
async accrueInterest(from: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.accrueInterest);
}
async flushSinkFee(from: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.flushSinkFee);
}
async supply(from: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.supply + encUint(amount));
}
async redeem(from: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.redeem + encUint(amount));
}
async depositCollateral(from: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.depositCollateral + encUint(amount));
}
async withdrawCollateral(from: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.withdrawCollateral + encUint(amount));
}
async borrow(from: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.borrow + encUint(amount));
}
async repay(from: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.repay + encUint(amount));
}
async repayOnBehalfOf(from: string, account: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.repayOnBehalfOf + encAddr(account) + encUint(amount));
}
async liquidate(from: string, borrower: string, repayAmount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.liquidate + encAddr(borrower) + encUint(repayAmount));
}
/* ---- helpers ---------------------------------------------------------- */
/**
* Returns the APY as a decimal (0.05 = 5%) computed from `borrowRatePerSecond`.
* Linear approximation; accurate at low rates.
*/
async borrowApy(): Promise<number> {
const rate = await this.borrowRatePerSecond();
const r = Number(rate) / 1e27;
return r * 365 * 86400;
}
}

View File

@ -0,0 +1,52 @@
/**
* RWATransferAdapterClient permissioned RWA USDC.e redemption.
* Contract: aerenew/contracts/contracts/lending/RWATransferAdapter.sol
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
const pad32 = (h: string) => h.toLowerCase().replace(/^0x/, '').padStart(64, '0');
const encUint = (n: bigint) => pad32(n.toString(16));
const encAddr = (a: string) => pad32(a.replace(/^0x/, ''));
async function ethCall(p: RpcProvider, to: string, data: string): Promise<string> {
return await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] }) as string;
}
async function ethSend(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;
}
const SEL = {
PAYOUT_TOKEN: '0x935ca8e6', // PAYOUT_TOKEN()
ORACLE: '0x7dc0d1d0', // ORACLE()
FOUNDATION: '0x4d09f93e', // FOUNDATION()
configs: '0x1d12be96', // configs(address)
states: '0xb95da3ec', // states(address)
periodRemaining: '0x3aff0d34', // periodRemaining(address)
payoutLiquidity: '0x8b8a55be', // payoutLiquidity()
redeem: '0x1e9a6950', // redeem(address,uint256)
} as const;
export class RWATransferAdapterClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async getPayoutToken(): Promise<string> {
return '0x' + (await ethCall(this.provider, this.address, SEL.PAYOUT_TOKEN)).slice(-40);
}
async getFoundation(): Promise<string> {
return '0x' + (await ethCall(this.provider, this.address, SEL.FOUNDATION)).slice(-40);
}
async periodRemaining(asset: string): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, SEL.periodRemaining + encAddr(asset)));
}
async payoutLiquidity(): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, SEL.payoutLiquidity));
}
/** Permissionless redeem. Caller must approve `asset` for `rwaAmount` on this contract first. */
async redeem(from: string, asset: string, rwaAmount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.redeem + encAddr(asset) + encUint(rwaAmount));
}
}

4
src/lending/index.ts Normal file
View File

@ -0,0 +1,4 @@
export { LendingMarketClient } from './LendingMarketClient.js';
export type { LendingMarketConfig } from './LendingMarketClient.js';
export { AereInsuranceFundClient } from './InsuranceFundClient.js';
export { RWATransferAdapterClient } from './RWATransferAdapterClient.js';

179
src/raas/RaaSClient.ts Normal file
View File

@ -0,0 +1,179 @@
/**
* RaaSClient AereRaaSFactory + AereRollupSettlement helpers.
*
* Contracts:
* aerenew/contracts/contracts/raas/AereRaaSFactory.sol
* aerenew/contracts/contracts/raas/AereRollupSettlement.sol
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
const pad32 = (h: string) => h.toLowerCase().replace(/^0x/, '').padStart(64, '0');
const encUint = (n: bigint) => pad32(n.toString(16));
const encAddr = (a: string) => pad32(a.replace(/^0x/, ''));
const encBytes32 = (b: string) => pad32(b.replace(/^0x/, ''));
const encBool = (b: boolean) => pad32(b ? '1' : '0');
async function ethCall(p: RpcProvider, to: string, data: string): Promise<string> {
return await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] }) as string;
}
async function ethSend(p: RpcProvider, from: string, to: string, data: string, value = '0x0'): Promise<string> {
return await p.request({ method: 'eth_sendTransaction', params: [{ from, to, data, value }] }) as string;
}
/* ============================== Factory =============================== */
const FAC_SEL = {
// immutable views
BOND_TOKEN: '0x18b13f55', // BOND_TOKEN()
SINK: '0xf4f3b200', // SINK()
REGISTRATION_BOND: '0x4b094c4b', // REGISTRATION_BOND()
MIN_SINK_BPS: '0x9dabd040', // MIN_SINK_BPS()
DEFAULT_CHALLENGE_WINDOW: '0x8e8a6f3a', // DEFAULT_CHALLENGE_WINDOW()
FOUNDATION: '0x4d09f93e', // FOUNDATION()
// state views
rollups: '0xdcecb8e3', // rollups(bytes32)
rollupCount: '0xff19a3a4', // rollupCount()
// writes
registerRollup: '0xbb4ab86c', // registerRollup(bytes32,address,address,address,uint16,uint16,uint16,uint256,uint64,string)
updateMetadata: '0xa1ae9b0e', // updateMetadata(bytes32,string)
exitRollup: '0x73db2bba', // exitRollup(bytes32,address,uint64,bytes)
} as const;
export class AereRaaSFactoryClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async rollupCount(): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, FAC_SEL.rollupCount));
}
async getSink(): Promise<string> {
return '0x' + (await ethCall(this.provider, this.address, FAC_SEL.SINK)).slice(-40);
}
async getRegistrationBond(): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, FAC_SEL.REGISTRATION_BOND));
}
async getMinSinkBps(): Promise<number> {
return Number(BigInt(await ethCall(this.provider, this.address, FAC_SEL.MIN_SINK_BPS)));
}
/**
* Register a rollup. Requires the caller to have approved BOND_TOKEN for
* REGISTRATION_BOND on the factory beforehand.
*/
async registerRollup(
from: string,
opts: {
rollupId: string; // bytes32 hex
rollupOwner: string;
sequencer: string;
debtToken: string;
sinkBps: number;
rollupBps: number;
sequencerBps: number;
minChallengeBond: bigint;
challengeWindow: number;
metadataUri: string;
}
): Promise<string> {
const meta = Buffer.from(opts.metadataUri, 'utf8');
const head =
encBytes32(opts.rollupId) +
encAddr(opts.rollupOwner) +
encAddr(opts.sequencer) +
encAddr(opts.debtToken) +
encUint(BigInt(opts.sinkBps)) +
encUint(BigInt(opts.rollupBps)) +
encUint(BigInt(opts.sequencerBps)) +
encUint(opts.minChallengeBond) +
encUint(BigInt(opts.challengeWindow)) +
encUint(0x140n); // 10 head slots * 32 = 320 = 0x140
const padded = Buffer.alloc(Math.ceil(meta.length / 32) * 32);
meta.copy(padded);
const dyn = encUint(BigInt(meta.length)) + padded.toString('hex');
const data = FAC_SEL.registerRollup + head + dyn;
return ethSend(this.provider, from, this.address, data);
}
}
/* ============================ Settlement ============================= */
const SET_SEL = {
ROLLUP_ID: '0x7b0a4eb1', // ROLLUP_ID()
SINK: '0xf4f3b200', // SINK()
DEBT_TOKEN: '0xea5b5ddf', // DEBT_TOKEN()
CHALLENGE_WINDOW: '0x7a6a4abe', // CHALLENGE_WINDOW()
SINK_BPS: '0xb3f54bdd', // SINK_BPS()
ROLLUP_BPS: '0xc70aa727', // ROLLUP_BPS()
SEQUENCER_BPS: '0xe35d8b85', // SEQUENCER_BPS()
rollupOwner: '0x0e8d63c0', // rollupOwner()
sequencer: '0x42d1de27', // sequencer()
latestEpoch: '0x6f6db00f', // latestEpoch()
latestFinalisedEpoch:'0x3aac4670', // latestFinalisedEpoch()
isFinalised: '0x3a8a4a3e', // isFinalised(uint256)
totalRevenueSettled: '0xf3b6db95', // totalRevenueSettled()
proposeStateRoot: '0x9e706b08', // proposeStateRoot(uint256,bytes32,bytes32)
advanceFinalisation: '0xb78f2ebd', // advanceFinalisation()
challenge: '0x8da548fa', // challenge(uint256,uint256,bytes32)
resolveChallenge: '0x437f2ba0', // resolveChallenge(uint256,bool)
resolveChallengeExpired: '0xb1cc5a3d', // resolveChallengeExpired(uint256)
settleRevenue: '0xc7c1aa8c', // settleRevenue(uint256)
setSequencer: '0xa6f9dae1', // setSequencer(address)
transferOwner: '0xc6dad082', // transferOwner(address)
} as const;
export class AereRollupSettlementClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async sequencer(): Promise<string> {
return '0x' + (await ethCall(this.provider, this.address, SET_SEL.sequencer)).slice(-40);
}
async rollupOwner(): Promise<string> {
return '0x' + (await ethCall(this.provider, this.address, SET_SEL.rollupOwner)).slice(-40);
}
async latestEpoch(): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, SET_SEL.latestEpoch));
}
async isFinalised(epoch: bigint): Promise<boolean> {
const res = await ethCall(this.provider, this.address, SET_SEL.isFinalised + encUint(epoch));
return BigInt(res) === 1n;
}
async totalRevenueSettled(): Promise<bigint> {
return BigInt(await ethCall(this.provider, this.address, SET_SEL.totalRevenueSettled));
}
/* sequencer-only */
async proposeStateRoot(from: string, epoch: bigint, root: string, l2BlockHash: string): Promise<string> {
const data = SET_SEL.proposeStateRoot + encUint(epoch) + encBytes32(root) + encBytes32(l2BlockHash);
return ethSend(this.provider, from, this.address, data);
}
async resolveChallenge(from: string, epoch: bigint, accepted: boolean): Promise<string> {
return ethSend(this.provider, from, this.address, SET_SEL.resolveChallenge + encUint(epoch) + encBool(accepted));
}
/* permissionless */
async challenge(from: string, epoch: bigint, bond: bigint, reasonHash: string): Promise<string> {
const data = SET_SEL.challenge + encUint(epoch) + encUint(bond) + encBytes32(reasonHash);
return ethSend(this.provider, from, this.address, data);
}
async resolveChallengeExpired(from: string, epoch: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SET_SEL.resolveChallengeExpired + encUint(epoch));
}
async advanceFinalisation(from: string): Promise<string> {
return ethSend(this.provider, from, this.address, SET_SEL.advanceFinalisation);
}
/* rollupOwner-only */
async settleRevenue(from: string, amount: bigint): Promise<string> {
return ethSend(this.provider, from, this.address, SET_SEL.settleRevenue + encUint(amount));
}
async setSequencer(from: string, newSequencer: string): Promise<string> {
return ethSend(this.provider, from, this.address, SET_SEL.setSequencer + encAddr(newSequencer));
}
async transferOwner(from: string, newOwner: string): Promise<string> {
return ethSend(this.provider, from, this.address, SET_SEL.transferOwner + encAddr(newOwner));
}
}

1
src/raas/index.ts Normal file
View File

@ -0,0 +1 @@
export { AereRaaSFactoryClient, AereRollupSettlementClient } from './RaaSClient.js';

1
src/saere/index.ts Normal file
View File

@ -0,0 +1 @@
export { SAereClient, type SAereClientConfig } from './sAEREClient.js';

212
src/saere/sAEREClient.ts Normal file
View File

@ -0,0 +1,212 @@
/**
* sAEREClient typed helpers for the sAERE ERC-4626 receipt vault.
*
* Contract: aerenew/contracts/contracts/staking/sAERE.sol
*
* Phase 1 (read): live conversion rate (pricePerShare), TVL (totalAssets),
* outstanding shares (totalSupply), per-user holdings.
*
* Phase 2 (write): deposit / mint / withdraw / redeem helper calldata
* generators. Assumes integrator already obtained a signer
* and approved WAERE for the sAERE vault.
*
* Same dependency-light EIP-1193 surface as AereSinkClient.
*/
import type { RpcProvider } from '../sink/AereSinkClient.js';
export interface SAereClientConfig {
/** sAERE contract address. */
address: `0x${string}`;
/** EIP-1193-style provider. */
provider: RpcProvider;
}
/* --------------------------- selector constants --------------------------- */
const SEL = {
asset: '0x38d52e0f', // asset()
totalAssets: '0x01e1d114', // totalAssets()
totalSupply: '0x18160ddd', // totalSupply()
pricePerShare: '0x99530b06', // pricePerShare()
decimals: '0x313ce567', // decimals()
// R5/R6 drip + atomic state additions
atomicState: '0x152f7185', // atomicState()
sync: '0xfff6cae9', // sync()
undistributedRewards: '0x319ce2bb', // undistributedRewards()
rewardRate: '0x7b0a47ee', // rewardRate()
periodFinish: '0xebe2b12b', // periodFinish()
lastObservedBalance: '0xc5ab5bf0', // lastObservedBalance()
DRIP_DURATION: '0x2a129169', // DRIP_DURATION()
} as const;
const BALANCE_OF_SELECTOR = '0x70a08231'; // balanceOf(address)
const CONVERT_TO_ASSETS_SEL = '0x07a2d13a'; // convertToAssets(uint256)
const CONVERT_TO_SHARES_SEL = '0xc6e6f592'; // convertToShares(uint256)
const DEPOSIT_SELECTOR = '0x6e553f65'; // deposit(uint256,address)
const REDEEM_SELECTOR = '0xba087652'; // redeem(uint256,address,address)
const WITHDRAW_SELECTOR = '0xb460af94'; // withdraw(uint256,address,address)
/* ----------------------------- helper functions ---------------------------- */
function hexToBigInt(hex: string): bigint {
if (!/^0x[0-9a-fA-F]+$/.test(hex)) {
throw new Error(`sAEREClient: malformed integer response: ${hex}`);
}
return BigInt(hex);
}
function hexToAddress(hex: string): `0x${string}` {
if (!/^0x[0-9a-fA-F]{64}$/.test(hex)) {
throw new Error(`sAEREClient: malformed address response: ${hex}`);
}
return ('0x' + hex.slice(-40)) as `0x${string}`;
}
function padAddress(address: `0x${string}`): string {
return address.slice(2).toLowerCase().padStart(64, '0');
}
function padUint(value: bigint): string {
return value.toString(16).padStart(64, '0');
}
/* -------------------------------- client -------------------------------- */
export class SAereClient {
readonly address: `0x${string}`;
private readonly provider: RpcProvider;
constructor(cfg: SAereClientConfig) {
this.address = cfg.address;
this.provider = cfg.provider;
}
/* ---------------------------- read methods ---------------------------- */
/** Snapshot of vault state — TVL, supply, conversion rate. */
async readVaultState(): Promise<{
underlying: `0x${string}`;
totalAssetsWei: bigint;
totalSupplyWei: bigint;
pricePerShareWei: bigint;
decimals: number;
}> {
const [underlying, totalAssets, totalSupply, pricePerShare, decimals] = await Promise.all([
this._call(SEL.asset),
this._call(SEL.totalAssets),
this._call(SEL.totalSupply),
this._call(SEL.pricePerShare),
this._call(SEL.decimals),
]);
return {
underlying: hexToAddress(underlying),
totalAssetsWei: hexToBigInt(totalAssets),
totalSupplyWei: hexToBigInt(totalSupply),
pricePerShareWei: hexToBigInt(pricePerShare),
decimals: Number(hexToBigInt(decimals)),
};
}
/** Get a user's sAERE share balance, in wei (1e18 = 1 share). */
async balanceOf(user: `0x${string}`): Promise<bigint> {
const data = BALANCE_OF_SELECTOR + padAddress(user);
const result = await this._call(data);
return hexToBigInt(result);
}
/** Convert sAERE shares → underlying WAERE assets at current rate. */
async convertToAssets(shares: bigint): Promise<bigint> {
const data = CONVERT_TO_ASSETS_SEL + padUint(shares);
return hexToBigInt(await this._call(data));
}
/** Convert WAERE assets → sAERE shares at current rate. */
async convertToShares(assets: bigint): Promise<bigint> {
const data = CONVERT_TO_SHARES_SEL + padUint(assets);
return hexToBigInt(await this._call(data));
}
/**
* R6 atomicState bal + ta + undist + supply + rate + periodFinish +
* lastObserved in a single eth_call. Use this for fuzz-resistant
* snapshotting or to avoid cross-block drift in dashboards.
*/
async readAtomicState(): Promise<{
balanceWei: bigint;
totalAssetsWei: bigint;
undistributedWei: bigint;
totalSupplyWei: bigint;
rewardRatePerSec: bigint;
periodFinish: bigint;
lastObservedBalanceWei: bigint;
}> {
const raw = await this._call(SEL.atomicState);
// 7 uint256 packed. Each 32 bytes = 64 hex chars after 0x.
const body = raw.slice(2);
if (body.length < 64 * 7) throw new Error('sAEREClient: short atomicState');
const slot = (i: number) => BigInt('0x' + body.slice(i * 64, (i + 1) * 64));
return {
balanceWei: slot(0),
totalAssetsWei: slot(1),
undistributedWei: slot(2),
totalSupplyWei: slot(3),
rewardRatePerSec: slot(4),
periodFinish: slot(5),
lastObservedBalanceWei: slot(6),
};
}
/** R5 undistributed (unvested) reward reserve, wei. */
async readUndistributed(): Promise<bigint> {
return hexToBigInt(await this._call(SEL.undistributedRewards));
}
/** R5 current per-second drip rate, wei. */
async readRewardRate(): Promise<bigint> {
return hexToBigInt(await this._call(SEL.rewardRate));
}
/** Unix timestamp when the current drip schedule ends. */
async readPeriodFinish(): Promise<bigint> {
return hexToBigInt(await this._call(SEL.periodFinish));
}
/* --------------------------- write: calldata --------------------------- */
/**
* R5 sync() permissionless. Recognises any pending AERE arrivals to the
* vault as a fresh drip schedule. Idempotent within a block. Callable by
* AereSink (auto-pinged on flush) but anyone can poke if needed.
*/
encodeSync(): `0x${string}` { return SEL.sync as `0x${string}`; }
/** Encode `deposit(assets, receiver)`. */
encodeDeposit(assets: bigint, receiver: `0x${string}`): `0x${string}` {
return (DEPOSIT_SELECTOR + padUint(assets) + padAddress(receiver)) as `0x${string}`;
}
/** Encode `withdraw(assets, receiver, owner)`. */
encodeWithdraw(assets: bigint, receiver: `0x${string}`, owner: `0x${string}`): `0x${string}` {
return (WITHDRAW_SELECTOR + padUint(assets) + padAddress(receiver) + padAddress(owner)) as `0x${string}`;
}
/** Encode `redeem(shares, receiver, owner)`. */
encodeRedeem(shares: bigint, receiver: `0x${string}`, owner: `0x${string}`): `0x${string}` {
return (REDEEM_SELECTOR + padUint(shares) + padAddress(receiver) + padAddress(owner)) as `0x${string}`;
}
/* ----------------------------- internals ------------------------------ */
private async _call(data: string): Promise<string> {
const result = await this.provider.request({
method: 'eth_call',
params: [{ to: this.address, data }, 'latest'],
});
if (typeof result !== 'string') {
throw new Error(`sAEREClient: unexpected eth_call result`);
}
return result;
}
}

168
src/sink/AereSinkClient.ts Normal file
View File

@ -0,0 +1,168 @@
/**
* AereSinkClient typed read/write helpers for the AereSink immutable
* 3-bucket protocol revenue router.
*
* Contract: aerenew/contracts/contracts/sink/AereSink.sol
*
* Phase 1 (read-only): poll the immutable bucket configuration + lifetime
* stats. Integrators dashboard their share of the
* flywheel without needing a signer.
*
* Phase 2 (write): integrators that already accumulate fees in some asset
* call flush() on this contract. They must `approve`
* the sink first; flushOne() helps with that.
*
* Dependencies: any EIP-1193 JSON-RPC provider (viem, ethers, ethereum
* window, raw fetch). The SDK accepts an `eth_call` shim instead of a
* concrete provider library, keeping the dep surface small.
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
export interface AereSinkConfig {
/** AereSink contract address. */
address: `0x${string}`;
/** EIP-1193-style provider (viem, ethers, or window.ethereum). */
provider: RpcProvider;
}
/* --------------------------- selector constants --------------------------- */
// 4-byte function selectors. Computed once at module load and held constant.
// (Verified via cast sig-of <name>().)
const SEL = {
AERE: '0x1cc59c1b', // AERE()
BURN_VAULT: '0x4e7eee76', // BURN_VAULT()
SAERE_VAULT: '0xc46aedd7', // SAERE_VAULT()
DEX_ROUTER: '0x3b1664c1', // DEX_ROUTER()
BURN_BPS: '0x9748d6c0', // BURN_BPS()
BUYBACK_BPS: '0xb56fcecf', // BUYBACK_BPS()
STAKER_YIELD_BPS: '0xc77b3402', // STAKER_YIELD_BPS()
MAX_SLIPPAGE_BPS: '0xfb6fb1f9', // MAX_SLIPPAGE_BPS()
} as const;
// flush(address,uint256) — for write
const FLUSH_SELECTOR = '0x6cd5c39b';
// sweepDust(address) — for write
const SWEEP_DUST_SELECTOR = '0xeb1a0a96';
/* ----------------------------- helper functions ---------------------------- */
function hexToAddress(hex: string): `0x${string}` {
// 32-byte ABI-encoded address → 20-byte address
// hex is 0x + 64 hex chars; the address is the last 40 chars.
if (!/^0x[0-9a-fA-F]{64}$/.test(hex)) {
throw new Error(`AereSinkClient: malformed address response: ${hex}`);
}
return ('0x' + hex.slice(-40)) as `0x${string}`;
}
function hexToBigInt(hex: string): bigint {
if (!/^0x[0-9a-fA-F]+$/.test(hex)) {
throw new Error(`AereSinkClient: malformed integer response: ${hex}`);
}
return BigInt(hex);
}
function padAddress(address: `0x${string}`): string {
// 20-byte address → 32-byte ABI-encoded slot.
return '0x' + address.slice(2).toLowerCase().padStart(64, '0');
}
function padUint(value: bigint): string {
return value.toString(16).padStart(64, '0');
}
/* ----------------------------- the client ---------------------------- */
export class AereSinkClient {
readonly address: `0x${string}`;
private readonly provider: RpcProvider;
constructor(cfg: AereSinkConfig) {
this.address = cfg.address;
this.provider = cfg.provider;
}
/* ---------------------- read: immutable parameters --------------------- */
/** Read all 8 immutable parameters in a single batched eth_call sequence. */
async readConfig(): Promise<{
aere: `0x${string}`;
burnVault: `0x${string}`;
sAereVault: `0x${string}`;
dexRouter: `0x${string}`;
burnBps: number;
buybackBps: number;
stakerYieldBps: number;
maxSlippageBps: number;
}> {
const [aere, burnVault, sAereVault, dexRouter, burnBps, buybackBps, stakerYieldBps, maxSlippageBps] =
await Promise.all([
this._call(SEL.AERE),
this._call(SEL.BURN_VAULT),
this._call(SEL.SAERE_VAULT),
this._call(SEL.DEX_ROUTER),
this._call(SEL.BURN_BPS),
this._call(SEL.BUYBACK_BPS),
this._call(SEL.STAKER_YIELD_BPS),
this._call(SEL.MAX_SLIPPAGE_BPS),
]);
return {
aere: hexToAddress(aere),
burnVault: hexToAddress(burnVault),
sAereVault: hexToAddress(sAereVault),
dexRouter: hexToAddress(dexRouter),
burnBps: Number(hexToBigInt(burnBps)),
buybackBps: Number(hexToBigInt(buybackBps)),
stakerYieldBps: Number(hexToBigInt(stakerYieldBps)),
maxSlippageBps: Number(hexToBigInt(maxSlippageBps)),
};
}
/** Convenience: returns the human-readable bucket percentages (e.g., 15.00). */
async readSplitPercentages(): Promise<{ burn: number; buyback: number; stakerYield: number }> {
const c = await this.readConfig();
return {
burn: c.burnBps / 100,
buyback: c.buybackBps / 100,
stakerYield: c.stakerYieldBps / 100,
};
}
/* ----------------------- write: encoded calldata ----------------------- */
/**
* Encode the calldata for `flush(token, amount)`. The integrator submits this
* to the sink contract after first calling `approve(sinkAddress, amount)`
* on the token.
*/
encodeFlush(token: `0x${string}`, amount: bigint): `0x${string}` {
const data =
FLUSH_SELECTOR +
padAddress(token).slice(2) +
padUint(amount);
return data as `0x${string}`;
}
/** Encode `sweepDust(token)`. Permissionless — anyone can sweep. */
encodeSweepDust(token: `0x${string}`): `0x${string}` {
return (SWEEP_DUST_SELECTOR + padAddress(token).slice(2)) as `0x${string}`;
}
/* ----------------------------- internals ------------------------------- */
private async _call(data: string): Promise<string> {
const result = await this.provider.request({
method: 'eth_call',
params: [{ to: this.address, data }, 'latest'],
});
if (typeof result !== 'string') {
throw new Error(`AereSinkClient: unexpected eth_call result`);
}
return result;
}
}

1
src/sink/index.ts Normal file
View File

@ -0,0 +1 @@
export { AereSinkClient, type AereSinkConfig, type RpcProvider } from './AereSinkClient.js';