UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

342 lines (303 loc) 13.6 kB
import { beforeAll, describe, expect, it } from 'vitest'; import { networks, payments, toHex } from '@btc-vision/bitcoin'; import { type UniversalSigner } from '@btc-vision/ecpair'; import type { FundingSpecificData, ISerializableTransactionState, UTXO, } from '../../build/opnet.js'; import { ChainId, currentConsensus, EcKeyPair, OfflineTransactionManager, SERIALIZATION_FORMAT_VERSION, TransactionSerializer, TransactionType, } from '../../build/opnet.js'; describe('Browser Parallel Signing', () => { const network = networks.regtest; let signer1: UniversalSigner; let signer2: UniversalSigner; let signer3: UniversalSigner; let defaultSigner: UniversalSigner; let address1: string; let address2: string; let address3: string; let defaultAddress: string; beforeAll(() => { signer1 = EcKeyPair.generateRandomKeyPair(network); signer2 = EcKeyPair.generateRandomKeyPair(network); signer3 = EcKeyPair.generateRandomKeyPair(network); defaultSigner = EcKeyPair.generateRandomKeyPair(network); address1 = EcKeyPair.getTaprootAddress(signer1, network); address2 = EcKeyPair.getTaprootAddress(signer2, network); address3 = EcKeyPair.getTaprootAddress(signer3, network); defaultAddress = EcKeyPair.getTaprootAddress(defaultSigner, network); }); const createTaprootUtxo = ( address: string, value: bigint, txId: string = '0'.repeat(64), index: number = 0, ): UTXO => { const p2tr = payments.p2tr({ address, network }); return { transactionId: txId, outputIndex: index, value, scriptPubKey: { hex: toHex(p2tr.output as Uint8Array), address, }, }; }; const createFundingParams = ( signer: UniversalSigner, from: string, to: string, txId: string, ) => ({ signer, mldsaSigner: null, network, utxos: [createTaprootUtxo(from, 100000n, txId, 0)], from, to, feeRate: 10, priorityFee: 1000n, gasSatFee: 500n, amount: 50000n, }); describe('Concurrent Signing (2 transactions)', () => { it('should sign two transactions concurrently', async () => { const params1 = createFundingParams( defaultSigner, defaultAddress, address2, 'a'.repeat(64), ); const params2 = createFundingParams(signer1, address1, address3, 'b'.repeat(64)); const state1 = OfflineTransactionManager.exportFunding(params1); const state2 = OfflineTransactionManager.exportFunding(params2); const [tx1, tx2] = await Promise.all([ OfflineTransactionManager.importSignAndExport(state1, { signer: defaultSigner }), OfflineTransactionManager.importSignAndExport(state2, { signer: signer1 }), ]); expect(tx1).toBeDefined(); expect(tx2).toBeDefined(); expect(/^[0-9a-f]+$/i.test(tx1)).toBe(true); expect(/^[0-9a-f]+$/i.test(tx2)).toBe(true); expect(tx1).not.toBe(tx2); }); }); describe('Concurrent Signing (3 transactions)', () => { it('should sign three transactions concurrently', async () => { const params1 = createFundingParams( defaultSigner, defaultAddress, address1, 'c'.repeat(64), ); const params2 = createFundingParams(signer1, address1, address2, 'd'.repeat(64)); const params3 = createFundingParams(signer2, address2, address3, 'e'.repeat(64)); const state1 = OfflineTransactionManager.exportFunding(params1); const state2 = OfflineTransactionManager.exportFunding(params2); const state3 = OfflineTransactionManager.exportFunding(params3); const [tx1, tx2, tx3] = await Promise.all([ OfflineTransactionManager.importSignAndExport(state1, { signer: defaultSigner }), OfflineTransactionManager.importSignAndExport(state2, { signer: signer1 }), OfflineTransactionManager.importSignAndExport(state3, { signer: signer2 }), ]); expect(tx1).toBeDefined(); expect(tx2).toBeDefined(); expect(tx3).toBeDefined(); expect(/^[0-9a-f]+$/i.test(tx1)).toBe(true); expect(/^[0-9a-f]+$/i.test(tx2)).toBe(true); expect(/^[0-9a-f]+$/i.test(tx3)).toBe(true); }); }); describe('Concurrent Fee Bumping', () => { it('should bump and sign multiple transactions concurrently', async () => { const params1 = createFundingParams( defaultSigner, defaultAddress, address2, 'f'.repeat(64), ); const params2 = createFundingParams(signer1, address1, address3, '1'.repeat(64)); const state1 = OfflineTransactionManager.exportFunding(params1); const state2 = OfflineTransactionManager.exportFunding(params2); const bumped1 = OfflineTransactionManager.rebuildWithNewFees(state1, 30); const bumped2 = OfflineTransactionManager.rebuildWithNewFees(state2, 50); const [tx1, tx2] = await Promise.all([ OfflineTransactionManager.importSignAndExport(bumped1, { signer: defaultSigner }), OfflineTransactionManager.importSignAndExport(bumped2, { signer: signer1 }), ]); expect(tx1).toBeDefined(); expect(tx2).toBeDefined(); expect(/^[0-9a-f]+$/i.test(tx1)).toBe(true); expect(/^[0-9a-f]+$/i.test(tx2)).toBe(true); }); }); describe('Concurrent Serialization', () => { it('should serialize and deserialize multiple states concurrently', async () => { const createState = (from: string, to: string): ISerializableTransactionState => ({ header: { formatVersion: SERIALIZATION_FORMAT_VERSION, consensusVersion: currentConsensus, transactionType: TransactionType.FUNDING, chainId: ChainId.Bitcoin, timestamp: Date.now(), }, baseParams: { from, to, feeRate: 10, priorityFee: '1000', gasSatFee: '500', networkName: 'regtest', txVersion: 2, anchor: false, }, utxos: [ { transactionId: '0'.repeat(64), outputIndex: 0, value: '100000', scriptPubKeyHex: 'aa', scriptPubKeyAddress: from, }, ], optionalInputs: [], optionalOutputs: [], addressRotationEnabled: false, signerMappings: [], typeSpecificData: { type: TransactionType.FUNDING, amount: '50000', splitInputsInto: 1, } as FundingSpecificData, precomputedData: {}, }); const states = [ createState(address1, address2), createState(address2, address3), createState(address3, address1), createState(defaultAddress, address1), createState(defaultAddress, address2), ]; // Serialize all concurrently const serialized = await Promise.all( states.map((state) => Promise.resolve(TransactionSerializer.serialize(state))), ); // Deserialize all concurrently const deserialized = await Promise.all( serialized.map((data) => Promise.resolve(TransactionSerializer.deserialize(data))), ); // Verify all round-tripped correctly for (let i = 0; i < states.length; i++) { const d = deserialized[i]; const s = states[i]; expect(d).toBeDefined(); expect(s).toBeDefined(); expect(d?.baseParams.from).toBe(s?.baseParams.from); expect(d?.baseParams.to).toBe(s?.baseParams.to); } }); it('should handle 10 concurrent serialize/deserialize operations', () => { const results = Array.from({ length: 10 }, (_, i) => { const state: ISerializableTransactionState = { header: { formatVersion: SERIALIZATION_FORMAT_VERSION, consensusVersion: currentConsensus, transactionType: TransactionType.FUNDING, chainId: ChainId.Bitcoin, timestamp: Date.now() + i, }, baseParams: { from: address1, to: address2, feeRate: 10 + i, priorityFee: String(1000 + i), gasSatFee: '500', networkName: 'regtest', txVersion: 2, anchor: false, }, utxos: [ { transactionId: String(i).repeat(64).slice(0, 64), outputIndex: i, value: String(100000 + i), scriptPubKeyHex: 'aa', scriptPubKeyAddress: address1, }, ], optionalInputs: [], optionalOutputs: [], addressRotationEnabled: false, signerMappings: [], typeSpecificData: { type: TransactionType.FUNDING, amount: String(50000 + i), splitInputsInto: 1, } as FundingSpecificData, precomputedData: {}, }; const serialized = TransactionSerializer.serialize(state); const deserialized = TransactionSerializer.deserialize(serialized); return { original: state, deserialized }; }); expect(results).toHaveLength(10); for (const { original, deserialized } of results) { expect(deserialized.baseParams.feeRate).toBeCloseTo(original.baseParams.feeRate, 3); expect(deserialized.baseParams.priorityFee).toBe(original.baseParams.priorityFee); } }); }); describe('Concurrent Two-Step Signing', () => { it('should import and sign multiple transactions via two-step process', async () => { const params1 = createFundingParams(signer1, address1, address2, '7'.repeat(64)); const params2 = createFundingParams(signer2, address2, address3, '8'.repeat(64)); const state1 = OfflineTransactionManager.exportFunding(params1); const state2 = OfflineTransactionManager.exportFunding(params2); const builder1 = OfflineTransactionManager.importForSigning(state1, { signer: signer1, }); const builder2 = OfflineTransactionManager.importForSigning(state2, { signer: signer2, }); const [tx1, tx2] = await Promise.all([ OfflineTransactionManager.signAndExport(builder1), OfflineTransactionManager.signAndExport(builder2), ]); expect(tx1).toBeDefined(); expect(tx2).toBeDefined(); expect(/^[0-9a-f]+$/i.test(tx1)).toBe(true); expect(/^[0-9a-f]+$/i.test(tx2)).toBe(true); }); }); describe('Concurrent Export/Import/Validate', () => { it('should export, validate, and inspect multiple states concurrently', async () => { const allParams = [ createFundingParams(defaultSigner, defaultAddress, address1, '9'.repeat(64)), createFundingParams(signer1, address1, address2, 'a1'.repeat(32)), createFundingParams(signer2, address2, address3, 'b2'.repeat(32)), createFundingParams(signer3, address3, defaultAddress, 'c3'.repeat(32)), ]; const exported = allParams.map((p) => OfflineTransactionManager.exportFunding(p)); const [validations, inspections] = await Promise.all([ Promise.all(exported.map((e) => Promise.resolve(OfflineTransactionManager.validate(e)))), Promise.all(exported.map((e) => Promise.resolve(OfflineTransactionManager.inspect(e)))), ]); for (const valid of validations) { expect(valid).toBe(true); } expect(inspections[0]?.baseParams.from).toBe(defaultAddress); expect(inspections[1]?.baseParams.from).toBe(address1); expect(inspections[2]?.baseParams.from).toBe(address2); expect(inspections[3]?.baseParams.from).toBe(address3); }); }); });