From 8783f95b42b824d6cb8eba678450979c8495afa3 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 5 Dec 2025 09:52:59 +0100 Subject: [PATCH 1/6] feat(abstract-utxo): add encodeTransaction function Add helper function to convert various transaction types to a Buffer Issue: BTC-2806 Co-authored-by: llm-git --- modules/abstract-utxo/src/transaction/decode.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/abstract-utxo/src/transaction/decode.ts b/modules/abstract-utxo/src/transaction/decode.ts index e4077d0224..feccd9de4d 100644 --- a/modules/abstract-utxo/src/transaction/decode.ts +++ b/modules/abstract-utxo/src/transaction/decode.ts @@ -57,3 +57,15 @@ export function decodePsbtWith( return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, toNetworkName(network)); } } + +export function encodeTransaction( + transaction: utxolib.bitgo.UtxoTransaction | utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt +): Buffer { + if (transaction instanceof utxolib.bitgo.UtxoTransaction) { + return transaction.toBuffer(); + } else if (transaction instanceof utxolib.bitgo.UtxoPsbt) { + return transaction.toBuffer(); + } else { + return Buffer.from(transaction.serialize()); + } +} From 19cd2358b8aef0a2d97522dbd1fde66b593aff7e Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 5 Dec 2025 09:53:52 +0100 Subject: [PATCH 2/6] feat(abstract-utxo): add fixedScriptWallet to decodeTransactionAsPsbt Support wasm-utxo based fixed script wallet PSBTs in the getMusig2Nonces method by implementing Musig2Participant for both PSBT types. Issue: BTC-2806 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 7ae4b5f961..882ce5a82a 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -42,6 +42,7 @@ import { isValidPrv, isValidXprv, } from '@bitgo/sdk-core'; +import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import { backupKeyRecovery, @@ -68,6 +69,7 @@ import { verifyTransaction, } from './transaction'; import type { TransactionExplanation } from './transaction/fixedScript/explainTransaction'; +import { Musig2Participant } from './transaction/fixedScript/musig2'; import { AggregateValidationError, ErrorMissingOutputs, @@ -77,7 +79,7 @@ import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptor import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names'; import { assertFixedScriptWalletAddress } from './address/fixedScript'; import { ParsedTransaction } from './transaction/types'; -import { decodePsbtWith, stringToBufferTryFormats } from './transaction/decode'; +import { decodePsbtWith, encodeTransaction, stringToBufferTryFormats } from './transaction/decode'; import { toBip32Triple, UtxoKeychain } from './keychains'; import { verifyKeySignature, verifyUserPublicKey } from './verifyKey'; import { getPolicyForEnv } from './descriptor/validatePolicy'; @@ -360,7 +362,10 @@ export interface SignPsbtResponse { psbt: string; } -export abstract class AbstractUtxoCoin extends BaseCoin { +export abstract class AbstractUtxoCoin + extends BaseCoin + implements Musig2Participant, Musig2Participant +{ public altScriptHash?: number; public supportAltScriptDestination?: boolean; public readonly amountType: 'number' | 'bigint'; @@ -509,7 +514,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin { if (_.isUndefined(prebuild.blockHeight)) { prebuild.blockHeight = (await this.getLatestBlockHeight()) as number; } - return _.extend({}, prebuild, { txHex: tx.toHex() }); + return _.extend({}, prebuild, { txHex: encodeTransaction(tx).toString('hex') }); } /** @@ -541,12 +546,12 @@ export abstract class AbstractUtxoCoin extends BaseCoin { } } - decodeTransactionAsPsbt(input: Buffer | string): utxolib.bitgo.UtxoPsbt { + decodeTransactionAsPsbt(input: Buffer | string): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt { const decoded = this.decodeTransaction(input); - if (!(decoded instanceof utxolib.bitgo.UtxoPsbt)) { - throw new Error('expected psbt but got transaction'); + if (decoded instanceof fixedScriptWallet.BitGoPsbt || decoded instanceof utxolib.bitgo.UtxoPsbt) { + return decoded; } - return decoded; + throw new Error('expected psbt but got transaction'); } decodeTransactionFromPrebuild(prebuild: { @@ -720,16 +725,29 @@ export abstract class AbstractUtxoCoin extends BaseCoin { /** * @returns input psbt added with deterministic MuSig2 nonce for bitgo key for each MuSig2 inputs. - * @param psbtHex all MuSig2 inputs should contain user MuSig2 nonce + * @param psbt all MuSig2 inputs should contain user MuSig2 nonce * @param walletId */ - async getMusig2Nonces(psbt: utxolib.bitgo.UtxoPsbt, walletId: string): Promise { - const params: SignPsbtRequest = { psbt: psbt.toHex() }; + async getMusig2Nonces(psbt: utxolib.bitgo.UtxoPsbt, walletId: string): Promise; + async getMusig2Nonces(psbt: fixedScriptWallet.BitGoPsbt, walletId: string): Promise; + async getMusig2Nonces( + psbt: T, + walletId: string + ): Promise; + async getMusig2Nonces( + psbt: T, + walletId: string + ): Promise { + const buffer = encodeTransaction(psbt); const response = await this.bitgo .post(this.url('/wallet/' + walletId + '/tx/signpsbt')) - .send(params) + .send({ psbt: buffer.toString('hex') }) .result(); - return this.decodeTransactionAsPsbt(response.psbt); + if (psbt instanceof utxolib.bitgo.UtxoPsbt) { + return decodePsbtWith(response.psbt, this.network, 'utxolib') as T; + } else { + return decodePsbtWith(response.psbt, this.network, 'wasm-utxo') as T; + } } /** @@ -739,7 +757,8 @@ export abstract class AbstractUtxoCoin extends BaseCoin { * @param walletId */ async signPsbt(psbtHex: string, walletId: string): Promise { - return { psbt: (await this.getMusig2Nonces(this.decodeTransactionAsPsbt(psbtHex), walletId)).toHex() }; + const psbt = await this.getMusig2Nonces(this.decodeTransactionAsPsbt(psbtHex), walletId); + return { psbt: encodeTransaction(psbt).toString('hex') }; } /** @@ -749,11 +768,10 @@ export abstract class AbstractUtxoCoin extends BaseCoin { async signPsbtFromOVC(ovcJson: Record): Promise> { assert(ovcJson['psbtHex'], 'ovcJson must contain psbtHex'); assert(ovcJson['walletId'], 'ovcJson must contain walletId'); - const psbt = await this.getMusig2Nonces( - this.decodeTransactionAsPsbt(ovcJson['psbtHex'] as string), - ovcJson['walletId'] as string - ); - return _.extend(ovcJson, { txHex: psbt.toHex() }); + const hex = ovcJson['psbtHex'] as string; + const walletId = ovcJson['walletId'] as string; + const psbt = await this.getMusig2Nonces(this.decodeTransactionAsPsbt(hex), walletId); + return _.extend(ovcJson, { txHex: encodeTransaction(psbt).toString('hex') }); } /** From c941b08afb3ade94164a51164f754bf3d45bcbc2 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 5 Dec 2025 09:56:55 +0100 Subject: [PATCH 3/6] feat(abstract-utxo): enable signing psbt with wasm implementation Update signTransaction to support wasm PSBT signing. Return Buffer instead of Uint8Array for extracted transactions. Fix type signatures and parameter structure in signPsbtWithMusig2ParticipantWasm to match other implementations. Issue: BTC-2806 Co-authored-by: llm-git --- .../transaction/fixedScript/signPsbtWasm.ts | 18 ++++----- .../fixedScript/signTransaction.ts | 40 +++++++++++++++---- .../src/transaction/signTransaction.ts | 4 +- .../unit/transaction/fixedScript/signPsbt.ts | 4 +- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signPsbtWasm.ts b/modules/abstract-utxo/src/transaction/fixedScript/signPsbtWasm.ts index 4d240e705f..cdcb35a4d9 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signPsbtWasm.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signPsbtWasm.ts @@ -20,7 +20,7 @@ const PSBT_CACHE_WASM = new Map(); function hasKeyPathSpendInput( tx: fixedScriptWallet.BitGoPsbt, - rootWalletKeys: fixedScriptWallet.IWalletKeys, + rootWalletKeys: fixedScriptWallet.RootWalletKeys, replayProtection: ReplayProtectionKeys ): boolean { const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, replayProtection); @@ -36,10 +36,10 @@ function hasKeyPathSpendInput( export function signAndVerifyPsbtWasm( tx: fixedScriptWallet.BitGoPsbt, signerKeychain: BIP32Interface, - rootWalletKeys: fixedScriptWallet.IWalletKeys, + rootWalletKeys: fixedScriptWallet.RootWalletKeys, replayProtection: ReplayProtectionKeys, { isLastSignature }: { isLastSignature: boolean } -): fixedScriptWallet.BitGoPsbt | Uint8Array { +): fixedScriptWallet.BitGoPsbt | Buffer { const wasmSigner = toWasmBIP32(signerKeychain); const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, replayProtection); @@ -85,7 +85,7 @@ export function signAndVerifyPsbtWasm( if (isLastSignature) { tx.finalizeAllInputs(); - return tx.extractTransaction(); + return Buffer.from(tx.extractTransaction()); } return tx; @@ -100,17 +100,17 @@ export async function signPsbtWithMusig2ParticipantWasm( coin: Musig2Participant, tx: fixedScriptWallet.BitGoPsbt, signerKeychain: BIP32Interface | undefined, - rootWalletKeys: fixedScriptWallet.IWalletKeys, - replayProtection: ReplayProtectionKeys, + rootWalletKeys: fixedScriptWallet.RootWalletKeys, params: { + replayProtection: ReplayProtectionKeys; isLastSignature: boolean; signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; walletId: string | undefined; } -): Promise { +): Promise { const wasmSigner = signerKeychain ? toWasmBIP32(signerKeychain) : undefined; - if (hasKeyPathSpendInput(tx, rootWalletKeys, replayProtection)) { + if (hasKeyPathSpendInput(tx, rootWalletKeys, params.replayProtection)) { // We can only be the first signature on a transaction with taproot key path spend inputs because // we require the secret nonce in the cache of the first signer, which is impossible to retrieve if // deserialized from a hex. @@ -162,7 +162,7 @@ export async function signPsbtWithMusig2ParticipantWasm( } assert(signerKeychain); - return signAndVerifyPsbtWasm(tx, signerKeychain, rootWalletKeys, replayProtection, { + return signAndVerifyPsbtWasm(tx, signerKeychain, rootWalletKeys, params.replayProtection, { isLastSignature: params.isLastSignature, }); } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts index 10c20561a0..773df4869c 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts @@ -1,17 +1,23 @@ +import assert from 'assert'; + +import { isTriple } from '@bitgo/sdk-core'; import _ from 'lodash'; import { BIP32Interface } from '@bitgo/secp256k1'; import { bitgo } from '@bitgo/utxo-lib'; import * as utxolib from '@bitgo/utxo-lib'; - -import { DecodedTransaction } from '../types'; +import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import { Musig2Participant } from './musig2'; import { signLegacyTransaction } from './signLegacyTransaction'; import { signPsbtWithMusig2Participant } from './signPsbt'; +import { signPsbtWithMusig2ParticipantWasm } from './signPsbtWasm'; +import { getReplayProtectionPubkeys } from './replayProtection'; -export async function signTransaction( - coin: Musig2Participant, - tx: DecodedTransaction, +export async function signTransaction< + T extends utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction | fixedScriptWallet.BitGoPsbt +>( + coin: Musig2Participant | Musig2Participant, + tx: T, signerKeychain: BIP32Interface | undefined, network: utxolib.Network, params: { @@ -24,7 +30,9 @@ export async function signTransaction( pubs: string[] | undefined; cosignerPub: string | undefined; } -): Promise> { +): Promise< + utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction | fixedScriptWallet.BitGoPsbt | Buffer +> { let isLastSignature = false; if (_.isBoolean(params.isLastSignature)) { // if build is called instead of buildIncomplete, no signature placeholders are left in the sig script @@ -32,11 +40,29 @@ export async function signTransaction( } if (tx instanceof bitgo.UtxoPsbt) { - return signPsbtWithMusig2Participant(coin, tx, signerKeychain, { + return signPsbtWithMusig2Participant(coin as Musig2Participant, tx, signerKeychain, { isLastSignature, signingStep: params.signingStep, walletId: params.walletId, }); + } else if (tx instanceof fixedScriptWallet.BitGoPsbt) { + assert(params.pubs, 'pubs are required for fixed script signing'); + assert(isTriple(params.pubs), 'pubs must be a triple'); + const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(params.pubs); + return signPsbtWithMusig2ParticipantWasm( + coin as Musig2Participant, + tx, + signerKeychain, + rootWalletKeys, + { + replayProtection: { + publicKeys: getReplayProtectionPubkeys(network), + }, + isLastSignature, + signingStep: params.signingStep, + walletId: params.walletId, + } + ); } return signLegacyTransaction(tx, signerKeychain, { diff --git a/modules/abstract-utxo/src/transaction/signTransaction.ts b/modules/abstract-utxo/src/transaction/signTransaction.ts index f99b4fcb1f..15c2d69949 100644 --- a/modules/abstract-utxo/src/transaction/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/signTransaction.ts @@ -10,6 +10,7 @@ import { fetchKeychains, toBip32Triple } from '../keychains'; import * as fixedScript from './fixedScript'; import * as descriptor from './descriptor'; +import { encodeTransaction } from './decode'; const debug = buildDebug('bitgo:abstract-utxo:transaction:signTransaction'); @@ -72,6 +73,7 @@ export async function signTransaction( pubs: params.pubs, cosignerPub: params.cosignerPub, }); - return { txHex: signedTx.toBuffer().toString('hex') }; + const buffer = Buffer.isBuffer(signedTx) ? signedTx : encodeTransaction(signedTx); + return { txHex: buffer.toString('hex') }; } } diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/signPsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/signPsbt.ts index 47c08d717f..00d1025517 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/signPsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/signPsbt.ts @@ -130,9 +130,9 @@ function describeSignPsbtWithMusig2Participant( getMockCoinWasm(acidTest.rootWalletKeys, acidTest.network), psbt, acidTest.rootWalletKeys.user, - wasmWalletKeys, - replayProtection, + fixedScriptWallet.RootWalletKeys.from(acidTest.rootWalletKeys), { + replayProtection, isLastSignature: false, signingStep: undefined, walletId: 'test-wallet-id', From cc67073e45a871769d54d4e1a349517b5df32137 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 5 Dec 2025 09:54:49 +0100 Subject: [PATCH 4/6] feat(abstract-utxo): extend DecodedTransaction to include wasm-utxo BitGoPsbt Issue: BTC-2806 Co-authored-by: llm-git --- modules/abstract-utxo/src/transaction/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/abstract-utxo/src/transaction/types.ts b/modules/abstract-utxo/src/transaction/types.ts index 06484aadbe..a74d1a6e5f 100644 --- a/modules/abstract-utxo/src/transaction/types.ts +++ b/modules/abstract-utxo/src/transaction/types.ts @@ -1,4 +1,5 @@ import * as utxolib from '@bitgo/utxo-lib'; +import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import type { UtxoNamedKeychains } from '../keychains'; @@ -8,7 +9,8 @@ export type SdkBackend = 'utxolib' | 'wasm-utxo'; export type DecodedTransaction = | utxolib.bitgo.UtxoTransaction - | utxolib.bitgo.UtxoPsbt; + | utxolib.bitgo.UtxoPsbt + | fixedScriptWallet.BitGoPsbt; export interface BaseOutput { address: string; From c78c2f210f3b78365a84b98f092a7346cc26fc50 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 5 Dec 2025 10:06:34 +0100 Subject: [PATCH 5/6] feat(abstract-utxo): add support for PSBT decoder selection Add decodeWith parameter to specify which PSBT decoder to use (utxolib or wasm-utxo). This allows switching between implementations when needed, which is important for compatibility and testing. Also add a helper function to validate decoder selection. Issue: BTC-2806 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 27 +++++++++++++++---- .../abstract-utxo/src/transaction/types.ts | 4 +++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 882ce5a82a..ff0227e422 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -78,7 +78,7 @@ import { import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor'; import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names'; import { assertFixedScriptWalletAddress } from './address/fixedScript'; -import { ParsedTransaction } from './transaction/types'; +import { isSdkBackend, ParsedTransaction, SdkBackend } from './transaction/types'; import { decodePsbtWith, encodeTransaction, stringToBufferTryFormats } from './transaction/decode'; import { toBip32Triple, UtxoKeychain } from './keychains'; import { verifyKeySignature, verifyUserPublicKey } from './verifyKey'; @@ -215,6 +215,7 @@ export interface ExplainTransactionOptions; feeInfo?: string; pubs?: Triple; + decodeWith?: SdkBackend; } export interface DecoratedExplainTransactionOptions @@ -227,6 +228,7 @@ export type UtxoNetwork = utxolib.Network; export interface TransactionPrebuild extends BaseTransactionPrebuild { txInfo?: TransactionInfo; blockHeight?: number; + decodeWith?: SdkBackend; } export interface TransactionParams extends BaseTransactionParams { @@ -286,6 +288,7 @@ type UtxoBaseSignTransactionOptions = walletId?: string; txHex: string; txInfo?: TransactionInfo; + decodeWith?: SdkBackend; }; /** xpubs triple for wallet (user, backup, bitgo). Required only when txPrebuild.txHex is not a PSBT */ pubs?: Triple; @@ -531,15 +534,21 @@ export abstract class AbstractUtxoCoin return utxolib.bitgo.createTransactionFromHex(hex, this.network, this.amountType); } - decodeTransaction(input: Buffer | string): DecodedTransaction { + decodeTransaction( + input: Buffer | string, + decodeWith?: SdkBackend + ): DecodedTransaction { if (typeof input === 'string') { const buffer = stringToBufferTryFormats(input, ['hex', 'base64']); - return this.decodeTransaction(buffer); + return this.decodeTransaction(buffer, decodeWith); } if (utxolib.bitgo.isPsbt(input)) { - return decodePsbtWith(input, this.network, 'utxolib'); + return decodePsbtWith(input, this.network, decodeWith ?? 'utxolib'); } else { + if (decodeWith ?? 'utxolib' !== 'utxolib') { + console.error('received decodeWith hint %s, ignoring for legacy transaction', decodeWith); + } return utxolib.bitgo.createTransactionFromBuffer(input, this.network, { amountType: this.amountType, }); @@ -557,12 +566,20 @@ export abstract class AbstractUtxoCoin decodeTransactionFromPrebuild(prebuild: { txHex?: string; txBase64?: string; + decodeWith?: string; }): DecodedTransaction { const string = prebuild.txHex ?? prebuild.txBase64; if (!string) { throw new Error('missing required txHex or txBase64 property'); } - return this.decodeTransaction(string); + let { decodeWith } = prebuild; + if (decodeWith !== undefined) { + if (typeof decodeWith !== 'string' || !isSdkBackend(decodeWith)) { + console.error('decodeWith %s is not a valid value, using default', decodeWith); + decodeWith = undefined; + } + } + return this.decodeTransaction(string, decodeWith); } toCanonicalTransactionRecipient(output: { valueString: string; address?: string }): { diff --git a/modules/abstract-utxo/src/transaction/types.ts b/modules/abstract-utxo/src/transaction/types.ts index a74d1a6e5f..635f226506 100644 --- a/modules/abstract-utxo/src/transaction/types.ts +++ b/modules/abstract-utxo/src/transaction/types.ts @@ -7,6 +7,10 @@ import type { CustomChangeOptions } from './fixedScript'; export type SdkBackend = 'utxolib' | 'wasm-utxo'; +export function isSdkBackend(backend: string): backend is SdkBackend { + return backend === 'utxolib' || backend === 'wasm-utxo'; +} + export type DecodedTransaction = | utxolib.bitgo.UtxoTransaction | utxolib.bitgo.UtxoPsbt From 0f5c86c4f00a37fe8d850ee4450f1ffa008390d4 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 5 Dec 2025 13:04:18 +0100 Subject: [PATCH 6/6] feat(abstract-utxo): add wasm-utxo decoding support to test suite Add test scenarios that verify wasm-utxo as a transaction decoding backend. Rework test suite structure for better readability and clearer test naming. Skip tests for networks not supported by wasm-utxo. Issue: BTC-2806 Co-authored-by: llm-git --- .../abstract-utxo/test/unit/transaction.ts | 91 +++++++++++-------- .../test/unit/transaction/fixedScript/util.ts | 1 + 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/modules/abstract-utxo/test/unit/transaction.ts b/modules/abstract-utxo/test/unit/transaction.ts index b5ee481aa0..6d99bd60b9 100644 --- a/modules/abstract-utxo/test/unit/transaction.ts +++ b/modules/abstract-utxo/test/unit/transaction.ts @@ -14,7 +14,9 @@ import { } from '@bitgo/sdk-core'; import { AbstractUtxoCoin, getReplayProtectionAddresses, generateAddress } from '../../src'; +import { SdkBackend } from '../../src/transaction/types'; +import { hasWasmUtxoSupport } from './transaction/fixedScript/util'; import { utxoCoins, shouldEqualJSON, @@ -271,11 +273,16 @@ function run( coin: AbstractUtxoCoin, inputScripts: testutil.InputScriptType[], txFormat: 'legacy' | 'psbt', - amountType: 'number' | 'bigint' = 'number' + { decodeWith }: { decodeWith?: SdkBackend } = {} ) { - describe(`Transaction Stages ${coin.getChain()} (${amountType}) scripts=${inputScripts.join( - ',' - )} txFormat=${txFormat}`, function () { + const amountType = coin.amountType; + const title = [ + inputScripts.join(','), + `txFormat=${txFormat}`, + `amountType=${amountType}`, + decodeWith ? `decodeWith=${decodeWith}` : '', + ]; + describe(`${title.join(' ')}`, function () { const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; const isTransactionWithKeyPathSpend = inputScripts.some((s) => s === 'taprootKeyPathSpend'); @@ -319,7 +326,8 @@ function run( function getSignParams( prebuildHex: string, signer: BIP32Interface, - cosigner: BIP32Interface + cosigner: BIP32Interface, + decodeWith: SdkBackend | undefined ): WalletSignTransactionOptions { const txInfo = { unspents: txFormat === 'psbt' ? undefined : getUnspents(), @@ -329,6 +337,7 @@ function run( walletId: isTransactionWithKeyPathSpend ? wallet.id() : undefined, txHex: prebuildHex, txInfo, + decodeWith, }, prv: signer.toBase58(), pubs: walletKeys.triple.map((k) => k.neutered().toBase58()), @@ -351,7 +360,7 @@ function run( // half-sign with the user key const result = (await wallet.signTransaction( - getSignParams(prebuild.toBuffer().toString('hex'), signer, cosigner) + getSignParams(prebuild.toBuffer().toString('hex'), signer, cosigner, decodeWith) )) as Promise; if (scope) { @@ -367,7 +376,7 @@ function run( cosigner: BIP32Interface ): Promise { return (await wallet.signTransaction({ - ...getSignParams(halfSigned.txHex, signer, cosigner), + ...getSignParams(halfSigned.txHex, signer, cosigner, decodeWith), isLastSignature: true, })) as FullySignedTransaction; } @@ -533,8 +542,8 @@ function run( async function testExplainTx( stageName: string, txHex: string, - unspents?: utxolib.bitgo.Unspent[], - pubs?: Triple + unspents: utxolib.bitgo.Unspent[], + pubs: Triple | undefined ): Promise { const explanation = await coin.explainTransaction({ txHex, @@ -542,18 +551,16 @@ function run( unspents, }, pubs, + decodeWith, }); - explanation.should.have.properties( - 'displayOrder', - 'id', - 'outputs', - 'changeOutputs', - 'changeAmount', - 'outputAmount', - 'inputSignatures', - 'signatures' - ); + const expectedProperties = ['id', 'outputs', 'changeOutputs', 'changeAmount', 'outputAmount']; + + if (decodeWith !== 'wasm-utxo') { + expectedProperties.push('displayOrder', 'inputSignatures', 'signatures'); + } + + explanation.should.have.properties(...expectedProperties); const expectedSignatureCount = stageName === 'prebuild' || pubs === undefined @@ -623,38 +630,44 @@ function run( ? getUnspentsForPsbt().map((u) => ({ ...u, value: bitgo.toTNumber(u.value, amountType) as TNumber })) : getUnspents(); await testExplainTx(stageName, txHex, unspents, pubs); - await testExplainTx(stageName, txHex, unspents); + if (decodeWith !== 'wasm-utxo') { + await testExplainTx(stageName, txHex, unspents, undefined); + } } }); }); } -function runWithAmountType( - coin: AbstractUtxoCoin, - inputScripts: testutil.InputScriptType[], - txFormat: 'legacy' | 'psbt' -) { - const amountType = coin.amountType; - if (amountType === 'bigint') { - run(coin, inputScripts, txFormat, amountType); - } else { - run(coin, inputScripts, txFormat, amountType); - } -} - -utxoCoins.forEach((coin) => +function runTestForCoin(coin: AbstractUtxoCoin) { getScriptTypes2Of3().forEach((type) => { (['legacy', 'psbt'] as const).forEach((txFormat) => { + if (!coin.supportsAddressType(type === 'taprootKeyPathSpend' ? 'p2trMusig2' : type)) { + return; + } + if ((type === 'taprootKeyPathSpend' || type === 'p2trMusig2') && txFormat !== 'psbt') { return; } - if (coin.supportsAddressType(type === 'taprootKeyPathSpend' ? 'p2trMusig2' : type)) { - runWithAmountType(coin, [type, type], txFormat); + run(coin, [type, type], txFormat); + if (getReplayProtectionAddresses(coin.network).length) { + run(coin, ['p2shP2pk', type], txFormat); + } + + if (txFormat === 'psbt' && hasWasmUtxoSupport(coin.network)) { + run(coin, [type, type], txFormat, { decodeWith: 'wasm-utxo' }); if (getReplayProtectionAddresses(coin.network).length) { - runWithAmountType(coin, ['p2shP2pk', type], txFormat); + run(coin, ['p2shP2pk', type], txFormat, { decodeWith: 'wasm-utxo' }); } } }); - }) -); + }); +} + +describe('Transaction Suite', function () { + utxoCoins.forEach((coin) => { + describe(`${coin.getChain()}`, function () { + runTestForCoin(coin); + }); + }); +}); diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/util.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/util.ts index 3b62f89179..813fe24d9e 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/util.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/util.ts @@ -4,6 +4,7 @@ export function hasWasmUtxoSupport(network: utxolib.Network): boolean { return ![ utxolib.networks.bitcoincash, utxolib.networks.bitcoingold, + utxolib.networks.bitcoinsv, utxolib.networks.ecash, utxolib.networks.zcash, ].includes(utxolib.getMainnet(network));