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).
|
// Ship Q3 2026 (post audit contests).
|
||||||
export * from './sink/index.js';
|
export * from './sink/index.js';
|
||||||
export * from './saere/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