From 30da2774d4b72e3723c2243c5070e544c786645e Mon Sep 17 00:00:00 2001 From: Liviu Date: Mon, 8 Jun 2026 01:45:57 +0300 Subject: [PATCH] feat(bestex): MiCA Article 78 best-execution receipt SDK client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BestExClient (dependency-free EIP-1193 wrapper, precomputed selectors): - caspOf / receiptNonce / issuedCount / nextReceiptId / caspIsActive - executionLatencyMs (decision → execution latency view) - encodeRegisterCasp / encodeSetCaspPaused (Foundation calldata) - encodeIssueReceipt (full ABI tuple encoder with string tail) Exports OrderType + ClientClass enums (Market/Limit/StopLoss/StopLimit/ TWAP/VWAP/RFQ/Other; Retail/Professional/Eligible) and the Receipt + CaspRegistration types from the package barrel. Matches the contract shipped 2026-06-08 (172/172 tests). Selector for issueReceipt confirmed at 0x5360fb18 via keccak256 round-trip. --- src/bestex/BestExClient.ts | 227 +++++++++++++++++++++++++++++++++++++ src/bestex/index.ts | 2 + src/index.ts | 25 ++++ 3 files changed, 254 insertions(+) create mode 100644 src/bestex/BestExClient.ts create mode 100644 src/bestex/index.ts diff --git a/src/bestex/BestExClient.ts b/src/bestex/BestExClient.ts new file mode 100644 index 0000000..6592fdf --- /dev/null +++ b/src/bestex/BestExClient.ts @@ -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; +} + +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 { + 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 { + 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 { + const r = await this.call(SEL.receiptNonce + addrPad(casp)); + return BigInt(r); + } + + async issuedCount(casp: `0x${string}`): Promise { + 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 { + const r = await this.call(SEL.caspIsActive + addrPad(casp)); + return parseInt(r.slice(-2), 16) !== 0; + } + + async executionLatencyMs(receiptId: `0x${string}`): Promise { + 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; diff --git a/src/bestex/index.ts b/src/bestex/index.ts new file mode 100644 index 0000000..605da70 --- /dev/null +++ b/src/bestex/index.ts @@ -0,0 +1,2 @@ +export { BestExClient, OrderType, ClientClass } from './BestExClient.js'; +export type { BestExConfig, Receipt, CaspRegistration, RpcProvider } from './BestExClient.js'; diff --git a/src/index.ts b/src/index.ts index 273a004..cab8231 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,3 +9,28 @@ export { // Ship Q3 2026 (post audit contests). export * from './sink/index.js'; export * from './saere/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';