diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 8c84f8db46..e6392b5dc2 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -2,9 +2,7 @@ import assert from 'assert'; import { randomBytes } from 'crypto'; import _ from 'lodash'; -import * as utxolib from '@bitgo/utxo-lib'; -import { BIP32, fixedScriptWallet } from '@bitgo/wasm-utxo'; -import { bitgo, getMainnet } from '@bitgo/utxo-lib'; +import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AddressCoinSpecific, BaseCoin, @@ -61,7 +59,6 @@ import { getReplayProtectionPubkeys, isReplayProtectionUnspent } from './transac import { supportedCrossChainRecoveries } from './config'; import { assertValidTransactionRecipient, - DecodedTransaction, explainTx, fromExtendedAddressFormat, isScriptRecipient, @@ -76,22 +73,10 @@ import { ErrorImplicitExternalOutputs, } from './transaction/descriptor/verifyTransaction'; import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor'; -import { - getFullNameFromCoinName, - getMainnetCoinName, - getNetworkFromCoinName, - isMainnetCoin, - UtxoCoinName, - UtxoCoinNameMainnet, -} from './names'; +import { getFullNameFromCoinName, getMainnetCoinName, isMainnetCoin, UtxoCoinName, UtxoCoinNameMainnet } from './names'; import { assertFixedScriptWalletAddress } from './address/fixedScript'; -import { isSdkBackend, ParsedTransaction, SdkBackend } from './transaction/types'; -import { - decodeDescriptorPsbt, - decodePsbtWith, - encodeTransaction, - stringToBufferTryFormats, -} from './transaction/decode'; +import { ParsedTransaction } from './transaction/types'; +import { decodeDescriptorPsbt, decodePsbt, encodeTransaction, stringToBufferTryFormats } from './transaction/decode'; import { fetchKeychains, toBip32Triple, UtxoKeychain } from './keychains'; import { verifyKeySignature, verifyUserPublicKey } from './verifyKey'; import { getPolicyForEnv } from './descriptor/validatePolicy'; @@ -100,7 +85,7 @@ import { isUtxoWalletData, UtxoWallet } from './wallet'; import { isDescriptorWalletData } from './descriptor/descriptorWallet'; import type { Unspent } from './unspent'; -import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; +type ScriptType2Of3 = 'p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2'; export type TxFormat = // This is a legacy transaction format based around the bitcoinjs-lib serialization of unsigned transactions @@ -146,28 +131,19 @@ type UtxoCustomSigningFunction = { }): Promise; }; -const { isChainCode, scriptTypeForChain, outputScripts } = bitgo; - /** * Check if a decoded transaction has at least one taproot key path spend (MuSig2) input. - * Works for both utxolib UtxoPsbt and wasm-utxo BitGoPsbt. */ -function hasKeyPathSpendInput( - tx: DecodedTransaction, +function hasKeyPathSpendInput( + tx: fixedScriptWallet.BitGoPsbt, pubs: string[] | undefined, coinName: UtxoCoinName ): boolean { - if (tx instanceof bitgo.UtxoPsbt) { - return bitgo.isTransactionWithKeyPathSpendInput(tx); - } - if (tx instanceof fixedScriptWallet.BitGoPsbt) { - assert(pubs && isTriple(pubs), 'pub triple is required to check for key path spend inputs in wasm-utxo PSBT'); - const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(pubs); - const replayProtection = { publicKeys: getReplayProtectionPubkeys(coinName) }; - const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection }); - return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath'); - } - return false; + assert(pubs && isTriple(pubs), 'pub triple is required to check for key path spend inputs in wasm-utxo PSBT'); + const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(pubs); + const replayProtection = { publicKeys: getReplayProtectionPubkeys(coinName) }; + const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection }); + return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath'); } /** @@ -220,8 +196,6 @@ function convertValidationErrorToTxIntentMismatch( export type { DecodedTransaction } from './transaction/types'; -export type RootWalletKeys = bitgo.RootWalletKeys; - export type UtxoCoinSpecific = AddressCoinSpecific | DescriptorAddressCoinSpecific; export interface VerifyAddressOptions extends BaseVerifyAddressOptions { @@ -249,7 +223,6 @@ export interface ExplainTransactionOptions; customChangeXpubs?: Triple; - decodeWith?: SdkBackend; } export interface DecoratedExplainTransactionOptions @@ -257,12 +230,9 @@ export interface DecoratedExplainTransactionOptions extends BaseTransactionPrebuild { txInfo?: TransactionInfo; blockHeight?: number; - decodeWith?: SdkBackend; /** * PSBT-lite hex present only in pending approval flows, where another user's send fixed the format. * Not set in regular /tx/build responses (where the caller controls the build parameters). @@ -328,7 +298,6 @@ 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; @@ -339,11 +308,6 @@ type UtxoBaseSignTransactionOptions = * When false, creates half-signed transaction with placeholder signatures. */ isLastSignature?: boolean; - /** - * If true, allows signing a non-segwit input with a witnessUtxo instead requiring a previous - * transaction (nonWitnessUtxo) - */ - allowNonSegwitSigningWithoutPrevTx?: boolean; /** * When true, the signed transaction will be converted from PSBT to legacy format before returning. * Set automatically by presignTransaction() when the caller explicitly requested txFormat: 'legacy'. @@ -417,10 +381,7 @@ export interface SignPsbtResponse { psbt: string; } -export abstract class AbstractUtxoCoin - extends BaseCoin - implements Musig2Participant, Musig2Participant -{ +export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Participant { abstract name: UtxoCoinName; /** @@ -434,31 +395,11 @@ export abstract class AbstractUtxoCoin public readonly amountType: 'number' | 'bigint'; - protected supportedTxFormats: { psbt: boolean; legacy: boolean } = { - psbt: true, - legacy: this.getChain() === 'btc', - }; - - protected supportedSdkBackends: { utxolib: boolean; 'wasm-utxo': boolean } = { - utxolib: this.getChain() === 'btc', - 'wasm-utxo': true, - }; - protected constructor(bitgo: BitGoBase, amountType: 'number' | 'bigint' = 'number') { super(bitgo); this.amountType = amountType; } - defaultSdkBackend: SdkBackend = 'wasm-utxo'; - - /** - * @deprecated - will be removed when we drop support for utxolib - * Use `name` property instead. - */ - get network(): utxolib.Network { - return getNetworkFromCoinName(this.name); - } - getChain(): UtxoCoinName { return this.name; } @@ -474,13 +415,8 @@ export abstract class AbstractUtxoCoin /** Indicates whether the coin supports a block target */ supportsBlockTarget(): boolean { // FIXME: the SDK does not seem to use this anywhere so it is unclear what the purpose of this method is - switch (getMainnet(this.network)) { - case utxolib.networks.bitcoin: - case utxolib.networks.dogecoin: - return true; - default: - return false; - } + const mainnet = getMainnetCoinName(this.name); + return mainnet === 'btc' || mainnet === 'doge'; } sweepWithSendMany(): boolean { @@ -489,7 +425,7 @@ export abstract class AbstractUtxoCoin /** @deprecated */ static get validAddressTypes(): ScriptType2Of3[] { - return [...outputScripts.scriptTypes2Of3]; + return ['p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2']; } /** @@ -527,15 +463,20 @@ export abstract class AbstractUtxoCoin // At the time of writing, the only additional address format is bch cashaddr. const anyFormat = (param as { anyFormat: boolean } | undefined)?.anyFormat ?? true; try { - // Find out if the address is valid for any format. Tries all supported formats by default. - // Throws if address cannot be decoded with any format. - const [format, script] = utxolib.addressFormat.toOutputScriptAndFormat(address, this.network); - // unless anyFormat is set, only 'default' is allowed. - if (!anyFormat && format !== 'default') { - return false; + const script = wasmAddress.toOutputScriptWithCoin(address, this.name); + // Determine which format the input address was in by round-tripping + // through each candidate and checking byte-equality. 'default' is tried + // first so canonical default-format addresses early-exit. + for (const format of ['default', 'cashaddr'] as const) { + try { + if (wasmAddress.fromOutputScriptWithCoin(script, this.name, format) === address) { + return anyFormat || format === 'default'; + } + } catch { + // coin doesn't support this format; try the next one + } } - // make sure that address is in normal representation for given format. - return address === utxolib.addressFormat.fromOutputScriptWithFormat(script, format, this.network); + return false; } catch (e) { return false; } @@ -615,74 +556,34 @@ export abstract class AbstractUtxoCoin * @param addressDetails */ static inferAddressType(addressDetails: { chain: number }): ScriptType2Of3 | null { - return isChainCode(addressDetails.chain) ? scriptTypeForChain(addressDetails.chain) : null; + return fixedScriptWallet.ChainCode.is(addressDetails.chain) + ? (fixedScriptWallet.ChainCode.scriptType(addressDetails.chain) as ScriptType2Of3) + : null; } - createTransactionFromHex( - hex: string - ): utxolib.bitgo.UtxoTransaction { - return utxolib.bitgo.createTransactionFromHex(hex, this.network, this.amountType); - } - - decodeTransaction( - input: Buffer | string, - decodeWith: SdkBackend = this.defaultSdkBackend - ): DecodedTransaction { - if (typeof input === 'string') { - const buffer = stringToBufferTryFormats(input, ['hex', 'base64']); - return this.decodeTransaction(buffer, decodeWith); - } - - if (utxolib.bitgo.isPsbt(input)) { - if (this.supportedSdkBackends[decodeWith] !== true) { - throw new Error(`SDK support for decodeWith=${decodeWith} is not available on this environment.`); - } - - if (!this.supportedTxFormats.psbt) { - throw new ErrorDeprecatedTxFormat('psbt'); - } - return decodePsbtWith(input, this.name, decodeWith); - } else { - // Legacy format transactions are deprecated. This will be an unconditional error in the future. - if (!this.supportedTxFormats.legacy) { - throw new ErrorDeprecatedTxFormat('legacy'); - } - if (decodeWith !== 'utxolib') { - console.error('received decodeWith hint %s, ignoring for legacy transaction', decodeWith); - } - return utxolib.bitgo.createTransactionFromBuffer(input, this.network, { - amountType: this.amountType, - }); + decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt { + const buffer = typeof input === 'string' ? stringToBufferTryFormats(input, ['hex', 'base64']) : input; + if (!hasPsbtMagic(buffer)) { + throw new ErrorDeprecatedTxFormat('legacy'); } + return decodePsbt(buffer, this.name); } - decodeTransactionAsPsbt(input: Buffer | string): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt { - const decoded = this.decodeTransaction(input); - if (decoded instanceof fixedScriptWallet.BitGoPsbt || decoded instanceof utxolib.bitgo.UtxoPsbt) { - return decoded; - } - throw new Error('expected psbt but got transaction'); + decodeTransactionAsPsbt(input: Buffer | string): fixedScriptWallet.BitGoPsbt { + return this.decodeTransaction(input); } - decodeTransactionFromPrebuild(prebuild: { + decodeTransactionFromPrebuild(prebuild: { txHex?: string; txBase64?: string; /** See TransactionPrebuild.txHexPsbt — only present in pending approval flows. */ txHexPsbt?: string; - decodeWith?: string; - }): DecodedTransaction { + }): fixedScriptWallet.BitGoPsbt { const string = prebuild.txHexPsbt ?? prebuild.txHex ?? prebuild.txBase64; if (!string) { throw new Error('missing required txHex or txBase64 property'); } - 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); + return this.decodeTransaction(string); } /** @@ -809,7 +710,7 @@ export abstract class AbstractUtxoCoin * @returns true iff coin supports spending from unspentType */ supportsAddressType(addressType: ScriptType2Of3): boolean { - return utxolib.bitgo.outputScripts.isSupportedScriptType(this.network, addressType); + return fixedScriptWallet.supportsScriptType(this.name, addressType); } /** inherited doc */ @@ -822,7 +723,10 @@ export abstract class AbstractUtxoCoin * @return true iff coin supports spending from chain */ supportsAddressChain(chain: number): boolean { - return isChainCode(chain) && this.supportsAddressType(utxolib.bitgo.scriptTypeForChain(chain)); + return ( + fixedScriptWallet.ChainCode.is(chain) && + this.supportsAddressType(fixedScriptWallet.ChainCode.scriptType(chain) as ScriptType2Of3) + ); } keyIdsForSigning(): number[] { @@ -834,26 +738,13 @@ export abstract class AbstractUtxoCoin * @param psbt all MuSig2 inputs should contain user MuSig2 nonce * @param walletId */ - 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 { + async getMusig2Nonces(psbt: fixedScriptWallet.BitGoPsbt, walletId: string): Promise { const buffer = encodeTransaction(psbt); const response = await this.bitgo .post(this.url('/wallet/' + walletId + '/tx/signpsbt')) .send({ psbt: buffer.toString('hex') }) .result(); - if (psbt instanceof utxolib.bitgo.UtxoPsbt) { - return decodePsbtWith(response.psbt, this.name, 'utxolib') as T; - } else { - return decodePsbtWith(response.psbt, this.name, 'wasm-utxo') as T; - } + return decodePsbt(response.psbt, this.name); } /** @@ -1124,17 +1015,6 @@ export abstract class AbstractUtxoCoin } const returnLegacyFormat = (params as Record).txFormat === 'legacy'; - - // In the case that we have a 'psbt-lite' transaction format, we want to indicate in signing to not fail - const txHex = (params.txHex ?? params.txPrebuild?.txHex) as string; - if ( - txHex && - utxolib.bitgo.isPsbt(txHex as string) && - utxolib.bitgo.isPsbtLite(utxolib.bitgo.createPsbtFromHex(txHex, this.network)) && - params.allowNonSegwitSigningWithoutPrevTx === undefined - ) { - return { ...params, allowNonSegwitSigningWithoutPrevTx: true, returnLegacyFormat }; - } return { ...params, returnLegacyFormat }; } diff --git a/modules/abstract-utxo/src/impl/doge/doge.ts b/modules/abstract-utxo/src/impl/doge/doge.ts index 79fe2da15c..f1ba54f030 100644 --- a/modules/abstract-utxo/src/impl/doge/doge.ts +++ b/modules/abstract-utxo/src/impl/doge/doge.ts @@ -1,5 +1,4 @@ import { BitGoBase, HalfSignedUtxoTransaction, SignedTransaction } from '@bitgo/sdk-core'; -import { bitgo } from '@bitgo/utxo-lib'; import { AbstractUtxoCoin, @@ -75,10 +74,6 @@ export class Doge extends AbstractUtxoCoin { /* postProcessPrebuild, isBitGoTaintedUnspent, verifyCustomChangeKeySignatures do not care whether they receive number or bigint */ - createTransactionFromHex(hex: string): bitgo.UtxoTransaction { - return super.createTransactionFromHex(hex); - } - async parseTransaction( params: ParseTransactionOptions ): /* diff --git a/modules/abstract-utxo/src/index.ts b/modules/abstract-utxo/src/index.ts index 7957ac614d..1118441b2a 100644 --- a/modules/abstract-utxo/src/index.ts +++ b/modules/abstract-utxo/src/index.ts @@ -4,7 +4,6 @@ export * from './config'; export * from './names'; export * from './recovery'; export * from './transaction/fixedScript/replayProtection'; -export * from './transaction/fixedScript/signLegacyTransaction'; export * from './unspent'; export { UtxoWallet } from './wallet'; diff --git a/modules/abstract-utxo/src/names.ts b/modules/abstract-utxo/src/names.ts index 555797ae3c..9e1a5c74c2 100644 --- a/modules/abstract-utxo/src/names.ts +++ b/modules/abstract-utxo/src/names.ts @@ -1,5 +1,3 @@ -import * as utxolib from '@bitgo/utxo-lib'; - export const utxoCoinsMainnet = ['btc', 'bch', 'bcha', 'bsv', 'btg', 'dash', 'doge', 'ltc', 'zec'] as const; export const utxoCoinsTestnet = [ 'tbtc', @@ -46,107 +44,6 @@ export function getMainnetCoinName(coinName: UtxoCoinName): UtxoCoinNameMainnet } } -function getNetworkName(n: utxolib.Network): utxolib.NetworkName { - const name = utxolib.getNetworkName(n); - if (!name) { - throw new Error('Unknown network'); - } - return name; -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * @param n - * @returns the family name for a network. Testnets and mainnets of the same coin share the same family name. - */ -export function getFamilyFromNetwork(n: utxolib.Network): UtxoCoinNameMainnet { - switch (getNetworkName(n)) { - case 'bitcoin': - case 'testnet': - case 'bitcoinPublicSignet': - case 'bitcoinTestnet4': - case 'bitcoinBitGoSignet': - return 'btc'; - case 'bitcoincash': - case 'bitcoincashTestnet': - return 'bch'; - case 'ecash': - case 'ecashTest': - return 'bcha'; - case 'bitcoingold': - case 'bitcoingoldTestnet': - return 'btg'; - case 'bitcoinsv': - case 'bitcoinsvTestnet': - return 'bsv'; - case 'dash': - case 'dashTest': - return 'dash'; - case 'dogecoin': - case 'dogecoinTest': - return 'doge'; - case 'litecoin': - case 'litecoinTest': - return 'ltc'; - case 'zcash': - case 'zcashTest': - return 'zec'; - } -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * Get the chain name for a network. - * The chain is different for every network. - */ -export function getCoinName(n: utxolib.Network): UtxoCoinName { - switch (getNetworkName(n)) { - case 'bitcoinPublicSignet': - return 'tbtcsig'; - case 'bitcoinTestnet4': - return 'tbtc4'; - case 'bitcoinBitGoSignet': - return 'tbtcbgsig'; - case 'bitcoin': - case 'testnet': - case 'bitcoincash': - case 'bitcoincashTestnet': - case 'ecash': - case 'ecashTest': - case 'bitcoingold': - case 'bitcoingoldTestnet': - case 'bitcoinsv': - case 'bitcoinsvTestnet': - case 'dash': - case 'dashTest': - case 'dogecoin': - case 'dogecoinTest': - case 'litecoin': - case 'litecoinTest': - case 'zcash': - case 'zcashTest': - const mainnetName = getFamilyFromNetwork(n); - return utxolib.isTestnet(n) ? `t${mainnetName}` : mainnetName; - } -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * @param coinName - the name of the coin (e.g. 'btc', 'bch', 'ltc'). Also called 'chain' in some contexts. - * @returns the network for a coin. This is the mainnet network for the coin. - */ -export function getNetworkFromCoinName(coinName: string): utxolib.Network { - for (const network of utxolib.getNetworkList()) { - if (getCoinName(network) === coinName) { - return network; - } - } - throw new Error(`Unknown coin name ${coinName}`); -} - -/** @deprecated - use getNetworkFromCoinName instead */ -export const getNetworkFromChain = getNetworkFromCoinName; - function getBaseNameFromMainnet(coinName: UtxoCoinNameMainnet): string { switch (coinName) { case 'btc': @@ -189,11 +86,6 @@ export function getFullNameFromCoinName(coinName: UtxoCoinName): string { return prefix + getBaseNameFromMainnet(getMainnetCoinName(coinName)); } -/** @deprecated - use getFullNameFromCoinName instead */ -export function getFullNameFromNetwork(n: utxolib.Network): string { - return getFullNameFromCoinName(getCoinName(n)); -} - export function isTestnetCoin(coinName: UtxoCoinName): boolean { return isUtxoCoinNameTestnet(coinName); } diff --git a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts index 8c902d1401..68d3f4b9c1 100644 --- a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts @@ -8,7 +8,7 @@ import { krsProviders, Triple, } from '@bitgo/sdk-core'; -import { BIP32, fixedScriptWallet } from '@bitgo/wasm-utxo'; +import { BIP32, fixedScriptWallet, Transaction } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin } from '../abstractUtxoCoin'; import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction'; @@ -172,8 +172,8 @@ async function queryBlockchainUnspentsPath( // json parse won't parse it correctly, so we requery the txid for the tx hex to decode here if (!Number.isSafeInteger(u.value)) { const txHex = await getPrevTx(txid); - const tx = coin.createTransactionFromHex(txHex); - val = tx.outs[vout].value; + const tx = Transaction.fromBytes(Buffer.from(txHex, 'hex')); + val = tx.getOutputs()[vout].value; } } // the api may return cashaddr's instead of legacy for BCH and BCHA diff --git a/modules/abstract-utxo/src/transaction/decode.ts b/modules/abstract-utxo/src/transaction/decode.ts index 77d461f6ac..35f4337582 100644 --- a/modules/abstract-utxo/src/transaction/decode.ts +++ b/modules/abstract-utxo/src/transaction/decode.ts @@ -1,9 +1,8 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt, utxolibCompat } from '@bitgo/wasm-utxo'; +import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt } from '@bitgo/wasm-utxo'; -import { getNetworkFromCoinName, UtxoCoinName } from '../names'; +import { UtxoCoinName } from '../names'; -import { SdkBackend, BitGoPsbt } from './types'; +import { BitGoPsbt } from './types'; type BufferEncoding = 'hex' | 'base64'; @@ -22,48 +21,11 @@ export function stringToBufferTryFormats(input: string, formats: BufferEncoding[ throw new Error('input must be a valid hex or base64 string'); } -function toNetworkName(coinName: UtxoCoinName): utxolibCompat.UtxolibName { - const network = getNetworkFromCoinName(coinName); - const networkName = utxolib.getNetworkName(network); - if (!networkName) { - throw new Error(`Invalid coinName: ${coinName}`); - } - return networkName; -} - -export function decodePsbtWith( - psbt: string | Buffer, - coinName: UtxoCoinName, - backend: 'utxolib' -): utxolib.bitgo.UtxoPsbt; -export function decodePsbtWith( - psbt: string | Buffer, - coinName: UtxoCoinName, - backend: 'wasm-utxo' -): fixedScriptWallet.BitGoPsbt; -export function decodePsbtWith( - psbt: string | Buffer, - coinName: UtxoCoinName, - backend: SdkBackend -): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt; -export function decodePsbtWith( - psbt: string | Buffer, - coinName: UtxoCoinName, - backend: SdkBackend -): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt { +export function decodePsbt(psbt: string | Buffer, coinName: UtxoCoinName): BitGoPsbt { if (typeof psbt === 'string') { psbt = Buffer.from(psbt, 'hex'); } - if (backend === 'utxolib') { - const network = getNetworkFromCoinName(coinName); - return utxolib.bitgo.createPsbtFromBuffer(psbt, network); - } else { - return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, toNetworkName(coinName)); - } -} - -export function decodePsbt(psbt: string | Buffer, coinName: UtxoCoinName): BitGoPsbt { - return decodePsbtWith(psbt, coinName, 'wasm-utxo'); + return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, coinName); } export type PrebuildLike = { @@ -90,14 +52,6 @@ export function decodeDescriptorPsbt(prebuild: PrebuildLike): WasmPsbt { return WasmPsbt.deserialize(bytes); } -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()); - } +export function encodeTransaction(transaction: fixedScriptWallet.BitGoPsbt): Buffer { + return Buffer.from(transaction.serialize()); } diff --git a/modules/abstract-utxo/src/transaction/explainTransaction.ts b/modules/abstract-utxo/src/transaction/explainTransaction.ts index 45c90da5e9..9da1ecc890 100644 --- a/modules/abstract-utxo/src/transaction/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/explainTransaction.ts @@ -1,4 +1,3 @@ -import * as utxolib from '@bitgo/utxo-lib'; import { fixedScriptWallet, Psbt as WasmPsbt } from '@bitgo/wasm-utxo'; import { isTriple, IWallet, Triple } from '@bitgo/sdk-core'; @@ -9,11 +8,7 @@ import { UtxoCoinName } from '../names'; import type { Unspent } from '../unspent'; import { getReplayProtectionPubkeys } from './fixedScript/replayProtection'; -import type { - TransactionExplanationUtxolibLegacy, - TransactionExplanationUtxolibPsbt, - TransactionExplanationWasm, -} from './fixedScript/explainTransaction'; +import type { TransactionExplanationUtxolibPsbt, TransactionExplanationWasm } from './fixedScript/explainTransaction'; import * as fixedScript from './fixedScript'; import * as descriptor from './descriptor'; @@ -22,16 +17,15 @@ import * as descriptor from './descriptor'; * change amounts, and transaction outputs. */ export function explainTx( - tx: utxolib.bitgo.UtxoTransaction | utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt | WasmPsbt, + tx: fixedScriptWallet.BitGoPsbt | WasmPsbt, params: { wallet?: IWallet; pubs?: string[]; customChangeXpubs?: Triple; txInfo?: { unspents?: Unspent[] }; - changeInfo?: fixedScript.ChangeAddressInfo[]; }, coinName: UtxoCoinName -): TransactionExplanationUtxolibLegacy | TransactionExplanationUtxolibPsbt | TransactionExplanationWasm { +): TransactionExplanationUtxolibPsbt | TransactionExplanationWasm { if (params.wallet && isDescriptorWallet(params.wallet)) { if (!(tx instanceof WasmPsbt)) { throw new Error('descriptor wallets require PSBT format transactions'); @@ -43,19 +37,12 @@ export function explainTx( const descriptors = getDescriptorMapFromWallet(params.wallet, walletKeys, getPolicyForEnv(params.wallet.bitgo.env)); return descriptor.explainPsbt(tx, descriptors, coinName); } - if (tx instanceof utxolib.bitgo.UtxoPsbt) { - return fixedScript.explainPsbt(tx, { ...params, customChangePubs: params.customChangeXpubs }, coinName); - } else if (tx instanceof fixedScriptWallet.BitGoPsbt) { + if (tx instanceof fixedScriptWallet.BitGoPsbt) { const pubs = params.pubs; if (!pubs) { throw new Error('pub triple is required'); } - const walletXpubs: Triple | undefined = - pubs instanceof utxolib.bitgo.RootWalletKeys - ? (pubs.triple.map((k) => k.neutered().toBase58()) as Triple) - : isTriple(pubs) - ? (pubs as Triple) - : undefined; + const walletXpubs: Triple | undefined = isTriple(pubs) ? (pubs as Triple) : undefined; if (!walletXpubs) { throw new Error('pub triple must be valid triple or RootWalletKeys'); } @@ -68,6 +55,6 @@ export function explainTx( } else if (tx instanceof WasmPsbt) { throw new Error('descriptor Psbt is only supported for descriptor wallets'); } else { - return fixedScript.explainLegacyTx(tx, params, coinName); + throw new Error('unsupported transaction type'); } } diff --git a/modules/abstract-utxo/src/transaction/fetchInputs.ts b/modules/abstract-utxo/src/transaction/fetchInputs.ts deleted file mode 100644 index 01459430a4..0000000000 --- a/modules/abstract-utxo/src/transaction/fetchInputs.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { BitGoBase, IRequestTracer } from '@bitgo/sdk-core'; - -import { AbstractUtxoCoin, TransactionPrebuild } from '../abstractUtxoCoin'; -import { getNetworkFromCoinName, UtxoCoinName } from '../names'; - -/** - * Get the inputs for a psbt from a prebuild. - */ -export function getPsbtTxInputs( - psbtArg: string | utxolib.bitgo.UtxoPsbt, - coinName: UtxoCoinName -): { address: string; value: bigint; valueString: string }[] { - const network = getNetworkFromCoinName(coinName); - const psbt = psbtArg instanceof utxolib.bitgo.UtxoPsbt ? psbtArg : utxolib.bitgo.createPsbtFromHex(psbtArg, network); - const txInputs = psbt.txInputs; - return psbt.data.inputs.map((input, index) => { - let address: string; - let value: bigint; - if (input.witnessUtxo) { - address = utxolib.address.fromOutputScript(input.witnessUtxo.script, network); - value = input.witnessUtxo.value; - } else if (input.nonWitnessUtxo) { - const tx = utxolib.bitgo.createTransactionFromBuffer(input.nonWitnessUtxo, network, { - amountType: 'bigint', - }); - const txId = (Buffer.from(txInputs[index].hash).reverse() as Buffer).toString('hex'); - if (tx.getId() !== txId) { - throw new Error('input transaction hex does not match id'); - } - const prevTxOutputIndex = txInputs[index].index; - address = utxolib.address.fromOutputScript(tx.outs[prevTxOutputIndex].script, network); - value = tx.outs[prevTxOutputIndex].value; - } else { - throw new Error('psbt input is missing both witnessUtxo and nonWitnessUtxo'); - } - return { address, value, valueString: value.toString() }; - }); -} - -/** - * Get the inputs for a transaction from a prebuild. - */ -export async function getTxInputs(params: { - txPrebuild: TransactionPrebuild; - bitgo: BitGoBase; - coin: AbstractUtxoCoin; - disableNetworking: boolean; - reqId?: IRequestTracer; -}): Promise<{ address: string; value: TNumber; valueString: string }[]> { - const { txPrebuild, bitgo, coin, disableNetworking, reqId } = params; - if (!txPrebuild.txHex) { - throw new Error(`txPrebuild.txHex not set`); - } - const transaction = coin.createTransactionFromHex(txPrebuild.txHex); - const transactionCache = {}; - return await Promise.all( - transaction.ins.map(async (currentInput): Promise<{ address: string; value: TNumber; valueString: string }> => { - const transactionId = (Buffer.from(currentInput.hash).reverse() as Buffer).toString('hex'); - const txHex = txPrebuild.txInfo?.txHexes?.[transactionId]; - if (txHex) { - const localTx = coin.createTransactionFromHex(txHex); - if (localTx.getId() !== transactionId) { - throw new Error('input transaction hex does not match id'); - } - const currentOutput = localTx.outs[currentInput.index]; - const address = utxolib.address.fromOutputScript(currentOutput.script, coin.network); - return { - address, - value: currentOutput.value, - valueString: currentOutput.value.toString(), - }; - } else if (!transactionCache[transactionId]) { - if (disableNetworking) { - throw new Error('attempting to retrieve transaction details externally with networking disabled'); - } - if (reqId) { - bitgo.setRequestTracer(reqId); - } - transactionCache[transactionId] = await bitgo.get(coin.url(`/public/tx/${transactionId}`)).result(); - } - const transactionDetails = transactionCache[transactionId]; - return transactionDetails.outputs[currentInput.index]; - }) - ); -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts index 0165816e7d..3e5738b7bb 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts @@ -1,18 +1,7 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { bip322 } from '@bitgo/utxo-core'; -import { bitgo } from '@bitgo/utxo-lib'; -import { ITransactionExplanation as BaseTransactionExplanation, Triple } from '@bitgo/sdk-core'; -import { BIP32 } from '@bitgo/wasm-utxo'; -import * as utxocore from '@bitgo/utxo-core'; +import { ITransactionExplanation as BaseTransactionExplanation } from '@bitgo/sdk-core'; import type { Bip322Message } from '../../abstractUtxoCoin'; import type { Output, FixedScriptWalletOutput } from '../types'; -import type { Unspent } from '../../unspent'; -import { toExtendedAddressFormat } from '../recipient'; -import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey'; -import { toBip32Triple } from '../../keychains'; -import { toUtxolibBIP32 } from '../../wasmUtil'; -import { getNetworkFromCoinName, UtxoCoinName } from '../../names'; // ===== Transaction Explanation Type Definitions ===== @@ -56,408 +45,9 @@ export type TransactionExplanationWasm = AbstractUtxoTransactionExplanation; - /** When parsing a PSBT, we can infer the fee so we set TFee to string. */ export type TransactionExplanationUtxolibPsbt = TransactionExplanationWithSignatures; export type TransactionExplanationDescriptor = TransactionExplanationWithSignatures; -export type TransactionExplanation = - | TransactionExplanationUtxolibLegacy - | TransactionExplanationUtxolibPsbt - | TransactionExplanationWasm; - -export type ChangeAddressInfo = { - address: string; - chain: number; - index: number; -}; - -function toChangeOutput( - txOutput: utxolib.TxOutput, - coinName: UtxoCoinName, - changeInfo: ChangeAddressInfo[] | undefined -): FixedScriptWalletOutput | undefined { - if (!changeInfo) { - return undefined; - } - const address = toExtendedAddressFormat(txOutput.script, coinName); - const change = changeInfo.find((change) => change.address === address); - if (!change) { - return undefined; - } - return { - address, - amount: txOutput.value.toString(), - chain: change.chain, - index: change.index, - external: false, - }; -} - -function outputSum(outputs: { amount: string | number }[]): bigint { - return outputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0)); -} - -function explainCommon( - tx: bitgo.UtxoTransaction, - params: { - changeInfo?: ChangeAddressInfo[]; - customChangeInfo?: ChangeAddressInfo[]; - feeInfo?: string; - }, - coinName: UtxoCoinName -) { - const displayOrder = ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs']; - const changeOutputs: FixedScriptWalletOutput[] = []; - const customChangeOutputs: FixedScriptWalletOutput[] = []; - const externalOutputs: Output[] = []; - - const { changeInfo, customChangeInfo } = params; - - tx.outs.forEach((currentOutput) => { - // Try to encode the script pubkey with an address. If it fails, try to parse it as an OP_RETURN output with the prefix. - // If that fails, then it is an unrecognized scriptPubkey and should fail - const currentAddress = toExtendedAddressFormat(currentOutput.script, coinName); - const currentAmount = BigInt(currentOutput.value); - - const changeOutput = toChangeOutput(currentOutput, coinName, changeInfo); - if (changeOutput) { - changeOutputs.push(changeOutput); - return; - } - - const customChangeOutput = toChangeOutput(currentOutput, coinName, customChangeInfo); - if (customChangeOutput) { - customChangeOutputs.push(customChangeOutput); - return; - } - - externalOutputs.push({ - address: currentAddress, - amount: currentAmount.toString(), - // If changeInfo has a length greater than or equal to zero, it means that the change information - // was provided to the function but the output was not identified as change. In this case, - // the output is external, and we can set it as so. If changeInfo is undefined, it means we were - // given no information about change outputs, so we can't determine anything about the output, - // so we leave it undefined. - external: changeInfo ? true : undefined, - }); - }); - - const outputDetails = { - outputs: externalOutputs, - outputAmount: outputSum(externalOutputs).toString(), - - changeOutputs, - changeAmount: outputSum(changeOutputs).toString(), - - customChangeAmount: outputSum(customChangeOutputs).toString(), - customChangeOutputs, - }; - - let fee: string | undefined; - let locktime: number | undefined; - - if (params.feeInfo) { - displayOrder.push('fee'); - fee = params.feeInfo; - } - - if (Number.isInteger(tx.locktime) && tx.locktime > 0) { - displayOrder.push('locktime'); - locktime = tx.locktime; - } - - return { displayOrder, id: tx.getId(), ...outputDetails, fee, locktime }; -} - -function getRootWalletKeys(params: { pubs?: bitgo.RootWalletKeys | string[] }): bitgo.RootWalletKeys | undefined { - if (params.pubs instanceof bitgo.RootWalletKeys) { - return params.pubs; - } - const keys = params.pubs?.map((xpub) => toUtxolibBIP32(BIP32.fromBase58(xpub))); - return keys && keys.length === 3 ? new bitgo.RootWalletKeys(keys as Triple) : undefined; -} - -function getPsbtInputSignaturesCount( - psbt: bitgo.UtxoPsbt, - params: { - pubs?: bitgo.RootWalletKeys | string[]; - } -) { - const rootWalletKeys = getRootWalletKeys(params); - return rootWalletKeys - ? bitgo.getSignatureValidationArrayPsbt(psbt, rootWalletKeys).map((sv) => sv[1].filter((v) => v).length) - : (Array(psbt.data.inputs.length) as number[]).fill(0); -} - -function getTxInputSignaturesCount( - tx: bitgo.UtxoTransaction, - params: { - txInfo?: { unspents?: Unspent[] }; - pubs?: bitgo.RootWalletKeys | string[]; - }, - coinName: UtxoCoinName -) { - const network = getNetworkFromCoinName(coinName); - const prevOutputs = params.txInfo?.unspents?.map((u) => bitgo.toOutput(u, network)); - const rootWalletKeys = getRootWalletKeys(params); - const { unspents = [] } = params.txInfo ?? {}; - - // get the number of signatures per input - return tx.ins.map((input, idx): number => { - if (unspents.length !== tx.ins.length) { - return 0; - } - if (!prevOutputs) { - throw new Error(`invalid state`); - } - if (!rootWalletKeys) { - // no pub keys or incorrect number of pub keys - return 0; - } - try { - return bitgo.verifySignatureWithUnspent(tx, idx, unspents, rootWalletKeys).filter((v) => v).length; - } catch (e) { - // some other error occurred and we can't validate the signatures - return 0; - } - }); -} - -function getChainAndIndexFromBip32Derivations(output: bitgo.PsbtOutput) { - const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined; - if (!derivations) { - return undefined; - } - const paths = derivations.map((d) => d.path); - if (!paths || paths.length !== 3) { - throw new Error('expected 3 paths in bip32Derivation or tapBip32Derivation'); - } - if (!paths.every((p) => paths[0] === p)) { - throw new Error('expected all paths to be the same'); - } - - paths.forEach((path) => { - if (paths[0] !== path) { - throw new Error( - 'Unable to get a single chain and index on the output because there are different paths for different keys' - ); - } - }); - return utxolib.bitgo.getChainAndIndexFromPath(paths[0]); -} - -function getChangeInfo( - psbt: bitgo.UtxoPsbt, - walletKeys?: Triple | Triple -): ChangeAddressInfo[] | undefined { - let utxolibKeys: Triple; - try { - utxolibKeys = walletKeys - ? (walletKeys.map((k) => toUtxolibBIP32(k)) as Triple) - : utxolib.bitgo.getSortedRootNodes(psbt); - } catch (e) { - if (e instanceof utxolib.bitgo.ErrorNoMultiSigInputFound) { - return undefined; - } - throw e; - } - - return utxolib.bitgo.findWalletOutputIndices(psbt, utxolibKeys).map((i) => { - const derivationInformation = getChainAndIndexFromBip32Derivations(psbt.data.outputs[i]); - if (!derivationInformation) { - throw new Error('could not find derivation information on bip32Derivation or tapBip32Derivation'); - } - return { - address: utxolib.address.fromOutputScript(psbt.txOutputs[i].script, psbt.network), - external: false, - ...derivationInformation, - }; - }); -} - -/** - * Extract PayGo address proof information from the PSBT if present - * @returns Information about the PayGo proof, including the output index and address - */ -function getPayGoVerificationInfo( - psbt: bitgo.UtxoPsbt, - coinName: UtxoCoinName -): { outputIndex: number; verificationPubkey: string } | undefined { - let outputIndex: number | undefined = undefined; - let address: string | undefined = undefined; - // Check if this PSBT has any PayGo address proofs - if (!utxocore.paygo.psbtOutputIncludesPaygoAddressProof(psbt)) { - return undefined; - } - - // This pulls the pubkey depending on given network - const verificationPubkey = getPayGoVerificationPubkey(coinName); - // find which output index that contains the PayGo proof - outputIndex = utxocore.paygo.getPayGoAddressProofOutputIndex(psbt); - if (outputIndex === undefined || !verificationPubkey) { - return undefined; - } - const network = getNetworkFromCoinName(coinName); - const output = psbt.txOutputs[outputIndex]; - address = utxolib.address.fromOutputScript(output.script, network); - if (!address) { - throw new Error(`Can not derive address ${address} Pay Go Attestation.`); - } - - return { outputIndex, verificationPubkey }; -} - -/** - * Extract the BIP322 messages and addresses from the PSBT inputs and perform - * verification on the transaction to ensure that it meets the BIP322 requirements. - * @returns An array of objects containing the message and address for each input, - * or undefined if no BIP322 messages are found. - */ -function getBip322MessageInfoAndVerify(psbt: bitgo.UtxoPsbt, coinName: UtxoCoinName): Bip322Message[] | undefined { - const network = getNetworkFromCoinName(coinName); - const bip322Messages: { message: string; address: string }[] = []; - for (let i = 0; i < psbt.data.inputs.length; i++) { - const message = bip322.getBip322ProofMessageAtIndex(psbt, i); - if (message) { - const input = psbt.data.inputs[i]; - if (!input.witnessUtxo) { - throw new Error(`Missing witnessUtxo for input index ${i}`); - } - const scriptPubKey = input.witnessUtxo.script; - - // Verify that the toSpend transaction can be recreated in the PSBT and is encoded correctly in the nonWitnessUtxo - const toSpend = bip322.buildToSpendTransaction(scriptPubKey, message); - - // Verify that the toSpend transaction ID matches the input's referenced transaction ID - if (toSpend.getId() !== utxolib.bitgo.getOutputIdForInput(psbt.txInputs[i]).txid) { - throw new Error(`ToSpend transaction ID does not match the input at index ${i}`); - } - - // Verify the input specifics - if (psbt.txInputs[i].sequence !== 0) { - throw new Error(`Unexpected sequence number at input index ${i}: ${psbt.txInputs[i].sequence}. Expected 0.`); - } - if (psbt.txInputs[i].index !== 0) { - throw new Error(`Unexpected input index at position ${i}: ${psbt.txInputs[i].index}. Expected 0.`); - } - - bip322Messages.push({ - message: message.toString('utf8'), - address: utxolib.address.fromOutputScript(scriptPubKey, network), - }); - } - } - - if (bip322Messages.length > 0) { - // If there is a BIP322 message in any input, all inputs must have one. - if (bip322Messages.length !== psbt.data.inputs.length) { - throw new Error('Inconsistent BIP322 messages across inputs.'); - } - - // Verify the transaction specifics for BIP322 - if (psbt.version !== 0 && psbt.version !== 2) { - throw new Error(`Unsupported PSBT version for BIP322: ${psbt.version}. Expected 0 `); - } - if ( - psbt.data.outputs.length !== 1 || - psbt.txOutputs[0].script.toString('hex') !== '6a' || - psbt.txOutputs[0].value !== 0n - ) { - throw new Error(`Invalid PSBT outputs for BIP322. Expected exactly one OP_RETURN output with zero value.`); - } - - return bip322Messages; - } - - return undefined; -} - -/** - * Decompose a raw psbt into useful information, such as the total amounts, - * change amounts, and transaction outputs. - * - * @param psbt {bitgo.UtxoPsbt} The PSBT to explain - * @param pubs {bitgo.RootWalletKeys | string[]} The public keys to use for the explanation - * @param coinName {UtxoCoinName} The coin name to use for the explanation - * @param strict {boolean} Whether to throw an error if the PayGo address proof is invalid - */ -export function explainPsbt( - psbt: bitgo.UtxoPsbt, - params: { - pubs?: bitgo.RootWalletKeys | string[]; - customChangePubs?: bitgo.RootWalletKeys | string[]; - }, - coinName: UtxoCoinName, - { strict = true }: { strict?: boolean } = {} -): TransactionExplanationUtxolibPsbt { - const network = getNetworkFromCoinName(coinName); - const payGoVerificationInfo = getPayGoVerificationInfo(psbt, coinName); - if (payGoVerificationInfo) { - try { - utxocore.paygo.verifyPayGoAddressProof( - psbt, - payGoVerificationInfo.outputIndex, - Buffer.from(BIP32.fromBase58(payGoVerificationInfo.verificationPubkey).publicKey) - ); - } catch (e) { - if (strict) { - throw e; - } - console.error(e); - } - } - - const messages = getBip322MessageInfoAndVerify(psbt, coinName); - const changeInfo = getChangeInfo(psbt); - const customChangeInfo = params.customChangePubs - ? getChangeInfo(psbt, toBip32Triple(params.customChangePubs)) - : undefined; - const tx = psbt.getUnsignedTx(); - const common = explainCommon(tx, { ...params, changeInfo, customChangeInfo }, coinName); - const inputSignaturesCount = getPsbtInputSignaturesCount(psbt, params); - - // Set fee from subtracting inputs from outputs - const outputAmount = psbt.txOutputs.reduce((cumulative, curr) => cumulative + BigInt(curr.value), BigInt(0)); - const inputAmount = psbt.txInputs.reduce((cumulative, txInput, i) => { - const data = psbt.data.inputs[i]; - if (data.witnessUtxo) { - return cumulative + BigInt(data.witnessUtxo.value); - } else if (data.nonWitnessUtxo) { - const tx = bitgo.createTransactionFromBuffer(data.nonWitnessUtxo, network, { amountType: 'bigint' }); - return cumulative + BigInt(tx.outs[txInput.index].value); - } else { - throw new Error('could not find value on input'); - } - }, BigInt(0)); - - return { - ...common, - fee: (inputAmount - outputAmount).toString(), - inputSignatures: inputSignaturesCount, - signatures: inputSignaturesCount.reduce((prev, curr) => (curr > prev ? curr : prev), 0), - messages, - }; -} - -export function explainLegacyTx( - tx: bitgo.UtxoTransaction, - params: { - pubs?: string[]; - txInfo?: { unspents?: Unspent[] }; - changeInfo?: { address: string; chain: number; index: number }[]; - }, - coinName: UtxoCoinName -): TransactionExplanationUtxolibLegacy { - const common = explainCommon(tx, params, coinName); - const inputSignaturesCount = getTxInputSignaturesCount(tx, params, coinName); - return { - ...common, - inputSignatures: inputSignaturesCount, - signatures: inputSignaturesCount.reduce((prev, curr) => (curr > prev ? curr : prev), 0), - }; -} +export type TransactionExplanation = TransactionExplanationUtxolibPsbt | TransactionExplanationWasm; diff --git a/modules/abstract-utxo/src/transaction/fixedScript/index.ts b/modules/abstract-utxo/src/transaction/fixedScript/index.ts index ee341fdcfc..3fdccbe180 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/index.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/index.ts @@ -1,4 +1,3 @@ -export { explainPsbt, explainLegacyTx, ChangeAddressInfo } from './explainTransaction'; export { explainPsbtWasm, explainPsbtWasmBigInt, @@ -11,6 +10,5 @@ export { parseTransaction } from './parseTransaction'; export { CustomChangeOptions } from './parseOutput'; export { verifyTransaction } from './verifyTransaction'; export { signTransaction } from './signTransaction'; -export * from './signLegacyTransaction'; export * from './SigningError'; export * from './replayProtection'; diff --git a/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts index c776cf5883..e28e5525a5 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts @@ -210,7 +210,6 @@ export async function parseTransaction( const explanation: TransactionExplanation = await coin.explainTransaction({ txHex: effectiveTxHex, txInfo: txPrebuild.txInfo, - decodeWith: txPrebuild.decodeWith, pubs: keychainArray.map((k) => k.pub) as Triple, customChangeXpubs, }); diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts deleted file mode 100644 index 304e012740..0000000000 --- a/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts +++ /dev/null @@ -1,190 +0,0 @@ -import assert from 'assert'; - -import * as utxolib from '@bitgo/utxo-lib'; -import { bitgo } from '@bitgo/utxo-lib'; -import { isTriple, Triple } from '@bitgo/sdk-core'; -import { BIP32, bip32 } from '@bitgo/wasm-utxo'; -import debugLib from 'debug'; - -import { UtxoCoinName } from '../../names'; -import { isWalletUnspent, type Unspent } from '../../unspent'; -import { toUtxolibBIP32 } from '../../wasmUtil'; - -import { getReplayProtectionAddresses } from './replayProtection'; -import { InputSigningError, TransactionSigningError } from './SigningError'; - -const debug = debugLib('bitgo:v2:utxo'); - -const { signInputWithUnspent, toOutput } = utxolib.bitgo; - -type RootWalletKeys = utxolib.bitgo.RootWalletKeys; - -const UTXOLIB_VALID_CHAIN_CODES = new Set([0, 1, 10, 11, 20, 21, 30, 31, 40, 41] as const); - -/** - * Sign all inputs of a wallet transaction and verify signatures after signing. - * Collects and logs signing errors and verification errors, throws error in the end if any of them - * failed. - * - * @param transaction - wallet transaction (builder) to be signed - * @param unspents - transaction unspents - * @param walletSigner - signing parameters - * @param coinName - coin name for network-specific logic - * @param isLastSignature - Returns full-signed transaction when true. Builds half-signed when false. - * @param replayProtectionAddresses - List of replay protection addresses to skip signing - */ -export function signAndVerifyWalletTransaction( - transaction: utxolib.bitgo.UtxoTransaction | utxolib.bitgo.UtxoTransactionBuilder, - unspents: Unspent[], - walletSigner: utxolib.bitgo.WalletUnspentSigner, - coinName: UtxoCoinName, - { - isLastSignature, - replayProtectionAddresses, - }: { - isLastSignature: boolean; - replayProtectionAddresses?: string[]; - } -): utxolib.bitgo.UtxoTransaction { - const network = transaction.network as utxolib.Network; - if (replayProtectionAddresses === undefined) { - replayProtectionAddresses = getReplayProtectionAddresses(coinName); - } - const prevOutputs = unspents.map((u) => toOutput(u, network)); - - let txBuilder: utxolib.bitgo.UtxoTransactionBuilder; - if (transaction instanceof utxolib.bitgo.UtxoTransaction) { - txBuilder = utxolib.bitgo.createTransactionBuilderFromTransaction(transaction, prevOutputs); - if (transaction.ins.length !== unspents.length) { - throw new Error(`transaction inputs must match unspents`); - } - } else if (transaction instanceof utxolib.bitgo.UtxoTransactionBuilder) { - txBuilder = transaction; - } else { - throw new Error(`must pass UtxoTransaction or UtxoTransactionBuilder`); - } - - const signErrors: InputSigningError[] = unspents - .map((unspent: Unspent, inputIndex: number) => { - if (replayProtectionAddresses.includes(unspent.address)) { - debug('Skipping signature for input %d of %d (RP input?)', inputIndex + 1, unspents.length); - return; - } - if (!isWalletUnspent(unspent)) { - return InputSigningError.expectedWalletUnspent(inputIndex, null, unspent); - } - if (!UTXOLIB_VALID_CHAIN_CODES.has(unspent.chain as utxolib.bitgo.ChainCode)) { - return new InputSigningError( - inputIndex, - null, - unspent, - new Error(`Chain code ${unspent.chain} is not supported for legacy signing`) - ); - } - try { - signInputWithUnspent( - txBuilder, - inputIndex, - unspent as unknown as utxolib.bitgo.WalletUnspent, - walletSigner - ); - debug('Successfully signed input %d of %d', inputIndex + 1, unspents.length); - } catch (e) { - return new InputSigningError(inputIndex, null, unspent, e); - } - }) - .filter((e): e is InputSigningError => e !== undefined); - - const signedTransaction = isLastSignature ? txBuilder.build() : txBuilder.buildIncomplete(); - - const verifyErrors: InputSigningError[] = signedTransaction.ins - .map((input, inputIndex) => { - const unspent = unspents[inputIndex] as Unspent; - if (replayProtectionAddresses.includes(unspent.address)) { - debug( - 'Skipping input signature %d of %d (unspent from replay protection address which is platform signed only)', - inputIndex + 1, - unspents.length - ); - return; - } - if (!isWalletUnspent(unspent)) { - return InputSigningError.expectedWalletUnspent(inputIndex, null, unspent); - } - if (!UTXOLIB_VALID_CHAIN_CODES.has(unspent.chain as utxolib.bitgo.ChainCode)) { - return new InputSigningError( - inputIndex, - null, - unspent, - new Error(`Chain code ${unspent.chain} is not supported for legacy verification`) - ); - } - const walletUnspent = unspent; - try { - const publicKey = walletSigner.deriveForChainAndIndex( - walletUnspent.chain as utxolib.bitgo.ChainCode, - walletUnspent.index - ).signer.publicKey; - if ( - !utxolib.bitgo.verifySignatureWithPublicKey(signedTransaction, inputIndex, prevOutputs, publicKey) - ) { - return new InputSigningError(inputIndex, null, unspent, new Error(`invalid signature`)); - } - } catch (e) { - debug('Invalid signature'); - return new InputSigningError(inputIndex, null, unspent, e); - } - }) - .filter((e): e is InputSigningError => e !== undefined); - - if (signErrors.length || verifyErrors.length) { - throw new TransactionSigningError(signErrors, verifyErrors); - } - - return signedTransaction; -} - -export function signLegacyTransaction( - tx: utxolib.bitgo.UtxoTransaction, - signerKeychain: bip32.BIP32Interface | undefined, - coinName: UtxoCoinName, - params: { - isLastSignature: boolean; - signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - txInfo: { unspents?: Unspent[] } | undefined; - pubs: string[] | undefined; - cosignerPub: string | undefined; - } -): utxolib.bitgo.UtxoTransaction { - switch (params.signingStep) { - case 'signerNonce': - case 'cosignerNonce': - /** - * In certain cases, the caller of this method may not know whether the txHex contains a psbt with taproot key path spend input(s). - * Instead of throwing error, no-op and return the txHex. So that the caller can call this method in the same sequence. - */ - return tx; - } - - if (tx.ins.length !== params.txInfo?.unspents?.length) { - throw new Error('length of unspents array should equal to the number of transaction inputs'); - } - - if (!params.pubs || !isTriple(params.pubs)) { - throw new Error(`must provide xpub array`); - } - - const keychains = params.pubs.map((pub) => toUtxolibBIP32(BIP32.fromBase58(pub))) as Triple; - const cosignerPub = params.cosignerPub ?? params.pubs[2]; - const cosignerKeychain = toUtxolibBIP32(BIP32.fromBase58(cosignerPub)); - - assert(signerKeychain); - const walletSigner = new bitgo.WalletUnspentSigner( - keychains, - toUtxolibBIP32(signerKeychain), - cosignerKeychain - ); - return signAndVerifyWalletTransaction(tx, params.txInfo.unspents, walletSigner, coinName, { - isLastSignature: params.isLastSignature, - }) as utxolib.bitgo.UtxoTransaction; -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signPsbtUtxolib.ts b/modules/abstract-utxo/src/transaction/fixedScript/signPsbtUtxolib.ts deleted file mode 100644 index 12bf626dd1..0000000000 --- a/modules/abstract-utxo/src/transaction/fixedScript/signPsbtUtxolib.ts +++ /dev/null @@ -1,159 +0,0 @@ -import assert from 'assert'; - -import * as utxolib from '@bitgo/utxo-lib'; -import { bitgo } from '@bitgo/utxo-lib'; -import debugLib from 'debug'; - -import { InputSigningError, TransactionSigningError } from './SigningError'; -import { Musig2Participant } from './musig2'; - -const debug = debugLib('bitgo:v2:utxo'); - -export type PsbtParsedScriptType = - | 'p2sh' - | 'p2wsh' - | 'p2shP2wsh' - | 'p2shP2pk' - | 'taprootKeyPathSpend' - | 'taprootScriptPathSpend' - // wasm-utxo types - | 'p2trLegacy' - | 'p2trMusig2ScriptPath' - | 'p2trMusig2KeyPath'; - -/** - * Sign all inputs of a psbt and verify signatures after signing. - * Collects and logs signing errors and verification errors, throws error in the end if any of them - * failed. - * - * This function mirrors signAndVerifyWalletTransaction, but is used for signing PSBTs instead of - * using TransactionBuilder - * - * @param psbt - * @param signerKeychain - */ -export function signAndVerifyPsbt( - psbt: utxolib.bitgo.UtxoPsbt, - signerKeychain: utxolib.BIP32Interface -): utxolib.bitgo.UtxoPsbt { - const txInputs = psbt.txInputs; - const outputIds: string[] = []; - const scriptTypes: PsbtParsedScriptType[] = []; - - const signErrors: InputSigningError[] = psbt.data.inputs - .map((input, inputIndex: number) => { - const outputId = utxolib.bitgo.formatOutputId(utxolib.bitgo.getOutputIdForInput(txInputs[inputIndex])); - outputIds.push(outputId); - - const { scriptType } = utxolib.bitgo.parsePsbtInput(input); - scriptTypes.push(scriptType); - - if (scriptType === 'p2shP2pk') { - debug('Skipping signature for input %d of %d (RP input?)', inputIndex + 1, psbt.data.inputs.length); - return; - } - - try { - psbt.signInputHD(inputIndex, signerKeychain); - debug('Successfully signed input %d of %d', inputIndex + 1, psbt.data.inputs.length); - } catch (e) { - return new InputSigningError(inputIndex, scriptType, { id: outputId }, e); - } - }) - .filter((e): e is InputSigningError => e !== undefined); - - const verifyErrors: InputSigningError[] = psbt.data.inputs - .map((input, inputIndex) => { - const scriptType = scriptTypes[inputIndex]; - if (scriptType === 'p2shP2pk') { - debug( - 'Skipping input signature %d of %d (unspent from replay protection address which is platform signed only)', - inputIndex + 1, - psbt.data.inputs.length - ); - return; - } - - const outputId = outputIds[inputIndex]; - try { - if (!psbt.validateSignaturesOfInputHD(inputIndex, signerKeychain)) { - return new InputSigningError(inputIndex, scriptType, { id: outputId }, new Error(`invalid signature`)); - } - } catch (e) { - debug('Invalid signature'); - return new InputSigningError(inputIndex, scriptType, { id: outputId }, e); - } - }) - .filter((e): e is InputSigningError => e !== undefined); - - if (signErrors.length || verifyErrors.length) { - throw new TransactionSigningError(signErrors, verifyErrors); - } - - return psbt; -} - -/** - * Key Value: Unsigned tx id => PSBT - * It is used to cache PSBTs with taproot key path (MuSig2) inputs during external express signer is activated. - * Reason: MuSig2 signer secure nonce is cached in the UtxoPsbt object. It will be required during the signing step. - * For more info, check SignTransactionOptions.signingStep - * - * TODO BTC-276: This cache may need to be done with LRU like memory safe caching if memory issues comes up. - */ -const PSBT_CACHE = new Map(); - -export async function signPsbtWithMusig2ParticipantUtxolib( - coin: Musig2Participant, - tx: utxolib.bitgo.UtxoPsbt, - signerKeychain: utxolib.BIP32Interface | undefined, - params: { - signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - walletId: string | undefined; - } -): Promise { - if (bitgo.isTransactionWithKeyPathSpendInput(tx)) { - switch (params.signingStep) { - case 'signerNonce': - assert(signerKeychain); - tx.setAllInputsMusig2NonceHD(signerKeychain); - PSBT_CACHE.set(tx.getUnsignedTx().getId(), tx); - return tx; - case 'cosignerNonce': - assert(params.walletId, 'walletId is required for MuSig2 bitgo nonce'); - return await coin.getMusig2Nonces(tx, params.walletId); - case 'signerSignature': - const txId = tx.getUnsignedTx().getId(); - const psbt = PSBT_CACHE.get(txId); - assert( - psbt, - `Psbt is missing from txCache (cache size ${PSBT_CACHE.size}). - This may be due to the request being routed to a different BitGo-Express instance that for signing step 'signerNonce'.` - ); - PSBT_CACHE.delete(txId); - tx = psbt.combine(tx); - break; - default: - // this instance is not an external signer - assert(params.walletId, 'walletId is required for MuSig2 bitgo nonce'); - assert(signerKeychain); - tx.setAllInputsMusig2NonceHD(signerKeychain); - const response = await coin.getMusig2Nonces(tx, params.walletId); - tx = tx.combine(response); - break; - } - } else { - switch (params.signingStep) { - case 'signerNonce': - case 'cosignerNonce': - /** - * In certain cases, the caller of this method may not know whether the txHex contains a psbt with taproot key path spend input(s). - * Instead of throwing error, no-op and return the txHex. So that the caller can call this method in the same sequence. - */ - return tx; - } - } - - assert(signerKeychain); - return signAndVerifyPsbt(tx, signerKeychain); -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts index 23961037da..a9dec08532 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts @@ -2,64 +2,30 @@ import assert from 'assert'; import { isTriple } from '@bitgo/sdk-core'; import _ from 'lodash'; -import { bitgo } from '@bitgo/utxo-lib'; -import * as utxolib from '@bitgo/utxo-lib'; import { BIP32, bip32, fixedScriptWallet } from '@bitgo/wasm-utxo'; import { UtxoCoinName } from '../../names'; import type { Unspent } from '../../unspent'; -import { toUtxolibBIP32 } from '../../wasmUtil'; import { Musig2Participant } from './musig2'; -import { signLegacyTransaction } from './signLegacyTransaction'; -import { signPsbtWithMusig2ParticipantUtxolib, signAndVerifyPsbt as signAndVerifyPsbtUtxolib } from './signPsbtUtxolib'; import { signPsbtWithMusig2ParticipantWasm, signAndVerifyPsbtWasm, ReplayProtectionKeys } from './signPsbtWasm'; import { getReplayProtectionPubkeys } from './replayProtection'; -/** - * Sign and verify a PSBT using either utxolib or wasm-utxo depending on the PSBT type. - */ -export function signAndVerifyPsbt( - psbt: utxolib.bitgo.UtxoPsbt, - signerKeychain: bip32.BIP32Interface | BIP32, - rootWalletKeys: fixedScriptWallet.RootWalletKeys | undefined, - replayProtection: ReplayProtectionKeys | undefined, - options?: { writeSignedWith?: boolean } -): utxolib.bitgo.UtxoPsbt; export function signAndVerifyPsbt( psbt: fixedScriptWallet.BitGoPsbt, signerKeychain: bip32.BIP32Interface | BIP32, rootWalletKeys: fixedScriptWallet.RootWalletKeys, replayProtection: ReplayProtectionKeys, - options?: { writeSignedWith?: boolean } -): fixedScriptWallet.BitGoPsbt; -export function signAndVerifyPsbt( - psbt: utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt, - signerKeychain: bip32.BIP32Interface | BIP32, - rootWalletKeys: fixedScriptWallet.RootWalletKeys, - replayProtection: ReplayProtectionKeys, - options?: { writeSignedWith?: boolean } -): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt; -export function signAndVerifyPsbt( - psbt: utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt, - signerKeychain: bip32.BIP32Interface | BIP32, - rootWalletKeys: fixedScriptWallet.RootWalletKeys | undefined, - replayProtection: ReplayProtectionKeys | undefined, options: { writeSignedWith?: boolean } = {} -): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt { - if (psbt instanceof bitgo.UtxoPsbt) { - return signAndVerifyPsbtUtxolib(psbt, toUtxolibBIP32(signerKeychain)); - } +): fixedScriptWallet.BitGoPsbt { assert(rootWalletKeys, 'rootWalletKeys required for wasm-utxo signing'); assert(replayProtection, 'replayProtection required for wasm-utxo signing'); return signAndVerifyPsbtWasm(psbt, signerKeychain, rootWalletKeys, replayProtection, options); } -export async function signTransaction< - T extends utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction | fixedScriptWallet.BitGoPsbt ->( - coin: Musig2Participant | Musig2Participant, - tx: T, +export async function signTransaction( + coin: Musig2Participant, + tx: fixedScriptWallet.BitGoPsbt, signerKeychain: bip32.BIP32Interface | undefined, coinName: UtxoCoinName, params: { @@ -67,17 +33,13 @@ export async function signTransaction< txInfo: { unspents?: Unspent[] } | undefined; isLastSignature: boolean; signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - /** deprecated */ - allowNonSegwitSigningWithoutPrevTx: boolean; pubs: string[] | undefined; cosignerPub: string | undefined; /** When true (default), extract finalized PSBT to legacy transaction format. When false, return finalized PSBT. */ extractTransaction?: boolean; writeSignedWith?: boolean; } -): Promise< - utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction | fixedScriptWallet.BitGoPsbt | Buffer -> { +): Promise { let isLastSignature = false; if (_.isBoolean(params.isLastSignature)) { // if build is called instead of buildIncomplete, no signature placeholders are left in the sig script @@ -86,59 +48,23 @@ export async function signTransaction< const { extractTransaction = true } = params; - if (tx instanceof bitgo.UtxoPsbt) { - const signedPsbt = await signPsbtWithMusig2ParticipantUtxolib( - coin as Musig2Participant, - tx, - signerKeychain ? toUtxolibBIP32(signerKeychain) : undefined, - { - signingStep: params.signingStep, - walletId: params.walletId, - } - ); - if (isLastSignature) { - if (extractTransaction) { - signedPsbt.finalizeAllInputs(); - return signedPsbt.extractTransaction(); - } - // Return signed PSBT without finalizing to preserve derivation info - return signedPsbt; - } - return signedPsbt; - } 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); - const signedPsbt = await signPsbtWithMusig2ParticipantWasm( - coin as Musig2Participant, - tx, - signerKeychain, - rootWalletKeys, - { - replayProtection: { - publicKeys: getReplayProtectionPubkeys(coinName), - }, - signingStep: params.signingStep, - walletId: params.walletId, - writeSignedWith: params.writeSignedWith, - } - ); - if (isLastSignature) { - if (extractTransaction) { - signedPsbt.finalizeAllInputs(); - return Buffer.from(signedPsbt.extractTransaction().toBytes()); - } - // Return finalized PSBT without extracting to legacy format - return signedPsbt; + 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); + const signedPsbt = await signPsbtWithMusig2ParticipantWasm(coin, tx, signerKeychain, rootWalletKeys, { + replayProtection: { + publicKeys: getReplayProtectionPubkeys(coinName), + }, + signingStep: params.signingStep, + walletId: params.walletId, + writeSignedWith: params.writeSignedWith, + }); + if (isLastSignature) { + if (extractTransaction) { + signedPsbt.finalizeAllInputs(); + return Buffer.from(signedPsbt.extractTransaction().toBytes()); } return signedPsbt; } - - return signLegacyTransaction(tx, signerKeychain, coinName, { - isLastSignature, - signingStep: params.signingStep, - txInfo: params.txInfo, - pubs: params.pubs, - cosignerPub: params.cosignerPub, - }); + return signedPsbt; } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts index c7a2f9ab5d..8b7659953b 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts @@ -2,12 +2,12 @@ import buildDebug from 'debug'; import _ from 'lodash'; import BigNumber from 'bignumber.js'; import { BitGoBase, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core'; -import * as utxolib from '@bitgo/utxo-lib'; +import { hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin'; -import { Output, ParsedTransaction } from '../types'; +import { ParsedTransaction } from '../types'; +import { stringToBufferTryFormats } from '../decode'; import { verifyCustomChangeKeySignatures, verifyKeySignature, verifyUserPublicKey } from '../../verifyKey'; -import { getPsbtTxInputs, getTxInputs } from '../fetchInputs'; const debug = buildDebug('bitgo:abstract-utxo:verifyTransaction'); @@ -60,7 +60,7 @@ export async function verifyTransaction( if (!_.isUndefined(verification.disableNetworking) && !_.isBoolean(verification.disableNetworking)) { throw new TypeError('verification.disableNetworking must be a boolean'); } - const isPsbt = txPrebuild.txHex && utxolib.bitgo.isPsbt(txPrebuild.txHex); + const isPsbt = txPrebuild.txHex && hasPsbtMagic(stringToBufferTryFormats(txPrebuild.txHex, ['hex', 'base64'])); if (isPsbt && txPrebuild.txInfo?.unspents) { throw new Error('should not have unspents in txInfo for psbt'); } @@ -173,29 +173,9 @@ export async function verifyTransaction( } } - const allOutputs = parsedTransaction.outputs; if (!txPrebuild.txHex) { throw new Error(`txPrebuild.txHex not set`); } - const inputs = isPsbt - ? getPsbtTxInputs(txPrebuild.txHex, coin.name).map((v) => ({ - ...v, - value: utxolib.bitgo.toTNumber(v.value, coin.amountType), - })) - : await getTxInputs({ txPrebuild, bitgo, coin, disableNetworking, reqId }); - // coins (doge) that can exceed number limits (and thus will use bigint) will have the `valueString` field - const inputAmount = inputs.reduce( - (sum: bigint, i) => sum + BigInt(coin.amountType === 'bigint' ? i.valueString : i.value), - BigInt(0) - ); - const outputAmount = allOutputs.reduce((sum: bigint, o: Output) => sum + BigInt(o.amount), BigInt(0)); - const fee = inputAmount - outputAmount; - - if (fee < 0) { - throw new Error( - `attempting to spend ${outputAmount} satoshis, which exceeds the input amount (${inputAmount} satoshis) by ${-fee}` - ); - } return true; } diff --git a/modules/abstract-utxo/src/transaction/index.ts b/modules/abstract-utxo/src/transaction/index.ts index a14f794f7d..d0231ff6cb 100644 --- a/modules/abstract-utxo/src/transaction/index.ts +++ b/modules/abstract-utxo/src/transaction/index.ts @@ -3,7 +3,6 @@ export * from './recipient'; export { explainTx } from './explainTransaction'; export { parseTransaction } from './parseTransaction'; export { verifyTransaction } from './verifyTransaction'; -export * from './fetchInputs'; export * as bip322 from './bip322'; -export { decodePsbt, decodePsbtWith } from './decode'; +export { decodePsbt } from './decode'; export * from './fixedScript'; diff --git a/modules/abstract-utxo/src/transaction/signTransaction.ts b/modules/abstract-utxo/src/transaction/signTransaction.ts index ace4ce0dac..7a08d0b79f 100644 --- a/modules/abstract-utxo/src/transaction/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/signTransaction.ts @@ -6,11 +6,10 @@ import buildDebug from 'debug'; import { AbstractUtxoCoin, SignTransactionOptions } from '../abstractUtxoCoin'; import { getDescriptorMapFromWallet, getPolicyForEnv, isDescriptorWallet } from '../descriptor'; import { fetchKeychains, toBip32Triple } from '../keychains'; -import { isUtxoLibPsbt } from '../wasmUtil'; import * as fixedScript from './fixedScript'; import * as descriptor from './descriptor'; -import { decodeDescriptorPsbt, decodePsbtWith, encodeTransaction } from './decode'; +import { decodeDescriptorPsbt, encodeTransaction } from './decode'; const debug = buildDebug('bitgo:abstract-utxo:transaction:signTransaction'); @@ -59,20 +58,13 @@ export async function signTransaction( }); return { txHex: Buffer.from(psbt.serialize()).toString('hex') }; } else { - let tx = coin.decodeTransactionFromPrebuild(params.txPrebuild); - - // When returnLegacyFormat is set, ensure we use wasm-utxo's BitGoPsbt so - // getHalfSignedLegacyFormat() is available after signing. - if (params.returnLegacyFormat && isUtxoLibPsbt(tx)) { - tx = decodePsbtWith(tx.toBuffer(), coin.name, 'wasm-utxo'); - } + const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild); const signedTx = await fixedScript.signTransaction(coin, tx, getSignerKeychain(params.prv), coin.name, { walletId: params.txPrebuild.walletId, txInfo: params.txPrebuild.txInfo, isLastSignature: params.isLastSignature ?? false, signingStep: params.signingStep, - allowNonSegwitSigningWithoutPrevTx: params.allowNonSegwitSigningWithoutPrevTx ?? false, pubs: params.pubs, cosignerPub: params.cosignerPub, extractTransaction: params.extractTransaction, diff --git a/modules/abstract-utxo/src/transaction/types.ts b/modules/abstract-utxo/src/transaction/types.ts index 329acace5b..2ed4bdd86b 100644 --- a/modules/abstract-utxo/src/transaction/types.ts +++ b/modules/abstract-utxo/src/transaction/types.ts @@ -1,4 +1,3 @@ -import * as utxolib from '@bitgo/utxo-lib'; import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import type { UtxoNamedKeychains } from '../keychains'; @@ -7,16 +6,8 @@ import type { CustomChangeOptions } from './fixedScript'; export type BitGoPsbt = fixedScriptWallet.BitGoPsbt; -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 - | fixedScriptWallet.BitGoPsbt; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type DecodedTransaction = fixedScriptWallet.BitGoPsbt; export interface BaseOutput { address: string; diff --git a/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts b/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts index 2beb751815..a6b064904b 100644 --- a/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts +++ b/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts @@ -11,6 +11,7 @@ import { encryptKeychain, getDefaultWalletKeys, getMinUtxoCoins, + getNetworkForCoinName, getUtxoWallet, keychainsBase58, getScriptTypes, @@ -46,12 +47,17 @@ describe('prebuildAndSign-returnLegacyFormat', function () { const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee; const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh'; const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType); - const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network); + const outputAddress = utxolib.bitgo.getWalletAddress( + rootWalletKeys, + outputChain, + 0, + getNetworkForCoinName(coin.name) + ); recipient = { address: outputAddress, amount: outputAmount.toString() }; prebuild = utxolib.testutil.constructPsbt( inputScripts.map((s) => ({ scriptType: s, value: BigInt(1e8) })), [{ scriptType: outputScriptType, value: outputAmount }], - coin.network, + getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned' ); diff --git a/modules/abstract-utxo/test/unit/customSigner.ts b/modules/abstract-utxo/test/unit/customSigner.ts index 0e673009aa..681c4155ff 100644 --- a/modules/abstract-utxo/test/unit/customSigner.ts +++ b/modules/abstract-utxo/test/unit/customSigner.ts @@ -3,7 +3,14 @@ import nock = require('nock'); import * as sinon from 'sinon'; import { CustomSigningFunction, common } from '@bitgo/sdk-core'; -import { defaultBitGo, getDefaultWalletKeys, getUtxoCoin, getUtxoWallet, assertHasProperty } from './util'; +import { + defaultBitGo, + getDefaultWalletKeys, + getNetworkForCoinName, + getUtxoCoin, + getUtxoWallet, + assertHasProperty, +} from './util'; nock.disableNetConnect(); @@ -56,7 +63,7 @@ describe('UTXO Custom Signer Function', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }], [{ scriptType: 'p2sh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); @@ -72,7 +79,7 @@ describe('UTXO Custom Signer Function', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'p2wsh', value: BigInt(1000) }], [{ scriptType: 'p2sh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); diff --git a/modules/abstract-utxo/test/unit/prebuildAndSign.ts b/modules/abstract-utxo/test/unit/prebuildAndSign.ts index e2ae0dd0ec..24910bea5b 100644 --- a/modules/abstract-utxo/test/unit/prebuildAndSign.ts +++ b/modules/abstract-utxo/test/unit/prebuildAndSign.ts @@ -12,6 +12,7 @@ import { defaultBitGo, getDefaultWalletKeys, getMinUtxoCoins, + getNetworkForCoinName, getUtxoWallet, keychainsBase58, getScriptTypes, @@ -62,7 +63,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { const psbt = utxolib.testutil.constructPsbt( inputs as utxolib.testutil.Input[], outputs, - coin.network, + getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { @@ -178,7 +179,12 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee; const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh'; const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType); - const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network); + const outputAddress = utxolib.bitgo.getWalletAddress( + rootWalletKeys, + outputChain, + 0, + getNetworkForCoinName(coin.name) + ); recipient = { address: outputAddress, @@ -222,7 +228,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { nocks.forEach((nock) => assert.ok(nock.isDone())); - assertSignable(res.txHex, inputScripts, coin.network); + assertSignable(res.txHex, inputScripts, getNetworkForCoinName(coin.name)); }); [true, false].forEach((selfSend) => { @@ -254,7 +260,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { nocks.forEach((nock) => assert.ok(nock.isDone())); - assertSignable(res.txHex, inputScripts, coin.network); + assertSignable(res.txHex, inputScripts, getNetworkForCoinName(coin.name)); }); }); }); diff --git a/modules/abstract-utxo/test/unit/signTransaction.ts b/modules/abstract-utxo/test/unit/signTransaction.ts index 7491ecf82b..f5dfd0e111 100644 --- a/modules/abstract-utxo/test/unit/signTransaction.ts +++ b/modules/abstract-utxo/test/unit/signTransaction.ts @@ -9,7 +9,14 @@ import { common, Triple } from '@bitgo/sdk-core'; import { getReplayProtectionPubkeys, ErrorDeprecatedTxFormat } from '../../src'; import type { Unspent } from '../../src/unspent'; -import { getUtxoWallet, getDefaultWalletKeys, getUtxoCoin, keychainsBase58, defaultBitGo } from './util'; +import { + getUtxoWallet, + getDefaultWalletKeys, + getNetworkForCoinName, + getUtxoCoin, + keychainsBase58, + defaultBitGo, +} from './util'; describe('signTransaction', function () { const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; @@ -21,7 +28,7 @@ describe('signTransaction', function () { const pubs = keychainsBase58.map((v) => v.pub) as Triple; function validatePsbt(txHex: string, targetSigCount: 0 | 1, targetNonceCount?: 1 | 2) { - const psbt = utxolib.bitgo.createPsbtFromHex(txHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(txHex, getNetworkForCoinName(coin.name)); psbt.data.inputs.forEach((input, index) => { const parsed = utxolib.bitgo.parsePsbtInput(input); if (parsed.scriptType === 'taprootKeyPathSpend') { @@ -38,7 +45,7 @@ describe('signTransaction', function () { } function validateTx(txHex: string, unspents: Unspent[], targetSigCount: 0 | 1) { - const tx = utxolib.bitgo.createTransactionFromHex(txHex, coin.network); + const tx = utxolib.bitgo.createTransactionFromHex(txHex, getNetworkForCoinName(coin.name)); unspents.forEach((u, i) => { const sigCount = utxolib.bitgo.getStrictSignatureCount(tx.ins[i]); const expectedSigCount = utxolib.bitgo.isWalletUnspent(u) && !!targetSigCount ? 1 : 0; @@ -56,7 +63,7 @@ describe('signTransaction', function () { const txHex = tx.toHex(); function nockSignPsbt(psbtHex: string): nock.Scope { - const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, getNetworkForCoinName(coin.name)); return nock(bgUrl) .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/signpsbt`, (body) => body.psbt) .reply(200, { psbt: psbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo).toHex() }); @@ -147,7 +154,7 @@ describe('signTransaction', function () { .map((scriptType) => ({ scriptType, value: BigInt(1000) })); const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { p2shP2pkKey: replayProtectionKey, }); @@ -163,7 +170,7 @@ describe('signTransaction', function () { .map((scriptType) => ({ scriptType, value: BigInt(1000) })); const unspentSum = inputs.reduce((prev: bigint, cur) => prev + cur.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { p2shP2pkKey: replayProtectionKey, }); @@ -181,8 +188,16 @@ describe('signTransaction', function () { })); const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.TxnOutput[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const txBuilder = testutil.constructTxnBuilder(inputs, outputs, coin.network, rootWalletKeys, 'unsigned'); - const unspents = inputs.map((v, i) => testutil.toTxnUnspent(v, i, coin.network, rootWalletKeys)); + const txBuilder = testutil.constructTxnBuilder( + inputs, + outputs, + getNetworkForCoinName(coin.name), + rootWalletKeys, + 'unsigned' + ); + const unspents = inputs.map((v, i) => + testutil.toTxnUnspent(v, i, getNetworkForCoinName(coin.name), rootWalletKeys) + ); // Legacy format transactions are now deprecated and should throw ErrorDeprecatedTxFormat await assert.rejects(async () => { @@ -194,7 +209,7 @@ describe('signTransaction', function () { const inputs: testutil.Input[] = [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }]; const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned'); + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned'); await assert.rejects( async () => { diff --git a/modules/abstract-utxo/test/unit/transaction.ts b/modules/abstract-utxo/test/unit/transaction.ts index ed69fa610e..09f8d51201 100644 --- a/modules/abstract-utxo/test/unit/transaction.ts +++ b/modules/abstract-utxo/test/unit/transaction.ts @@ -31,6 +31,7 @@ import { getWalletKeys, defaultBitGo, getMinUtxoCoins, + getNetworkForCoinName, getScriptTypes, } from './util'; @@ -60,7 +61,7 @@ function run( return testutil.toUnspent( { scriptType: t, value: t === 'p2shP2pk' ? BigInt(1000) : BigInt(value) }, index, - coin.network, + getNetworkForCoinName(coin.name), walletKeys ); }); @@ -72,7 +73,7 @@ function run( function getUnspents(): Unspent[] { return inputScripts.map((type, i) => - mockUnspent(coin.network, walletKeys, toTxnInputScriptType(type), i, value) + mockUnspent(getNetworkForCoinName(coin.name), walletKeys, toTxnInputScriptType(type), i, value) ); } @@ -113,7 +114,9 @@ function run( scope = nock(bgUrl) .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/signpsbt`, (body) => body.psbt) .reply(200, (_uri: string, requestBody: unknown) => { - const networkName = utxolib.getNetworkName(coin.network) as fixedScriptWallet.NetworkName; + const networkName = utxolib.getNetworkName( + getNetworkForCoinName(coin.name) + ) as fixedScriptWallet.NetworkName; const reqBytes = Buffer.from((requestBody as { psbt: string }).psbt, 'hex'); const reqPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(reqBytes, networkName); const cosignerWasm = BIP32.fromBase58(cosigner.toBase58()); @@ -166,7 +169,7 @@ function run( const outputs: testutil.Output[] = [ { address: getOutputAddress(getWalletKeys('test')), value: unspentSum - BigInt(1000) }, ]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, walletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), walletKeys, 'unsigned', { p2shP2pkKey: getReplayProtectionPubkeys(coin.name)[0], }); utxolib.bitgo.addXpubsToPsbt(psbt, walletKeys); @@ -177,7 +180,11 @@ function run( const prebuild = txFormat === 'psbt' ? createPrebuildPsbt() - : createPrebuildTransaction(coin.network, getUnspents(), getOutputAddress(walletKeys)); + : createPrebuildTransaction( + getNetworkForCoinName(coin.name), + getUnspents(), + getOutputAddress(walletKeys) + ); const halfSignedUserBitGo = await createHalfSignedTransaction(prebuild, walletKeys.user, walletKeys.bitgo); const fullSignedUserBitGo = @@ -225,7 +232,7 @@ function run( ? undefined : v instanceof utxolib.bitgo.UtxoTransaction ? transactionToObj(v) - : transactionHexToObj(v.txHex, coin.network, amountType) + : transactionHexToObj(v.txHex, getNetworkForCoinName(coin.name), amountType) ) as TransactionObjStages; } @@ -240,7 +247,7 @@ function run( }); function testPsbtValidSignatures(tx: HalfSignedUtxoTransaction, signedBy: BIP32Interface[]) { - const psbt = utxolib.bitgo.createPsbtFromHex(tx.txHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(tx.txHex, getNetworkForCoinName(coin.name)); const unspents = getUnspentsForPsbt(); psbt.data.inputs.forEach((input, index) => { const unspent = unspents[index]; @@ -278,7 +285,7 @@ function run( const transaction = utxolib.bitgo.createTransactionFromBuffer( Buffer.from(tx.txHex, 'hex'), - coin.network, + getNetworkForCoinName(coin.name), { amountType } ); transaction.ins.forEach((input, index) => { diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts index fe19d2a92a..1d7e29d091 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts @@ -4,31 +4,21 @@ import * as utxolib from '@bitgo/utxo-lib'; import { testutil } from '@bitgo/utxo-lib'; import { fixedScriptWallet } from '@bitgo/wasm-utxo'; -import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; import { - explainPsbt, explainPsbtWasm, explainPsbtWasmBigInt, aggregateTransactionExplanations, type TransactionExplanationBigInt, } from '../../../../src/transaction/fixedScript'; -import { getCoinName } from '../../../../src/names'; function describeTransactionWith(acidTest: testutil.AcidTest) { describe(`${acidTest.name}`, function () { - let psbt: utxolib.bitgo.UtxoPsbt; - let psbtBytes: Buffer; let walletXpubs: fixedScriptWallet.RootWalletKeys; let customChangeWalletXpubs: fixedScriptWallet.RootWalletKeys | undefined; let wasmPsbt: fixedScriptWallet.BitGoPsbt; - let refExplanation: TransactionExplanation; before('prepare', function () { - psbt = acidTest.createPsbt(); - const coinName = getCoinName(acidTest.network); - refExplanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, coinName, { - strict: true, - }); - psbtBytes = psbt.toBuffer(); + const psbt = acidTest.createPsbt(); + const psbtBytes = psbt.toBuffer(); const networkName = utxolib.getNetworkName(acidTest.network); assert(networkName); walletXpubs = fixedScriptWallet.RootWalletKeys.from(acidTest.rootWalletKeys); @@ -36,55 +26,19 @@ function describeTransactionWith(acidTest: testutil.AcidTest) { wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbtBytes, networkName); }); - it('should match the expected values for explainPsbt', function () { - // note: `outputs` means external outputs here - assert.strictEqual(refExplanation.outputs.length, 3); - assert.strictEqual(refExplanation.changeOutputs.length, acidTest.outputs.length - 3); - assert.strictEqual(refExplanation.outputAmount, '1800'); - assert.strictEqual(refExplanation.changeOutputs.length, acidTest.outputs.length - 3); - refExplanation.changeOutputs.forEach((change) => { - assert.strictEqual(change.amount, '900'); - assert.strictEqual(typeof change.address, 'string'); - }); - }); - - it('reference implementation should support custom change outputs', function () { - const coinName = getCoinName(acidTest.network); - const customChangeExplanation = explainPsbt( - psbt, - { pubs: acidTest.rootWalletKeys, customChangePubs: acidTest.otherWalletKeys }, - coinName, - { strict: true } - ); - assert.ok(customChangeExplanation.customChangeOutputs); - assert.strictEqual(customChangeExplanation.changeOutputs.length, refExplanation.changeOutputs.length); - assert.strictEqual(customChangeExplanation.outputs.length, refExplanation.outputs.length - 1); - assert.strictEqual(customChangeExplanation.customChangeOutputs.length, 1); - assert.strictEqual(customChangeExplanation.customChangeOutputs[0].amount, '900'); - }); - - it('should match explainPsbtWasm', function () { + it('should return expected outputs from explainPsbtWasm', function () { const wasmExplanation = explainPsbtWasm(wasmPsbt, walletXpubs, { replayProtection: { publicKeys: [acidTest.getReplayProtectionPublicKey()], }, }); - - for (const key of Object.keys(refExplanation)) { - const refValue = refExplanation[key]; - const wasmValue = wasmExplanation[key]; - switch (key) { - case 'displayOrder': - case 'inputSignatures': - case 'signatures': - // these are deprecated fields that we want to get rid of - assert.deepStrictEqual(wasmValue, undefined); - break; - default: - assert.deepStrictEqual(wasmValue, refValue, `mismatch for key ${key}`); - break; - } - } + assert.strictEqual(wasmExplanation.outputs.length, 3); + assert.strictEqual(wasmExplanation.changeOutputs.length, acidTest.outputs.length - 3); + assert.strictEqual(wasmExplanation.outputAmount, '1800'); + wasmExplanation.changeOutputs.forEach((change) => { + assert.strictEqual(change.amount, '900'); + assert.strictEqual(typeof change.address, 'string'); + }); // verify new fields are present and stringified assert.strictEqual(typeof wasmExplanation.inputAmount, 'string'); diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts index 9e527c2e8e..e6c2676073 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts @@ -3,18 +3,14 @@ import assert from 'node:assert/strict'; import * as sinon from 'sinon'; import * as utxolib from '@bitgo/utxo-lib'; import { Wallet, VerificationOptions, ITransactionRecipient, Triple } from '@bitgo/sdk-core'; -import { address as wasmAddress, fixedScriptWallet } from '@bitgo/wasm-utxo'; +import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import { parseTransaction } from '../../../../src/transaction/fixedScript/parseTransaction'; import { ParsedTransaction } from '../../../../src/transaction/types'; import { UtxoWallet } from '../../../../src/wallet'; -import { getUtxoCoin } from '../../util'; -import { explainLegacyTx, explainPsbt, explainPsbtWasm } from '../../../../src/transaction/fixedScript'; -import type { - TransactionExplanation, - ChangeAddressInfo, -} from '../../../../src/transaction/fixedScript/explainTransaction'; -import { getCoinName } from '../../../../src/names'; +import { getCoinNameForNetwork, getUtxoCoin } from '../../util'; +import { explainPsbtWasm } from '../../../../src/transaction/fixedScript'; +import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; import { TransactionPrebuild } from '../../../../src/abstractUtxoCoin'; function getTxParamsFromExplanation( @@ -52,40 +48,14 @@ function getTxParamsFromExplanation( }; } -function getChangeInfoFromPsbt(psbt: utxolib.bitgo.UtxoPsbt): ChangeAddressInfo[] | undefined { - try { - return utxolib.bitgo.findInternalOutputIndices(psbt).map((i) => { - const output = psbt.data.outputs[i]; - const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined; - if (!derivations || derivations.length !== 3) { - throw new Error('expected 3 derivation paths'); - } - const path = derivations[0].path; - const { chain, index } = utxolib.bitgo.getChainAndIndexFromPath(path); - return { - address: wasmAddress.fromOutputScriptWithCoin(psbt.txOutputs[i].script, getCoinName(psbt.network)), - chain, - index, - }; - }); - } catch (e) { - if (e instanceof utxolib.bitgo.ErrorNoMultiSigInputFound) { - return undefined; - } - throw e; - } -} - function describeParseTransactionWith( acidTest: utxolib.testutil.AcidTest, label: string, - backend: 'utxolib' | 'wasm', { txParams, externalCustomChangeAddress = false, expectedExplicitExternalSpendAmount, expectedImplicitExternalSpendAmount, - txFormat = 'psbt', }: { txParams: | { @@ -98,7 +68,6 @@ function describeParseTransactionWith( externalCustomChangeAddress?: boolean; expectedExplicitExternalSpendAmount: bigint; expectedImplicitExternalSpendAmount: bigint; - txFormat?: 'psbt' | 'legacy'; } ) { describe(`${acidTest.name}/${label}`, function () { @@ -108,7 +77,7 @@ function describeParseTransactionWith( let stubExplainTransaction: sinon.SinonStub; before('prepare', async function () { - const coinName = getCoinName(acidTest.network); + const coinName = getCoinNameForNetwork(acidTest.network); coin = getUtxoCoin(coinName); // Create PSBT and explanation @@ -116,37 +85,19 @@ function describeParseTransactionWith( const tx = psbt.getUnsignedTx(); const txHash = tx.getId(); - let explanation: TransactionExplanation; - if (txFormat === 'psbt') { - if (backend === 'utxolib') { - explanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, coinName, { - strict: true, - }); - } else if (backend === 'wasm') { - const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes( - psbt.toBuffer(), - utxolib.getNetworkName(acidTest.network)! - ); - explanation = explainPsbtWasm( - wasmPsbt, - acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple, - { - replayProtection: { - publicKeys: [acidTest.getReplayProtectionPublicKey()], - }, - } - ); - } else { - throw new Error(`Invalid backend: ${backend}`); + const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes( + psbt.toBuffer(), + utxolib.getNetworkName(acidTest.network)! + ); + const explanation: TransactionExplanation = explainPsbtWasm( + wasmPsbt, + acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple, + { + replayProtection: { + publicKeys: [acidTest.getReplayProtectionPublicKey()], + }, } - } else if (txFormat === 'legacy') { - const pubs = acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()); - // Extract change info from PSBT to pass to explainLegacyTx - const changeInfo = getChangeInfoFromPsbt(psbt); - explanation = explainLegacyTx(tx, { pubs, changeInfo }, coinName); - } else { - throw new Error(`Invalid txFormat: ${txFormat}`); - } + ); // Determine txParams let resolvedTxParams; @@ -198,18 +149,9 @@ function describeParseTransactionWith( // Stub explainTransaction to return the explanation without making network calls stubExplainTransaction = sinon.stub(coin, 'explainTransaction').resolves(explanation); - let txPrebuild: TransactionPrebuild; - if (txFormat === 'psbt') { - txPrebuild = { - txHex: psbt.toHex(), - }; - } else if (txFormat === 'legacy') { - txPrebuild = { - txHex: psbt.getUnsignedTx().toHex(), - }; - } else { - throw new Error(`Invalid txFormat: ${txFormat}`); - } + const txPrebuild: TransactionPrebuild = { + txHex: psbt.toHex(), + }; refParsedTransaction = await parseTransaction(coin, { wallet: mockWallet as unknown as UtxoWallet, @@ -260,16 +202,13 @@ function describeParseTransactionWith( }); } -function describeTransaction( - backend: 'utxolib' | 'wasm', - filter: (test: utxolib.testutil.AcidTest) => boolean = () => true -) { - describe(`parseTransaction (${backend})`, function () { +function describeTransaction(filter: (test: utxolib.testutil.AcidTest) => boolean = () => true) { + describe(`parseTransaction`, function () { utxolib.testutil.AcidTest.suite() .filter(filter) .forEach((test) => { // Default case: psbt format, infer recipients from explanation - describeParseTransactionWith(test, 'default', backend, { + describeParseTransactionWith(test, 'default', { txParams: 'inferFromExplanation', expectedExplicitExternalSpendAmount: 1800n, expectedImplicitExternalSpendAmount: 0n, @@ -280,14 +219,7 @@ function describeTransaction( } // extended test suite for bitcoin - describeParseTransactionWith(test, 'legacy', backend, { - txFormat: 'legacy', - txParams: 'inferFromExplanation', - expectedExplicitExternalSpendAmount: 1800n, - expectedImplicitExternalSpendAmount: 0n, - }); - - describeParseTransactionWith(test, 'empty recipients', backend, { + describeParseTransactionWith(test, 'empty recipients', { txParams: { recipients: [], }, @@ -295,7 +227,7 @@ function describeTransaction( expectedImplicitExternalSpendAmount: 1800n, }); - describeParseTransactionWith(test, 'rbf', backend, { + describeParseTransactionWith(test, 'rbf', { txParams: { rbfTxIds: ['PLACEHOLDER'], }, @@ -303,7 +235,7 @@ function describeTransaction( expectedImplicitExternalSpendAmount: 0n, }); - describeParseTransactionWith(test, 'allowExternalChangeAddress', backend, { + describeParseTransactionWith(test, 'allowExternalChangeAddress', { txParams: 'inferFromExplanation', externalCustomChangeAddress: true, expectedExplicitExternalSpendAmount: 1800n, @@ -313,5 +245,4 @@ function describeTransaction( }); } -describeTransaction('utxolib'); -describeTransaction('wasm'); +describeTransaction(); diff --git a/modules/abstract-utxo/test/unit/verifyTransaction.ts b/modules/abstract-utxo/test/unit/verifyTransaction.ts index b7eabebf13..fc81f28f07 100644 --- a/modules/abstract-utxo/test/unit/verifyTransaction.ts +++ b/modules/abstract-utxo/test/unit/verifyTransaction.ts @@ -1,6 +1,5 @@ import assert from 'assert'; -import * as utxolib from '@bitgo/utxo-lib'; import * as sinon from 'sinon'; import { Wallet } from '@bitgo/sdk-core'; @@ -217,10 +216,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -234,7 +229,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should not allow any implicit external outputs if paygo outputs are disallowed', async () => { @@ -284,10 +278,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -302,7 +292,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should work with bigint amounts', async () => { @@ -332,10 +321,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(bigintCoin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await bigintCoin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -350,6 +335,5 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); }); diff --git a/modules/abstract-utxo/test/unit/wallet.ts b/modules/abstract-utxo/test/unit/wallet.ts index 7c9b6223e7..4313a70237 100644 --- a/modules/abstract-utxo/test/unit/wallet.ts +++ b/modules/abstract-utxo/test/unit/wallet.ts @@ -5,7 +5,7 @@ import nock = require('nock'); import * as _ from 'lodash'; import { Wallet, ManageUnspentsOptions, common } from '@bitgo/sdk-core'; -import { defaultBitGo, getDefaultWalletKeys, toKeychainObjects, getUtxoCoin } from './util'; +import { defaultBitGo, getDefaultWalletKeys, getNetworkForCoinName, toKeychainObjects, getUtxoCoin } from './util'; const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; const bitgo = defaultBitGo; @@ -35,7 +35,7 @@ describe('manage unspents', function () { utxoLib.testutil.constructPsbt( [{ scriptType, value: BigInt(1000) }], [{ scriptType, value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ) @@ -74,7 +74,7 @@ describe('manage unspents', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'p2wsh', value: BigInt(1000) }], [{ scriptType: 'p2shP2wsh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' );