SPB Git

Repository Public

aptos.test.ts 5.9 KB 92 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 { Account, Deserializer, SignedTransaction, TransactionAuthenticatorEd25519, TransactionPayloadEntryFunction, generateSigningMessage } from '@aptos-labs/ts-sdk';7import { aptosRawBytes, aptosTxid, 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 of ours, never used outside tests.14const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';15const keys = deriveKeys(MNEMONIC);16const RP = 'wallet.test', ORIGIN = 'https://wallet.test';17const DEST = Account.fromDerivationPath({ path: "m/44'/637'/1'/0'/0'", mnemonic: MNEMONIC, legacy: true }).accountAddress.toStringLong();1819async function setup() {20  const dir = mkdtempSync(join(tmpdir(), 'spw-signer-apt-'));21  const store = new Store(join(dir, 'state.json'), join(dir, 'policy.json'));22  store.state.setupTokenHash = createHash('sha256').update('setup-token').digest('hex');23  const t = Math.floor(Date.now() / 1000);24  const signer = new Signer(keys, store, { rpId: RP, origin: ORIGIN }, () => t);25  const pk = new TestPasskey(RP, ORIGIN);26  await signer.enroll({ credential: pk.credential(), setupToken: 'setup-token' });27  return { store, signer, pk, t };28}29let n = 0;30const apt = (t: number, over: Partial<Intent> = {}): Intent => ({31  v: 1, proposalId: `6f1c1f0e-8f5f-4b1e-9c56-${String(++n).padStart(12, '0')}`, keyId: keys.keyId, createdAt: t, expiresAt: t + 120, policyVersion: 3,32  network: 'aptos', chainRef: NETWORKS.aptos.chainRef, asset: 'native', symbol: 'APT', decimals: 8, amount: '250000000', from: keys.addresses.aptos!, to: DEST, maxFee: '812200',33  x: { sequence_number: '7', max_gas_amount: '8122', gas_unit_price: '100', expiration_timestamp_secs: String(t + 180) }, ...over,34});3536describe('aptos through the signer', () => {37  it("derives the Petra / official SDK account from the mnemonic (m/44'/637'/0'/0'/0')", () => {38    expect(keys.addresses.aptos).toBe('0xeb663b681209e7087d681c5d3eed12aaa8e1915e7c87794542c3f96e94b3d3bf');39    expect(keys.addresses.aptos).toBe(Account.fromDerivationPath({ path: "m/44'/637'/0'/0'/0'", mnemonic: MNEMONIC, legacy: true }).accountAddress.toStringLong());40  });4142  it('signs an approved APT transfer; the official SDK decodes it and verifies the signature', async () => {43    const { signer, pk, t, store } = await setup();44    const i = apt(t);45    const r = await signer.sign(i, pk.assert(intentHash(i)));46    const bytes = aptosRawBytes(r.raw);47    expect(r.txid).toBe(aptosTxid(bytes));48    const d = new Deserializer(bytes), tx = SignedTransaction.deserialize(d);49    expect(d.remaining()).toBe(0);50    const raw = tx.raw_txn, ef = (raw.payload as TransactionPayloadEntryFunction).entryFunction;51    expect([raw.sender.toStringLong(), raw.sequence_number, raw.max_gas_amount, raw.gas_unit_price, raw.expiration_timestamp_secs, raw.chain_id.chainId]).toEqual([i.from, 7n, 8122n, 100n, BigInt(t + 180), 1]);52    expect([ef.module_name.address.toString(), ef.module_name.name.identifier, ef.function_name.identifier, ef.type_args.length, ef.args.length]).toEqual(['0x1', 'aptos_account', 'transfer', 0, 2]);53    expect('0x' + Buffer.from(ef.args[0]!.bcsToBytes()).toString('hex')).toBe(DEST);54    expect(Buffer.from(ef.args[1]!.bcsToBytes()).readBigUInt64LE()).toBe(250_000_000n);55    const auth = tx.authenticator as TransactionAuthenticatorEd25519;56    expect(tx.authenticator).toBeInstanceOf(TransactionAuthenticatorEd25519);57    expect(auth.public_key.authKey().derivedAddress().toStringLong()).toBe(keys.addresses.aptos);58    expect(auth.public_key.verifySignature({ message: generateSigningMessage(raw.bcsToBytes(), 'APTOS::RawTransaction'), signature: auth.signature })).toBe(true);59    expect(verifyAuditChain(store.state.journal)).toBeNull();60    await expect(signer.sign(i, pk.assert(intentHash(i)))).rejects.toThrow(/already signed/);61  });6263  it('an approval cannot be reused for other transaction parameters', async () => {64    const { signer, pk, t } = await setup();65    const ok = apt(t);66    for (const x of [{ sequence_number: '8' }, { max_gas_amount: '8121' }, { max_gas_amount: '8000', gas_unit_price: '101' }, { expiration_timestamp_secs: String(t + 181) }]) {67      const swapped = { ...ok, x: { ...ok.x!, ...x } };68      await expect(signer.sign(swapped, pk.assert(intentHash(ok))), JSON.stringify(x)).rejects.toThrow(/approval/);69    }70  });7172  it('refuses unsupported or out-of-policy Aptos intents before looking at the approval', async () => {73    const { signer, pk, t } = await setup();74    const cases: [Partial<Intent>, RegExp][] = [75      [{ chainRef: '2' }, /chain identity/],76      [{ from: DEST }, /not controlled/],77      [{ to: keys.addresses.aptos! }, /equals source/],78      [{ to: '0x' + DEST.slice(2).toUpperCase() }, /Aptos address/],79      [{ to: '0x' + '0'.repeat(63) + '1' }, /reserved/],80      [{ asset: '0x1::aptos_coin::AptosCoin' }, /native/],81      [{ memo: 'hello' }, /no memo/],82      [{ x: { ...apt(t).x!, max_gas_amount: '8123' } }, /above approved maximum/],83      [{ x: { ...apt(t).x!, gas_unit_price: '99' } }, /range/],84      [{ x: { ...apt(t).x!, function: '0x1::coin::transfer' } }, /unknown or missing/],85      [{ x: { ...apt(t).x!, expiration_timestamp_secs: String(t + 3600) } }, /too far/],86      [{ amount: '8800000001' }, /per-transaction limit/], // default cap: 88 APT87      [{ maxFee: '10000100', x: { ...apt(t).x!, max_gas_amount: '100001' } }, /fee ceiling/],88    ];89    for (const [over, re] of cases) { const i = apt(t, over); await expect(signer.sign(i, pk.assert(intentHash(i))), JSON.stringify(over)).rejects.toThrow(re); }90  });91});92
Files main
Menu

Theme