Compare commits
7 Commits
16fbe2932f
...
b45682d578
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b45682d578 | ||
|
|
094688d1d1 | ||
|
|
fbc6d4f68a | ||
|
|
99abfff98a | ||
|
|
1917a4cd20 | ||
|
|
136b95c3a4 | ||
|
|
88626e43ae |
85
examples/agent-402-client.js
Normal file
85
examples/agent-402-client.js
Normal 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());
|
||||
})();
|
||||
84
examples/express-402-paid-endpoint.js
Normal file
84
examples/express-402-paid-endpoint.js
Normal 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}`);
|
||||
});
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
203
src/agentic/AERE402Middleware.ts
Normal file
203
src/agentic/AERE402Middleware.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
205
src/agentic/AereAgentClient.ts
Normal file
205
src/agentic/AereAgentClient.ts
Normal 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 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
227
src/agentic/NewAgenticClients.ts
Normal file
227
src/agentic/NewAgenticClients.ts
Normal 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
18
src/agentic/index.ts
Normal 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
227
src/bestex/BestExClient.ts
Normal 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
2
src/bestex/index.ts
Normal 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
113
src/channels/index.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
260
src/compliance/NewComplianceClients.ts
Normal file
260
src/compliance/NewComplianceClients.ts
Normal 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
185
src/compliance/index.ts
Normal 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';
|
||||
220
src/corebook/AereCoreBookClient.ts
Normal file
220
src/corebook/AereCoreBookClient.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
724
src/corebook/CoreBookClient.ts
Normal file
724
src/corebook/CoreBookClient.ts
Normal 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
6
src/corebook/index.ts
Normal 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';
|
||||
53
src/index.ts
53
src/index.ts
@ -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';
|
||||
|
||||
71
src/lending/InsuranceFundClient.ts
Normal file
71
src/lending/InsuranceFundClient.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
166
src/lending/LendingMarketClient.ts
Normal file
166
src/lending/LendingMarketClient.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
52
src/lending/RWATransferAdapterClient.ts
Normal file
52
src/lending/RWATransferAdapterClient.ts
Normal 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
4
src/lending/index.ts
Normal 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
179
src/raas/RaaSClient.ts
Normal 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
1
src/raas/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AereRaaSFactoryClient, AereRollupSettlementClient } from './RaaSClient.js';
|
||||
1
src/saere/index.ts
Normal file
1
src/saere/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { SAereClient, type SAereClientConfig } from './sAEREClient.js';
|
||||
212
src/saere/sAEREClient.ts
Normal file
212
src/saere/sAEREClient.ts
Normal 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
168
src/sink/AereSinkClient.ts
Normal 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
1
src/sink/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AereSinkClient, type AereSinkConfig, type RpcProvider } from './AereSinkClient.js';
|
||||
Loading…
Reference in New Issue
Block a user