SPB Git

Repository Public

core.test.ts 15.0 KB 234 lines typescript

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
Files main
Menu

Theme