sdk-js/src/sink/AereSinkClient.ts
Liviu 874051f52e feat(sdk): add AereSinkClient + sAereClient (Week-1 flywheel surface)
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.
2026-06-08 03:33:52 +03:00

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;
}
}