/** * 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; } 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 ().) 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 { 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; } }