/** * 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; } 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 { 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 { 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 { 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 { return ethSend(this.provider, from, this.address, SEL.finalize + encBytes32(channelId)); } async settleHtlc(from: string, channelId: string, preimage: string): Promise { 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 { 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 { 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, }; } }