Two new typed clients for the Week-1 flywheel contracts, dependency-light
(no viem/ethers import required — accepts any EIP-1193-style provider).
src/sink/AereSinkClient.ts
- readConfig(): all 8 immutable parameters in one batched eth_call sequence
(AERE token, BURN_VAULT, SAERE_VAULT, DEX_ROUTER, 4 bps values, slippage cap).
- readSplitPercentages(): convenience that returns burn/buyback/staker
as human-readable percentages.
- encodeFlush(token, amount): calldata for the integrator-side flush call.
- encodeSweepDust(token): calldata for the permissionless dust-sweep.
src/saere/sAEREClient.ts
- readVaultState(): live snapshot — underlying address, totalAssets,
totalSupply, pricePerShare, decimals.
- balanceOf(user): per-user sAERE share balance.
- convertToAssets / convertToShares: round-trip conversion helpers.
- encodeDeposit / encodeWithdraw / encodeRedeem: ERC-4626 write calldata.
Both clients precompute 4-byte function selectors at module load so the
SDK doesn't depend on a keccak helper. ABI-encoded address and uint
arguments built manually so consumers can use this with either a
high-level library (viem, ethers) or raw fetch against rpc.aere.network.
Re-exported from src/index.ts under 'export * from sink/saere' so the
SDK keeps a single import path.
169 lines
5.7 KiB
TypeScript
169 lines
5.7 KiB
TypeScript
/**
|
|
* 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<unknown>;
|
|
}
|
|
|
|
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 <name>().)
|
|
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<string> {
|
|
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;
|
|
}
|
|
}
|