Repository Public
1import { describe, expect, it } from 'vitest';2import fc from 'fast-check';3import { address as kitAddress, getCompiledTransactionMessageDecoder } from '@solana/kit';4import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';5import { parseTransaction, decodeFunctionData, erc20Abi } from 'viem';6import { base58 } from '@scure/base';7import {8 appendAudit, associatedTokenAddress, assertTransition, buildBtcTx, buildEvmTx, buildSolMessage, canonicalBytes, canTransition,9 checkPolicy, DEFAULT_POLICY, formatUnits, getAsset, intentHash, normalizeAddress, parseUnits, serializeEvmUnsigned, toHex,10 verifyAuditChain, NETWORKS, type AuditRecord, type Intent,11} from '../src/index.ts';1213const now = 1_800_000_000;14const base = { v: 1 as const, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a11', keyId: 'spw-0123456789abcdef', createdAt: now, expiresAt: now + 120, policyVersion: 3 };15const USDC_BASE = NETWORKS.base.tokens[0]!.id, USDC_SOL = NETWORKS.solana.tokens[0]!.id;16const evmIntent: Intent = { ...base, network: 'base', chainRef: '8453', asset: USDC_BASE, symbol: 'USDC', decimals: 6, amount: '12500000',17 from: '0x9858EfFD232B4033E47d90003D41EC34EcaEda94', to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', maxFee: '100000000000000',18 evm: { nonce: 3, gasLimit: '90000', maxFeePerGas: '1000000000', maxPriorityFeePerGas: '1000000' } };19const solIntent: Intent = { ...base, network: 'solana', chainRef: NETWORKS.solana.chainRef, asset: USDC_SOL, symbol: 'USDC', decimals: 6, amount: '10000000',20 from: 'HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk', to: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM', maxFee: '100000',21 sol: { recentBlockhash: 'EkSnNWid2cvwEVnVx9aBqawnmiCNiDgp3gUdkDPTKN1N', lastValidBlockHeight: 1000, createAta: true, computeUnitLimit: 60000, computeUnitPrice: '10000' } };22const btcIntent: Intent = { ...base, network: 'bitcoin', chainRef: NETWORKS.bitcoin.chainRef, asset: 'native', symbol: 'BTC', decimals: 8, amount: '20000',23 from: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu', to: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', maxFee: '2000',24 btc: { inputs: [{ txid: 'aa'.repeat(32), vout: 1, value: '50000' }], change: '29500', fee: '500' } };2526describe('amounts', () => {27 it('parses and formats exactly', () => {28 expect(parseUnits('12.5', 6)).toBe(12_500_000n);29 expect(parseUnits('0,000001', 6)).toBe(1n);30 expect(formatUnits(12_500_000n, 6)).toBe('12.5');31 expect(() => parseUnits('1.0000001', 6)).toThrow();32 expect(() => parseUnits('-1', 6)).toThrow();33 expect(() => parseUnits('1e3', 6)).toThrow();34 expect(() => parseUnits('', 6)).toThrow();35 });36 it('round-trips (property)', () => {37 fc.assert(fc.property(fc.bigInt({ min: 0n, max: 1n << 190n }), fc.integer({ min: 0, max: 18 }), (v, d) => parseUnits(formatUnits(v, d), d) === v));38 });39});4041describe('addresses', () => {42 it('EVM checksum', () => {43 expect(normalizeAddress('base', '0x70997970c51812dc3a010c7d01b50e0d17dc79c8')).toBe('0x70997970C51812dc3A010C7d01b50e0d17dc79C8');44 expect(() => normalizeAddress('base', '0x70997970C51812dc3A010C7d01b50e0d17dc79c8')).toThrow(); // bad checksum45 expect(() => normalizeAddress('base', '0x0000000000000000000000000000000000000000')).toThrow();46 expect(() => normalizeAddress('base', 'HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk')).toThrow();47 });48 it('BTC and Solana', () => {49 expect(normalizeAddress('bitcoin', 'BC1QAR0SRRR7XFKVY5L643LYDNW9RE59GTZZWF5MDQ')).toBe('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq');50 expect(() => normalizeAddress('bitcoin', 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdx')).toThrow();51 expect(() => normalizeAddress('bitcoin', 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx')).toThrow();52 expect(normalizeAddress('solana', solIntent.to)).toBe(solIntent.to);53 expect(() => normalizeAddress('solana', '0x70997970C51812dc3A010C7d01b50e0d17dc79C8')).toThrow();54 });55 it('token identity is by address only', () => {56 expect(getAsset('base', USDC_BASE.toLowerCase()).symbol).toBe('USDC');57 expect(() => getAsset('base', NETWORKS.ethereum.tokens[0]!.id)).toThrow();58 expect(() => getAsset('base', 'USDC')).toThrow();59 });60});6162describe('canonical intent hash', () => {63 it('is stable and independent of JSON key order', () => {64 const shuffled = Object.fromEntries(Object.entries(evmIntent).reverse()) as Intent;65 expect(toHex(intentHash(shuffled))).toBe(toHex(intentHash(evmIntent)));66 });67 it('changes when any material field changes', () => {68 const h = toHex(intentHash(evmIntent));69 const muts: Intent[] = [70 { ...evmIntent, amount: '12500001' }, { ...evmIntent, to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC' },71 { ...evmIntent, network: 'arbitrum', chainRef: '42161' }, { ...evmIntent, asset: 'native' }, { ...evmIntent, maxFee: '100000000000001' },72 { ...evmIntent, expiresAt: now + 121 }, { ...evmIntent, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a12' }, { ...evmIntent, keyId: 'spw-0123456789abcdee' },73 { ...evmIntent, evm: { ...evmIntent.evm!, nonce: 4 } }, { ...evmIntent, evm: { ...evmIntent.evm!, gasLimit: '90001' } },74 { ...evmIntent, evm: { ...evmIntent.evm!, maxFeePerGas: '1000000001' } }, { ...evmIntent, policyVersion: 4 }, { ...evmIntent, decimals: 18 },75 ];76 const hs = new Set(muts.map((m) => toHex(intentHash(m))));77 expect(hs.size).toBe(muts.length);78 expect(hs.has(h)).toBe(false);79 });80 it('field boundaries cannot be shifted', () => {81 const a = canonicalBytes({ ...evmIntent, symbol: 'USDC', keyId: 'spw-0123456789abcdef' });82 const b = canonicalBytes({ ...evmIntent, symbol: 'USD', keyId: 'spw-0123456789abcdefC' });83 expect(toHex(a)).not.toBe(toHex(b));84 });85 it('rejects unknown fields and mismatched sections', () => {86 expect(() => intentHash({ ...evmIntent, extra: 1 } as never)).toThrow();87 expect(() => intentHash({ ...evmIntent, evm: undefined, sol: solIntent.sol })).toThrow();88 });89});9091describe('EVM builder', () => {92 it('builds exactly an ERC-20 transfer to the allowlisted USDC', () => {93 const tx = parseTransaction(serializeEvmUnsigned(evmIntent));94 expect(tx.chainId).toBe(8453);95 expect(tx.to?.toLowerCase()).toBe(USDC_BASE.toLowerCase());96 expect(tx.value ?? 0n).toBe(0n);97 const d = decodeFunctionData({ abi: erc20Abi, data: tx.data! });98 expect(d.functionName).toBe('transfer');99 expect(d.args).toEqual([evmIntent.to, 12_500_000n]);100 });101 it('fails closed', () => {102 expect(() => buildEvmTx({ ...evmIntent, asset: '0xdAC17F958D2ee523a2206206994597C13D831ec7' })).toThrow(/allowlisted/);103 expect(() => buildEvmTx({ ...evmIntent, chainRef: '1' })).toThrow();104 expect(() => buildEvmTx({ ...evmIntent, maxFee: '1' })).toThrow(/fee/);105 expect(() => buildEvmTx({ ...evmIntent, decimals: 18 })).toThrow();106 expect(() => buildEvmTx({ ...evmIntent, to: USDC_BASE })).toThrow();107 expect(() => buildEvmTx({ ...evmIntent, asset: 'native', symbol: 'ETH', decimals: 18 })).toThrow(/21000/);108 });109});110111describe('Solana builder (cross-checked against @solana/kit)', () => {112 it('derives the same associated token accounts as the SDK', async () => {113 for (const owner of [solIntent.from, solIntent.to, 'vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg']) {114 const [pda] = await findAssociatedTokenPda({ owner: kitAddress(owner), mint: kitAddress(USDC_SOL), tokenProgram: TOKEN_PROGRAM_ADDRESS });115 expect(associatedTokenAddress(owner, USDC_SOL)).toBe(pda);116 }117 });118 it('message decodes to the fixed instruction set', () => {119 const m = getCompiledTransactionMessageDecoder().decode(buildSolMessage(solIntent)) as any;120 expect(m.version).toBe('legacy');121 expect(m.header).toEqual({ numSignerAccounts: 1, numReadonlySignerAccounts: 0, numReadonlyNonSignerAccounts: 6 });122 expect(m.staticAccounts[0]).toBe(solIntent.from);123 expect(m.lifetimeToken).toBe(solIntent.sol!.recentBlockhash);124 const progs = m.instructions.map((ix: any) => m.staticAccounts[ix.programAddressIndex]);125 expect(progs).toEqual(['ComputeBudget111111111111111111111111111111', 'ComputeBudget111111111111111111111111111111',126 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA']);127 const xfer = m.instructions[3];128 const accts = xfer.accountIndices.map((k: number) => m.staticAccounts[k]);129 expect(accts).toEqual([associatedTokenAddress(solIntent.from, USDC_SOL), USDC_SOL, associatedTokenAddress(solIntent.to, USDC_SOL), solIntent.from]);130 const data = Buffer.from(xfer.data);131 expect(data[0]).toBe(12);132 expect(data.readBigUInt64LE(1)).toBe(10_000_000n);133 expect(data[9]).toBe(6);134 });135 it('native transfer + fail-closed cases', () => {136 const n: Intent = { ...solIntent, asset: 'native', symbol: 'SOL', decimals: 9, amount: '1000000', sol: { ...solIntent.sol!, createAta: false } };137 const m = getCompiledTransactionMessageDecoder().decode(buildSolMessage(n)) as any;138 expect(m.instructions.length).toBe(3);139 expect(Buffer.from(m.instructions[2].data).readBigUInt64LE(4)).toBe(1_000_000n);140 expect(() => buildSolMessage({ ...solIntent, to: solIntent.from })).toThrow();141 expect(() => buildSolMessage({ ...solIntent, to: USDC_SOL })).toThrow();142 expect(() => buildSolMessage({ ...solIntent, asset: 'So11111111111111111111111111111111111111112' })).toThrow(/allowlisted/);143 expect(() => buildSolMessage({ ...solIntent, maxFee: '5000' })).toThrow(/fee/);144 expect(base58.decode(solIntent.to).length).toBe(32);145 });146});147148describe('registry (wave 1: more networks and tokens)', () => {149 it('every listed asset has a signer limit and every network a fee ceiling; nothing is sendable by default otherwise', () => {150 for (const n of Object.values(NETWORKS)) {151 expect(DEFAULT_POLICY.maxFee[n.id], n.id).toBeDefined();152 for (const a of [n.native, ...n.tokens]) expect(BigInt(DEFAULT_POLICY.perTx[`${n.id}:${a.symbol}`] ?? '0') > 0n, `${n.id}:${a.symbol}`).toBe(true);153 expect(new Set(n.tokens.map((t) => t.id.toLowerCase())).size).toBe(n.tokens.length); // no duplicate contracts154 expect(new Set(n.tokens.map((t) => t.symbol)).size).toBe(n.tokens.length); // one contract per ticker per network155 expect(n.tokens.some((t) => t.symbol === n.native.symbol), `${n.id}: a token carries the native coin's ticker`).toBe(false); // limits are keyed by network:symbol156 }157 expect(Object.keys(NETWORKS).length).toBeGreaterThanOrEqual(10);158 });159 it('EVM token addresses are checksummed and chain ids are distinct', () => {160 const ids = Object.values(NETWORKS).filter((n) => n.family === 'evm').map((n) => n.chainRef);161 expect(new Set(ids).size).toBe(ids.length);162 for (const n of Object.values(NETWORKS)) if (n.family === 'evm') for (const t of n.tokens) expect(normalizeAddress(n.id, t.id)).toBe(t.id);163 });164 it('builds transfers on the new EVM networks with their own chain id', () => {165 for (const [net, chain] of [['optimism', 10], ['bsc', 56], ['avalanche', 43114], ['hyperevm', 999]] as const) {166 const n = NETWORKS[net];167 const tx = parseTransaction(serializeEvmUnsigned({ ...evmIntent, network: net, chainRef: n.chainRef, asset: 'native', symbol: n.native.symbol, decimals: 18, amount: '1000', evm: { ...evmIntent.evm!, gasLimit: '21000' } }));168 expect(tx.chainId).toBe(chain);169 expect(() => buildEvmTx({ ...evmIntent, network: net, chainRef: '1' })).toThrow();170 }171 });172 it('Token-2022 mints use the Token-2022 program for the instruction and the token-account derivation', async () => {173 const py = NETWORKS.solana.tokens.find((t) => t.symbol === 'PYUSD')!;174 expect(py.program).toBe('token2022');175 const T22 = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';176 const [pda] = await findAssociatedTokenPda({ owner: kitAddress(solIntent.from), mint: kitAddress(py.id), tokenProgram: kitAddress(T22) });177 expect(associatedTokenAddress(solIntent.from, py.id, T22)).toBe(pda);178 const m = getCompiledTransactionMessageDecoder().decode(buildSolMessage({ ...solIntent, asset: py.id, symbol: 'PYUSD', decimals: py.decimals })) as any;179 const progs = m.instructions.map((ix: any) => m.staticAccounts[ix.programAddressIndex]);180 expect(progs.slice(2)).toEqual(['ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', T22]);181 expect(m.staticAccounts).not.toContain('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');182 expect(m.instructions[3].accountIndices.map((k: number) => m.staticAccounts[k])[0]).toBe(pda);183 });184});185186describe('Bitcoin builder', () => {187 it('balances inputs, outputs and fee', () => {188 const tx = buildBtcTx(btcIntent);189 expect(tx.inputsLength).toBe(1);190 expect(tx.outputsLength).toBe(2);191 expect(tx.getOutput(0).amount).toBe(20000n);192 expect(tx.getOutput(1).amount).toBe(29500n);193 });194 it('fails closed', () => {195 expect(() => buildBtcTx({ ...btcIntent, btc: { ...btcIntent.btc!, fee: '501' } })).toThrow(/inputs/);196 expect(() => buildBtcTx({ ...btcIntent, maxFee: '100' })).toThrow(/fee/);197 expect(() => buildBtcTx({ ...btcIntent, amount: '100', btc: { ...btcIntent.btc!, change: '49400' } })).toThrow(/dust/);198 expect(() => buildBtcTx({ ...btcIntent, btc: { ...btcIntent.btc!, inputs: [btcIntent.btc!.inputs[0]!, btcIntent.btc!.inputs[0]!] } })).toThrow(/duplicate/);199 expect(() => buildBtcTx({ ...btcIntent, from: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa' })).toThrow(/SegWit/);200 });201});202203describe('policy', () => {204 it('defaults deny unknown assets and enforce limits', () => {205 expect(checkPolicy(DEFAULT_POLICY, evmIntent, 0n, now, false)).toEqual({ ok: true });206 expect(checkPolicy(DEFAULT_POLICY, evmIntent, 0n, now, true).ok).toBe(false);207 expect(checkPolicy(DEFAULT_POLICY, { ...evmIntent, amount: '50000001' }, 0n, now, false).ok).toBe(false);208 expect(checkPolicy(DEFAULT_POLICY, evmIntent, 90_000_000n, now, false).ok).toBe(false);209 expect(checkPolicy(DEFAULT_POLICY, evmIntent, 0n, now + 121, false).ok).toBe(false);210 expect(checkPolicy(DEFAULT_POLICY, { ...evmIntent, expiresAt: now + 3600 }, 0n, now, false).ok).toBe(false);211 expect(checkPolicy(DEFAULT_POLICY, { ...evmIntent, symbol: 'NOPE' }, 0n, now, false).ok).toBe(false);212 expect(checkPolicy(DEFAULT_POLICY, { ...evmIntent, policyVersion: 1 }, 0n, now, false).ok).toBe(false);213 expect(checkPolicy(DEFAULT_POLICY, { ...evmIntent, maxFee: '900000000000000000' }, 0n, now, false).ok).toBe(false);214 expect(checkPolicy({ ...DEFAULT_POLICY, allowlist: ['base:0x0000000000000000000000000000000000000001'] }, evmIntent, 0n, now, false).ok).toBe(false);215 });216});217218describe('audit chain + state machine', () => {219 it('detects tampering', () => {220 const recs: AuditRecord[] = [];221 for (let k = 0; k < 5; k++) recs.push(appendAudit(recs[k - 1] ?? null, 't', 'owner', { k }));222 expect(verifyAuditChain(recs)).toBeNull();223 const bad = structuredClone(recs); bad[2]!.data = { k: 99 };224 expect(verifyAuditChain(bad)).toBe(3);225 expect(verifyAuditChain([recs[0]!, recs[2]!])).not.toBeNull();226 });227 it('only allows legal transitions', () => {228 expect(canTransition('draft', 'pending_approval')).toBe(true);229 expect(canTransition('draft', 'signed')).toBe(false);230 expect(canTransition('pending_approval', 'broadcast')).toBe(false);231 expect(() => assertTransition('finalized', 'draft')).toThrow();232 });233});234