SPB Git

Repository Public

stellar.test.ts 5.2 KB 86 lines typescript

1import { createHash } from 'node:crypto';2import { mkdtempSync } from 'node:fs';3import { tmpdir } from 'node:os';4import { join } from 'node:path';5import { describe, expect, it } from 'vitest';6import { Keypair, Networks, TransactionBuilder, type Transaction } from '@stellar/stellar-base';7import { intentHash, NETWORKS, verifyAuditChain, type Intent } from '@spw/core';8import { TestPasskey } from '../../../tests/helpers/passkey.ts';9import { deriveKeys } from '../src/keys.ts';10import { Signer } from '../src/signer.ts';11import { Store } from '../src/store.ts';1213// Public BIP-39 test vector mnemonic — holds no funds, never used outside tests.14const keys = deriveKeys('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about');15const RP = 'wallet.test', ORIGIN = 'https://wallet.test';16const DEST = 'GDVSYYTUAJ3ACHTPQNSTQBDQ4LDHQCMNY4FCEQH5TJUMSSLWQSTG42MV'; // SEP-0005 test 5, account 11718async function setup() {19  const dir = mkdtempSync(join(tmpdir(), 'spw-signer-xlm-'));20  const store = new Store(join(dir, 'state.json'), join(dir, 'policy.json'));21  store.state.setupTokenHash = createHash('sha256').update('setup-token').digest('hex');22  const t = Math.floor(Date.now() / 1000);23  const signer = new Signer(keys, store, { rpId: RP, origin: ORIGIN }, () => t);24  const pk = new TestPasskey(RP, ORIGIN);25  await signer.enroll({ credential: pk.credential(), setupToken: 'setup-token' });26  return { store, signer, pk, t };27}28let n = 0;29const xlm = (t: number, over: Partial<Intent> = {}): Intent => ({30  v: 1, proposalId: `6f1c1f0e-8f5f-4b1e-9c56-${String(++n).padStart(12, '0')}`, keyId: keys.keyId, createdAt: t, expiresAt: t + 120, policyVersion: 3,31  network: 'stellar', chainRef: NETWORKS.stellar.chainRef, asset: 'native', symbol: 'XLM', decimals: 7, amount: '25000000', from: keys.addresses.stellar!, to: DEST, maxFee: '10112',32  memo: '4242', x: { op: 'payment', sequence: '2390162185806736', fee: '10112', maxTime: String(t + 180), memoType: 'id' }, ...over,33});3435describe('stellar through the signer', () => {36  it('derives the SEP-0005 account from the mnemonic (m/44\'/148\'/0\')', () => {37    expect(keys.addresses.stellar).toBe('GB3JDWCQJCWMJ3IILWIGDTQJJC5567PGVEVXSCVPEQOTDN64VJBDQBYX');38  });3940  it('signs an approved XLM payment; the official SDK decodes it and verifies the signature', async () => {41    const { signer, pk, t, store } = await setup();42    const i = xlm(t);43    const r = await signer.sign(i, pk.assert(intentHash(i)));44    const tx = TransactionBuilder.fromXDR(r.raw, Networks.PUBLIC) as Transaction;45    expect(tx.hash().toString('hex')).toBe(r.txid);46    expect([tx.source, tx.fee, tx.sequence, tx.timeBounds?.maxTime, tx.memo.type, tx.memo.value]).toEqual([i.from, '10112', i.x!.sequence, String(t + 180), 'id', '4242']);47    expect(tx.operations).toHaveLength(1);48    const op = tx.operations[0]!;49    expect(op.type === 'payment' && op.destination === DEST && op.asset.isNative() && op.amount === '2.5000000').toBe(true);50    expect(tx.signatures).toHaveLength(1);51    expect(Keypair.fromPublicKey(keys.addresses.stellar!).verify(tx.hash(), tx.signatures[0]!.signature())).toBe(true);52    expect(verifyAuditChain(store.state.journal)).toBeNull();53    await expect(signer.sign(i, pk.assert(intentHash(i)))).rejects.toThrow(/already signed/);54  });5556  it('signs a CreateAccount only when the approved intent says so', async () => {57    const { signer, pk, t } = await setup();58    const { memo: _m, ...rest } = xlm(t);59    const i: Intent = { ...rest, x: { ...rest.x!, op: 'createAccount', memoType: 'none' } };60    const r = await signer.sign(i, pk.assert(intentHash(i)));61    const op = (TransactionBuilder.fromXDR(r.raw, Networks.PUBLIC) as Transaction).operations[0]!;62    expect(op.type === 'createAccount' && op.destination === DEST && op.startingBalance === '2.5000000').toBe(true);63    // an approval given for a payment cannot be reused for the other operation64    const pay = xlm(t), swapped = { ...pay, x: { ...pay.x!, op: 'createAccount' } };65    await expect(signer.sign(swapped, pk.assert(intentHash(pay)))).rejects.toThrow(/approval/);66  });6768  it('refuses unsupported or out-of-policy Stellar intents before looking at the approval', async () => {69    const { signer, pk, t } = await setup();70    const cases: [Partial<Intent>, RegExp][] = [71      [{ chainRef: 'Test SDF Network ; September 2015' }, /chain identity/],72      [{ from: DEST }, /not controlled/],73      [{ to: keys.addresses.stellar! }, /equals source/],74      [{ to: DEST.toLowerCase() }, /Stellar address/],75      [{ asset: 'USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' }, /native/],76      [{ x: { ...xlm(t).x!, fee: '10113' } }, /above approved maximum/],77      [{ x: { ...xlm(t).x!, memoType: 'text' } }, /memoType/],78      [{ x: { ...xlm(t).x!, extra: '1' } }, /unknown or missing/],79      [{ x: { ...xlm(t).x!, maxTime: String(t + 3600) } }, /too far/],80      [{ amount: '3500000001' }, /per-transaction limit/], // default cap: 350 XLM81      [{ maxFee: '1000001', x: { ...xlm(t).x!, fee: '1000001' } }, /fee ceiling/],82    ];83    for (const [over, re] of cases) { const i = xlm(t, over); await expect(signer.sign(i, pk.assert(intentHash(i))), JSON.stringify(over)).rejects.toThrow(re); }84  });85});86
Files main
Menu

Theme