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:
parent
88626e43ae
commit
136b95c3a4
@ -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
1
src/saere/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { SAereClient, type SAereClientConfig } from './sAEREClient.js';
|
||||
151
src/saere/sAEREClient.ts
Normal file
151
src/saere/sAEREClient.ts
Normal 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
168
src/sink/AereSinkClient.ts
Normal 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
1
src/sink/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AereSinkClient, type AereSinkConfig, type RpcProvider } from './AereSinkClient.js';
|
||||
Loading…
Reference in New Issue
Block a user