diff --git a/src/index.ts b/src/index.ts index 6fddf11..273a004 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,8 @@ 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'; diff --git a/src/saere/index.ts b/src/saere/index.ts new file mode 100644 index 0000000..195921b --- /dev/null +++ b/src/saere/index.ts @@ -0,0 +1 @@ +export { SAereClient, type SAereClientConfig } from './sAEREClient.js'; diff --git a/src/saere/sAEREClient.ts b/src/saere/sAEREClient.ts new file mode 100644 index 0000000..5ce88a2 --- /dev/null +++ b/src/saere/sAEREClient.ts @@ -0,0 +1,151 @@ +/** + * 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() +} 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 { + 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 { + 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 { + const data = CONVERT_TO_SHARES_SEL + padUint(assets); + return hexToBigInt(await this._call(data)); + } + + /* --------------------------- write: calldata --------------------------- */ + + /** 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 { + 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; + } +} diff --git a/src/sink/AereSinkClient.ts b/src/sink/AereSinkClient.ts new file mode 100644 index 0000000..a08ee19 --- /dev/null +++ b/src/sink/AereSinkClient.ts @@ -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; +} + +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; + } +} diff --git a/src/sink/index.ts b/src/sink/index.ts new file mode 100644 index 0000000..32d9793 --- /dev/null +++ b/src/sink/index.ts @@ -0,0 +1 @@ +export { AereSinkClient, type AereSinkConfig, type RpcProvider } from './AereSinkClient.js';