import { CreatedPdaAccount, UtxoMetaData } from '@saturnbtcio/arch-sdk'; import { getUtxosIdsFromPsbts } from '@saturnbtcio/psbt'; import { NETWORK, OutScript } from '@scure/btc-signer'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { PoolErrorException } from '../../error/pool.error'; import { buildUtxoInfoFromInputs, buildUtxoInfoFromOutputs, } from '../utxo-info'; interface TestUtxo extends UtxoMetaData { value: bigint; collectionStatuses: any[]; hasInscription?: boolean; status: { confirmed: boolean; }; } interface Wallet { address: string; balance: bigint; utxos: TestUtxo[]; } vi.mock('@saturnbtcio/psbt', async () => { const actual = await vi.importActual('@saturnbtcio/psbt'); return { ...actual, getTotalSizeAndFeesFromAncestors: vi.fn((txStatuses: any[]) => { return txStatuses.length === 0 ? { totalAncestorsSize: 0, totalAncestorsFee: 0n } : { totalAncestorsSize: 100, totalAncestorsFee: 200n }; }), getUtxosIdsFromPsbts: vi.fn(() => ['tx1:0', 'tx2:0']), containsOpReturn: (script: Uint8Array) => script[0] === 106, }; }); vi.mock('base64', () => ({ encode: (buf: Buffer) => buf.toString(), })); const dummyRunestone = { edicts: [ { output: 0, id: { block: 123n, tx: 456 }, amount: 1000n }, { output: 2, id: { block: 123n, tx: 456 }, amount: 500n }, ], encipher: () => 'X'.repeat(30), // length = 30 }; vi.mock('@saturnbtcio/ordinals-lib', () => ({ Runestone: { decipher: vi.fn(() => dummyRunestone), default: () => ({ pointer: undefined, edicts: [] as any[], encipher: () => 'X'.repeat(30), }), }, })); class DummyTransaction { outputsLength: number; vsize: number; fee: bigint; id: string; private outputs: any[]; constructor(outputs: any[], vsize: number, fee: bigint, id: string) { this.outputs = outputs; this.outputsLength = outputs.length; this.vsize = vsize; this.fee = fee; this.id = id; } getOutput(i: number) { return this.outputs[i]; } toPSBT(_flag: number) { return Buffer.from('dummyPSBT'); } } vi.mock('@scure/btc-signer', async () => { const actual = await vi.importActual('@scure/btc-signer'); return { ...actual, Address: (network: any) => ({ encode: (decoded: any) => { if (decoded === 'decodedProg') return 'programAddress'; if (decoded === 'decodedAccount') return 'accountAddress'; return 'otherAddress'; }, }), }; }); vi.spyOn(OutScript, 'decode').mockImplementation((script: Uint8Array) => { const str = Buffer.from(script).toString(); if (str === 'prog') return 'decodedProg' as any; if (str === 'acct') return 'decodedAccount' as any; return 'decodedOther' as any; }); describe('buildUtxoInfoFromInputs', () => { let wallet: Wallet; beforeEach(() => { // Create a dummy wallet with two utxos. wallet = { address: 'dummyWalletAddress', balance: 1000000n, utxos: [ { txid: 'tx1', vout: 0, value: 1000n, collectionStatuses: [], status: { confirmed: true }, }, { txid: 'tx2', vout: 0, value: 2000n, collectionStatuses: [], status: { confirmed: true }, }, ], }; }); it('Returns UTXO metadata for each input', () => { const psbtBase64 = 'dummyPsbtBase64'; const result = buildUtxoInfoFromInputs(psbtBase64, wallet.utxos as any); expect(result).toEqual([ { txid: 'tx1', vout: 0 }, { txid: 'tx2', vout: 0 }, ]); }); it('Throws an error if any input utxo is not found in the wallet', () => { vi.mocked(getUtxosIdsFromPsbts).mockReturnValue(['tx1:0', 'tx3:0']); const psbtBase64 = 'dummyPsbtBase64'; expect(() => buildUtxoInfoFromInputs(psbtBase64, wallet.utxos as any), ).toThrow(PoolErrorException); }); }); describe('buildUtxoInfoFromOutputs', () => { let dummyTx: DummyTransaction; let walletUtxos: TestUtxo[]; let accounts: CreatedPdaAccount[]; const programAddress = 'programAddress'; const network: typeof NETWORK = { bech32: 'bc', pubKeyHash: 0x00, scriptHash: 0x05, wif: 0x80, }; beforeEach(() => { walletUtxos = [ { txid: 'dummyTxId', vout: 0, value: 1000n, collectionStatuses: [], status: { confirmed: true }, }, ]; accounts = [{ address: 'accountAddress', pubkey: 'dummyPubkey' }]; }); it('Returns UTXO metadata for outputs matching programAddress', () => { // Create dummy outputs: // Output 0: valid script that decodes to programAddress. // Output 1: has no script (should be skipped). // Output 2: valid script that decodes to an account address (present in accounts). const outputs = [ { script: Buffer.from('prog') }, // OutScript.decode returns 'decodedProg', Address.encode returns 'programAddress' { script: null }, { script: Buffer.from('acct') }, // OutScript.decode returns 'decodedAccount', Address.encode returns 'accountAddress' ]; dummyTx = new DummyTransaction(outputs, 250, 50n, 'dummyTxId'); const result = buildUtxoInfoFromOutputs( dummyTx as any, programAddress, accounts, network as any, walletUtxos as any, new Map(), ); expect(result).toEqual([ { txid: 'dummyTxId', vout: 0 }, { txid: 'dummyTxId', vout: 2 }, ]); }); it('Skips outputs that have no script or contain an OP_RETURN', () => { const opReturnScript = new Uint8Array([106, 1, 2, 3]); // starts with 106 → OP_RETURN const outputs = [ { script: Buffer.from('prog') }, { script: opReturnScript }, { script: Buffer.from('acct') }, { script: null }, ]; dummyTx = new DummyTransaction(outputs, 250, 50n, 'dummyTxId'); const result = buildUtxoInfoFromOutputs( dummyTx as any, programAddress, accounts, network, walletUtxos as any, new Map(), ); expect(result).toEqual([ { txid: 'dummyTxId', vout: 0 }, { txid: 'dummyTxId', vout: 2 }, ]); }); it('Returns an empty array if no outputs match', () => { const opReturnScript = new Uint8Array([106, 1, 2, 3]); const outputs = [{ script: null }, { script: opReturnScript }]; dummyTx = new DummyTransaction(outputs, 250, 50n, 'dummyTxId'); const result = buildUtxoInfoFromOutputs( dummyTx as any, programAddress, accounts, network, walletUtxos as any, new Map(), ); expect(result).toEqual([]); }); });