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.
This commit is contained in:
Liviu 2026-06-07 02:50:43 +03:00
parent 88626e43ae
commit 136b95c3a4
5 changed files with 326 additions and 0 deletions

View File

@ -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';

1
src/saere/index.ts Normal file
View File

@ -0,0 +1 @@
export { SAereClient, type SAereClientConfig } from './sAEREClient.js';

151
src/saere/sAEREClient.ts Normal file
View File

@ -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<bigint> {
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<bigint> {
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<bigint> {
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<string> {
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;
}
}

168
src/sink/AereSinkClient.ts Normal file
View File

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

1
src/sink/index.ts Normal file
View File

@ -0,0 +1 @@
export { AereSinkClient, type AereSinkConfig, type RpcProvider } from './AereSinkClient.js';