From 2d515789c44201f8df4fe3ebc395e9bf89110020 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 12 Nov 2025 14:28:22 +0100 Subject: [PATCH 1/6] feat(abstract-utxo): add psbt-lite to txFormat Add a new type to tx format to support psbt-lite, a more lightweight version of the PSBT format for transaction signing. Issue: BTC-2732 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 7d6da1fea5..4d7f7b5bad 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -84,6 +84,8 @@ import { isDescriptorWalletData } from './descriptor/descriptorWallet'; import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; +type TxFormat = 'legacy' | 'psbt' | 'psbt-lite'; + type UtxoCustomSigningFunction = { (params: { coin: IBaseCoin; @@ -1001,10 +1003,10 @@ export abstract class AbstractUtxoCoin extends BaseCoin { } async getExtraPrebuildParams(buildParams: ExtraPrebuildParamsOptions & { wallet: Wallet }): Promise<{ - txFormat?: 'legacy' | 'psbt'; + txFormat?: TxFormat; changeAddressType?: ScriptType2Of3[] | ScriptType2Of3; }> { - let txFormat = buildParams.txFormat as 'legacy' | 'psbt' | undefined; + let txFormat = buildParams.txFormat as TxFormat | undefined; let changeAddressType = buildParams.changeAddressType as ScriptType2Of3[] | ScriptType2Of3 | undefined; if (this.shouldDefaultToPsbtTxFormat(buildParams)) { From 75da4142ce5b8c315697821140367a718814cf41 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 12 Nov 2025 14:37:13 +0100 Subject: [PATCH 2/6] feat(abstract-utxo): refactor tx format decision into separate method Extract logic to determine transaction format into a dedicated method to improve readability and maintainability. Now using explicit return values instead of relying on a boolean evaluation for PSBT format. Issue: BTC-2732 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 4d7f7b5bad..c61abfe0fb 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -982,14 +982,14 @@ export abstract class AbstractUtxoCoin extends BaseCoin { }; } - private shouldDefaultToPsbtTxFormat(buildParams: ExtraPrebuildParamsOptions & { wallet: Wallet }) { - const walletFlagMusigKp = buildParams.wallet.flag('musigKp') === 'true'; - const isHotWallet = buildParams.wallet.type() === 'hot'; + private getTxFormat(wallet: Wallet, requestedTxFormat: unknown): TxFormat | undefined { + const walletFlagMusigKp = wallet.flag('musigKp') === 'true'; + const isHotWallet = wallet.type() === 'hot'; // if not txFormat is already specified figure out if we should default to psbt format - return ( - buildParams.txFormat === undefined && - (buildParams.wallet.subType() === 'distributedCustody' || + if ( + requestedTxFormat === undefined && + (wallet.subType() === 'distributedCustody' || // default to testnet for all utxo coins except zcash (isTestnet(this.network) && // FIXME(BTC-1322): fix zcash PSBT support @@ -999,20 +999,19 @@ export abstract class AbstractUtxoCoin extends BaseCoin { (isMainnet(this.network) && getMainnet(this.network) === utxolib.networks.bitcoin && isHotWallet) || // default to psbt if it has the wallet flag walletFlagMusigKp) - ); + ) { + return 'psbt'; + } + + return requestedTxFormat as TxFormat; } async getExtraPrebuildParams(buildParams: ExtraPrebuildParamsOptions & { wallet: Wallet }): Promise<{ txFormat?: TxFormat; changeAddressType?: ScriptType2Of3[] | ScriptType2Of3; }> { - let txFormat = buildParams.txFormat as TxFormat | undefined; let changeAddressType = buildParams.changeAddressType as ScriptType2Of3[] | ScriptType2Of3 | undefined; - if (this.shouldDefaultToPsbtTxFormat(buildParams)) { - txFormat = 'psbt'; - } - // if the addressType is not specified, we need to default to p2trMusig2 for testnet hot wallets for staged rollout of p2trMusig2 if ( buildParams.addressType === undefined && // addressType is deprecated and replaced by `changeAddress` @@ -1024,7 +1023,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin { } return { - txFormat, + txFormat: this.getTxFormat(buildParams.wallet, buildParams.txFormat), changeAddressType, }; } From c29bc87534b663dcf6b07102eda231f0fd112cc7 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 12 Nov 2025 14:39:31 +0100 Subject: [PATCH 3/6] feat(abstract-utxo): validate txFormat param on testnet networks Validate that the txFormat parameter is one of the allowed values for testnet networks to prevent invalid inputs. Added new utility function and error class to handle validation. Issue: BTC-2732 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index c61abfe0fb..05895000a3 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -86,6 +86,16 @@ import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; type TxFormat = 'legacy' | 'psbt' | 'psbt-lite'; +function isTxFormat(txFormat: unknown): txFormat is TxFormat { + return txFormat === 'legacy' || txFormat === 'psbt' || txFormat === 'psbt-lite'; +} + +class ErrorInvalidTxFormat extends Error { + constructor(txFormat: unknown) { + super(`Invalid txFormat: ${txFormat}. Must be one of: legacy, psbt, psbt-lite`); + } +} + type UtxoCustomSigningFunction = { (params: { coin: IBaseCoin; @@ -983,6 +993,12 @@ export abstract class AbstractUtxoCoin extends BaseCoin { } private getTxFormat(wallet: Wallet, requestedTxFormat: unknown): TxFormat | undefined { + if (utxolib.isTestnet(this.network)) { + if (requestedTxFormat !== undefined && !isTxFormat(requestedTxFormat)) { + throw new ErrorInvalidTxFormat(requestedTxFormat); + } + } + const walletFlagMusigKp = wallet.flag('musigKp') === 'true'; const isHotWallet = wallet.type() === 'hot'; From 836c26baccc2f0dbbf3f2f1317e56970e4b53c0d Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 12 Nov 2025 14:41:24 +0100 Subject: [PATCH 4/6] feat(abstract-utxo): always return txFormat if provided Refactor `getTxFormat` method to prioritize explicitly requested txFormat when provided. Default to PSBT for certain wallet types only when no format is specified. Issue: BTC-2732 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 05895000a3..5d31ed0b5d 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -999,27 +999,31 @@ export abstract class AbstractUtxoCoin extends BaseCoin { } } + if (requestedTxFormat !== undefined) { + return requestedTxFormat as TxFormat; + } + const walletFlagMusigKp = wallet.flag('musigKp') === 'true'; const isHotWallet = wallet.type() === 'hot'; // if not txFormat is already specified figure out if we should default to psbt format if ( - requestedTxFormat === undefined && - (wallet.subType() === 'distributedCustody' || - // default to testnet for all utxo coins except zcash - (isTestnet(this.network) && - // FIXME(BTC-1322): fix zcash PSBT support - getMainnet(this.network) !== utxolib.networks.zcash && - isHotWallet) || - // if mainnet, only default to psbt for btc hot wallets - (isMainnet(this.network) && getMainnet(this.network) === utxolib.networks.bitcoin && isHotWallet) || - // default to psbt if it has the wallet flag - walletFlagMusigKp) + wallet.subType() === 'distributedCustody' || + // default to testnet for all utxo coins except zcash + (isTestnet(this.network) && + // FIXME(BTC-1322): fix zcash PSBT support + getMainnet(this.network) !== utxolib.networks.zcash && + isHotWallet) || + // if mainnet, only default to psbt for btc hot wallets + (isMainnet(this.network) && getMainnet(this.network) === utxolib.networks.bitcoin && isHotWallet) || + // default to psbt if it has the wallet flag + walletFlagMusigKp ) { return 'psbt'; } - return requestedTxFormat as TxFormat; + // let API decide + return undefined; } async getExtraPrebuildParams(buildParams: ExtraPrebuildParamsOptions & { wallet: Wallet }): Promise<{ From 9e82ee2e9fd2251fd57ab6750fa22d32844bfa88 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 12 Nov 2025 14:44:23 +0100 Subject: [PATCH 5/6] feat(abstract-utxo): force PSBT format for Bitcoin Testnet hot wallets Forces PSBT format for Bitcoin Testnet hot wallets and explicitly handles Zcash separately. This change maintains legacy format for Zcash while ensuring all other UTXO-based coins on testnet use PSBT format. Issue: BTC-2732 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 5d31ed0b5d..c584e9d148 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -1003,6 +1003,11 @@ export abstract class AbstractUtxoCoin extends BaseCoin { return requestedTxFormat as TxFormat; } + if (utxolib.getMainnet(this.network) === utxolib.networks.zcash) { + // FIXME(BTC-1322): fix zcash PSBT support + return 'legacy'; + } + const walletFlagMusigKp = wallet.flag('musigKp') === 'true'; const isHotWallet = wallet.type() === 'hot'; @@ -1010,10 +1015,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin { if ( wallet.subType() === 'distributedCustody' || // default to testnet for all utxo coins except zcash - (isTestnet(this.network) && - // FIXME(BTC-1322): fix zcash PSBT support - getMainnet(this.network) !== utxolib.networks.zcash && - isHotWallet) || + (isTestnet(this.network) && isHotWallet) || // if mainnet, only default to psbt for btc hot wallets (isMainnet(this.network) && getMainnet(this.network) === utxolib.networks.bitcoin && isHotWallet) || // default to psbt if it has the wallet flag From 7410da352563a98c72bd2c1c16b5c20a6c8abaeb Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 12 Nov 2025 14:50:16 +0100 Subject: [PATCH 6/6] feat(abstract-utxo): force PSBT for testnet bitcoin Force PSBT for testnet Bitcoin by rejecting 'legacy' format and defaulting to 'psbt-lite' for Bitcoin testnet networks. Add new error class for deprecated transaction formats. Issue: BTC-2732 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index c584e9d148..63e1a5dd99 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -96,6 +96,12 @@ class ErrorInvalidTxFormat extends Error { } } +class ErrorDeprecatedTxFormat extends Error { + constructor(txFormat: unknown) { + super(`Deprecated txFormat: ${txFormat} for this network`); + } +} + type UtxoCustomSigningFunction = { (params: { coin: IBaseCoin; @@ -997,6 +1003,14 @@ export abstract class AbstractUtxoCoin extends BaseCoin { if (requestedTxFormat !== undefined && !isTxFormat(requestedTxFormat)) { throw new ErrorInvalidTxFormat(requestedTxFormat); } + + if (utxolib.getMainnet(this.network) === utxolib.networks.bitcoin) { + if (requestedTxFormat === 'legacy') { + throw new ErrorDeprecatedTxFormat(requestedTxFormat); + } + + return 'psbt-lite'; + } } if (requestedTxFormat !== undefined) {