From 02b1d4da72a466af04cbe47d1798f17e3b75d821 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Wed, 5 Aug 2026 15:00:28 -0400 Subject: [PATCH] feat: add needsXpubsForColdSigning capability method Adds a coin-level capability so callers can check whether a built transaction requires xpubs/derivation paths for cold signing instead of sniffing tx format themselves. PSBT transactions carry BIP-32 derivation info inline, so xpubs are only needed for legacy UTXO txs. TICKET: WCN-1799 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 13 ++++++ .../sdk-core/src/bitgo/baseCoin/baseCoin.ts | 42 ++++++++++++++++++- .../sdk-core/src/bitgo/baseCoin/iBaseCoin.ts | 18 +++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 8c9430ecf4..e4fb3c2333 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -7,6 +7,7 @@ import { AddressCoinSpecific, BaseCoin, BitGoBase, + ColdTransactionPrebuild, CreateAddressFormat, ExtraPrebuildParamsOptions, HalfSignedUtxoTransaction, @@ -17,6 +18,7 @@ import { IRequestTracer, isTriple, IWallet, + Keychain, KeychainsTriplet, KeyIndices, MismatchedRecipient, @@ -1068,6 +1070,17 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici return false; } + prepareColdTransaction( + txPrebuild: T, + keychains: Triple + ): T & ColdTransactionPrebuild { + // PSBTs already carry BIP-32 derivation info inline, so offline signers do not need the extra information + if (typeof txPrebuild.txHex === 'string' && hasPsbtMagic(stringToBufferTryFormats(txPrebuild.txHex, ['hex']))) { + return txPrebuild as T & ColdTransactionPrebuild; + } + return super.prepareColdTransaction(txPrebuild, keychains); + } + getRecoveryProvider(apiToken?: string): RecoveryProvider { return forCoin(this.getChain(), apiToken); } diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index 0f5a3c142b..5a6f027f4d 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -13,7 +13,7 @@ import { signMessage } from '../bip32util'; import { NotImplementedError } from '../../account-lib'; import { BitGoBase } from '../bitgoBase'; import { Enterprises } from '../enterprise'; -import { Keychains, KeyIndices } from '../keychain'; +import { ApiKeyShare, Keychain, Keychains, KeyIndices } from '../keychain'; import { Markets } from '../market'; import { PendingApprovals } from '../pendingApproval'; import { IWallet, Wallet, Wallets } from '../wallet'; @@ -22,6 +22,7 @@ import { BaseBroadcastTransactionOptions, BaseBroadcastTransactionResult, BuildNftTransferDataOptions, + ColdTransactionPrebuild, DeriveKeyWithSeedOptions, ExtraPrebuildParamsOptions, FeeEstimateOptions, @@ -57,6 +58,7 @@ import { PopulatedIntent, PrebuildTransactionWithIntentOptions, TokenTransferRecipientParams, + Triple, } from '../utils'; export abstract class BaseCoin implements IBaseCoin { @@ -244,6 +246,44 @@ export abstract class BaseCoin implements IBaseCoin { return false; } + /** + * Prepare a built transaction for cold (offline) signing by attaching the + * xpubs / xpub derivation paths the offline signer (OVC) needs in order to + * sign it. Coins whose tx format already carries this information inline + * (e.g. PSBT) can override this to skip attaching it. + * @param txPrebuild The built transaction JSON returned by the platform + * @param keychains The user/backup/bitgo keychains for this wallet, in that order + * @returns The txPrebuild JSON, augmented with `pubs` and `xpubsWithDerivationPath` + */ + prepareColdTransaction( + txPrebuild: T, + keychains: Triple + ): T & ColdTransactionPrebuild { + const [user, backup, bitgo] = keychains; + const sources: Array<[ApiKeyShare['from'], Keychain]> = [ + ['user', user], + ['backup', backup], + ['bitgo', bitgo], + ]; + const pubs: string[] = []; + const xpubsWithDerivationPath: NonNullable = {}; + for (const [source, keychain] of sources) { + // MPC keychains only have a commonKeychain, not a pub - skip those. + if (keychain.pub) { + pubs.push(keychain.pub); + xpubsWithDerivationPath[source] = { + xpub: keychain.pub, + derivedFromParentWithSeed: keychain.derivedFromParentWithSeed, + }; + } + } + return { + ...txPrebuild, + pubs, + xpubsWithDerivationPath, + }; + } + /** * Returns the factor between the base unit and its smallest subdivison * @return {number} diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index 097a4bd04d..ccd5a4adf4 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -2,7 +2,7 @@ import { BaseTokenConfig, BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; import BigNumber from 'bignumber.js'; import { IRequestTracer } from '../../api'; import { IEnterprises } from '../enterprise'; -import { IKeychains, Keychain } from '../keychain'; +import { ApiKeyShare, IKeychains, Keychain } from '../keychain'; import { IMarkets } from '../market'; import { IPendingApprovals } from '../pendingApproval'; import { InitiateRecoveryOptions } from '../recovery'; @@ -14,7 +14,7 @@ import { TokenEnablement } from '@bitgo/public-types'; import { Hash } from 'crypto'; import { TransactionType } from '../../account-lib'; import { IInscriptionBuilder } from '../inscriptionBuilder'; -import { MessageStandardType, MPCTx, PopulatedIntent, TokenTransferRecipientParams, TokenType } from '../utils'; +import { MessageStandardType, MPCTx, PopulatedIntent, TokenTransferRecipientParams, TokenType, Triple } from '../utils'; import type { SignableTransaction } from '../utils/tss/baseTypes'; import { IWebhooks } from '../webhook/iWebhooks'; @@ -442,6 +442,16 @@ export interface TransactionPrebuild extends BaseSignable { txInfo?: unknown; } +export interface XpubWithDerivationPath { + xpub: string; + derivedFromParentWithSeed?: string; +} + +export interface ColdTransactionPrebuild { + pubs?: string[]; + xpubsWithDerivationPath?: Partial>; +} + export interface Message extends BaseSignable { messageRaw: string; messageEncoded?: string; @@ -672,6 +682,10 @@ export interface IBaseCoin { supportsDeriveKeyWithSeed(): boolean; isEVM(): boolean; supportsBlsDkg(): boolean; + prepareColdTransaction( + txPrebuild: T, + keychains: Triple + ): T & ColdTransactionPrebuild; getBaseFactor(): number | string; baseUnitsToBigUnits(baseUnits: string | number): string; bigUnitsToBaseUnits(bigUnits: string | number): string;