feat(bestex): MiCA Article 78 best-execution receipt SDK client
BestExClient (dependency-free EIP-1193 wrapper, precomputed selectors): - caspOf / receiptNonce / issuedCount / nextReceiptId / caspIsActive - executionLatencyMs (decision → execution latency view) - encodeRegisterCasp / encodeSetCaspPaused (Foundation calldata) - encodeIssueReceipt (full ABI tuple encoder with string tail) Exports OrderType + ClientClass enums (Market/Limit/StopLoss/StopLimit/ TWAP/VWAP/RFQ/Other; Retail/Professional/Eligible) and the Receipt + CaspRegistration types from the package barrel. Matches the contract shipped 2026-06-08 (172/172 tests). Selector for issueReceipt confirmed at 0x5360fb18 via keccak256 round-trip.
This commit is contained in:
parent
136b95c3a4
commit
1917a4cd20
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';
|
||||
25
src/index.ts
25
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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user