sdk-js/src/channels/index.ts
Aere Network 6f13c671f1 0.16.13: repair ~130 client selectors that did not match the deployed contracts (D-512)
Ten clients (RaaS, Sink, Agent and x402 settlement, state channels, compliance,
lending, the compliant pool) carried hand-written function selectors that matched
neither the compiled ABI nor the signature written next to them, so every call
through them reverted. Every selector is now the keccak of a signature in the
compiled ABI. Functions the contracts never had are replaced by the real ones:
AereSanctionsRegistry (latestEpoch, isSanctioned with epoch, isSanctionedLatest,
currentRoot via snapshotStatus), ChainalysisOracleWrapper (upstreamOracle),
AereTravelRuleHashRegistry (status by commitment id), AereInsuranceFund (two-step
proposeMarket + registerMarket). AereRaaSFactoryClient.registerRollup targets the
canonical AereRaaSFactoryV2 (11 parameters, optional defenseGrace).

Proof: the gate aerenew/scripts/deschise/sdk-selectori-corecti.cjs reads 330 of
330 selectors correct and fails on a planted one; src/test/selectors-live.ts reads
18 values from the deployed contracts through the shipped clients, and the old
rollupCount selector still reverts (its negative control).

Also: state-window-live records that both public endpoints now refuse state
outside the 512-block window ("World state unavailable") instead of returning a
false 0x0, and proves the guard against an endpoint that still lies; npm test
runs the offline suite through scripts/run-tests.mjs (61/61), live proofs stay
separate. README network facts brought to 2026-10-15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:10:49 +03:00

114 lines
5.0 KiB
TypeScript

/**
* State Channels SDK — AereStateChannels.
*
* Contract: aerenew/contracts/contracts/channels/AereStateChannels.sol
*
* Off-chain transactions: both parties hold a current `ChannelState` and
* exchange new ones (signed by both) on every payment. On dispute, EITHER
* party submits the latest state to start a 24h challenge window; the other
* can override with a higher-nonce state.
*/
export interface RpcProvider {
request(args: { method: string; params: unknown[] }): Promise<unknown>;
}
export interface ChannelState {
channelId: string; // bytes32
nonce: bigint;
balanceA: bigint;
balanceB: bigint;
htlcLockedFromA: bigint;
htlcLockedFromB: bigint;
lockHash: string; // bytes32 ('0x000...' if no HTLC active)
lockTimeout: bigint; // unix seconds
}
const pad32 = (h: string) => h.toLowerCase().replace(/^0x/, '').padStart(64, '0');
const encUint = (n: bigint) => pad32(n.toString(16));
const encAddr = (a: string) => pad32(a.replace(/^0x/, ''));
const encBytes32 = (b: string) => pad32(b.replace(/^0x/, ''));
async function ethCall(p: RpcProvider, to: string, data: string): Promise<string> {
return await p.request({ method: 'eth_call', params: [{ to, data }, 'latest'] }) as string;
}
async function ethSend(p: RpcProvider, from: string, to: string, data: string): Promise<string> {
return await p.request({ method: 'eth_sendTransaction', params: [{ from, to, data, value: '0x0' }] }) as string;
}
function encChannelState(s: ChannelState): string {
return encBytes32(s.channelId) + encUint(s.nonce) + encUint(s.balanceA) + encUint(s.balanceB) +
encUint(s.htlcLockedFromA) + encUint(s.htlcLockedFromB) +
encBytes32(s.lockHash) + encUint(s.lockTimeout);
}
function encDynBytes(hex: string): { offset: bigint; encoded: string } {
const clean = hex.replace(/^0x/, '');
const bytes = Buffer.from(clean, 'hex');
const padded = Buffer.alloc(Math.ceil(bytes.length / 32) * 32);
bytes.copy(padded);
return { offset: 0n, encoded: encUint(BigInt(bytes.length)) + padded.toString('hex') };
}
const SEL = {
CHALLENGE_WINDOW: '0xd62aad29', // CHALLENGE_WINDOW()
channels: '0x7a7ebd7b', // channels(bytes32)
open: '0xf9bff880', // open(address,address,address,uint256,uint256)
cooperativeClose: '0xeb8edd09', // cooperativeClose((bytes32,uint256,uint256,uint256,uint256,uint256,bytes32,uint256),bytes,bytes)
startChallenge: '0x1b6a88c1', // startChallenge((bytes32,uint256,uint256,uint256,uint256,uint256,bytes32,uint256),bytes,bytes)
overrideChallenge: '0x70c84312', // overrideChallenge((bytes32,uint256,uint256,uint256,uint256,uint256,bytes32,uint256),bytes,bytes)
finalize: '0x92584d80', // finalize(bytes32)
settleHtlc: '0x9be71c4d', // settleHtlc(bytes32,bytes)
withdraw: '0x8e19899e', // withdraw(bytes32)
} as const;
export class AereStateChannelsClient {
constructor(public readonly provider: RpcProvider, public readonly address: string) {}
async open(from: string, partyA: string, partyB: string, token: string, depositA: bigint, depositB: bigint): Promise<string> {
const data = SEL.open + encAddr(partyA) + encAddr(partyB) + encAddr(token) + encUint(depositA) + encUint(depositB);
return ethSend(this.provider, from, this.address, data);
}
async finalize(from: string, channelId: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.finalize + encBytes32(channelId));
}
async settleHtlc(from: string, channelId: string, preimage: string): Promise<string> {
const dyn = encDynBytes(preimage);
const data = SEL.settleHtlc + encBytes32(channelId) + encUint(64n) + dyn.encoded;
return ethSend(this.provider, from, this.address, data);
}
async withdraw(from: string, channelId: string): Promise<string> {
return ethSend(this.provider, from, this.address, SEL.withdraw + encBytes32(channelId));
}
/** Helper: encode the EIP-712 typed-data object for off-chain signing. */
buildStateTypedData(chainId: number, state: ChannelState): Record<string, unknown> {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
ChannelState: [
{ name: 'channelId', type: 'bytes32' },
{ name: 'nonce', type: 'uint256' },
{ name: 'balanceA', type: 'uint256' },
{ name: 'balanceB', type: 'uint256' },
{ name: 'htlcLockedFromA', type: 'uint256' },
{ name: 'htlcLockedFromB', type: 'uint256' },
{ name: 'lockHash', type: 'bytes32' },
{ name: 'lockTimeout', type: 'uint256' },
],
},
primaryType: 'ChannelState',
domain: { name: 'AereStateChannels', version: '1', chainId, verifyingContract: this.address },
message: state,
};
}
}