From 7bf0b12993085eb1dd1ea322f87c8d4b27e7b885 Mon Sep 17 00:00:00 2001 From: Marzooqa Kather Date: Fri, 8 May 2026 12:17:15 +0000 Subject: [PATCH] fix(sdk-core): route EdDSA MPCv2 hot wallets to full apiVersion - getTxRequestApiVersion: add || wallet.multisigTypeVersion() === 'MPCv2' to the 'full' branch so EdDSA MPCv2 hot wallets are not incorrectly routed to 'lite', which causes signRequestBase to fail with "Missing signableHex in unsignedTx" at runtime. - validateTxRequestApiVersion: merge ECDSA and MPCv2 check so that passing apiVersion: 'lite' on any MPCv2 wallet throws immediately. - baseTSSUtils.supportedTxRequestVersions: return ['full'] (not ['lite', 'full']) for EdDSA MPCv2 hot wallets; v1 hot wallets unchanged. - Tests: add multisigTypeVersion to all txRequest wallet stubs; add two new cases (MPCv2 throws on 'lite', MPCv2 defaults to 'full'); add EddsaMPCv2Utils.supportedTxRequestVersions test for MPCv2 hot wallet returning ['full']. Ticket: WCI-156 Session-Id: ac2678cb-270b-48ab-851f-7787f697cba9 Task-Id: 77a26d69-dcc5-43cf-b339-dace5e08c890 --- .../test/v2/unit/internal/tssUtils/eddsa.ts | 16 +++- .../src/bitgo/utils/tss/baseTSSUtils.ts | 3 + modules/sdk-core/src/bitgo/utils/txRequest.ts | 7 +- .../test/unit/bitgo/utils/txRequest.ts | 74 +++++++++++++++++++ 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index 61dfb4641e..d3dea3383b 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -15,6 +15,7 @@ import { common, createSharedDataProof, Ed25519BIP32, + EDDSAUtils, Eddsa, EncryptedSignerShareType, ExchangeCommitmentResponse, @@ -1130,11 +1131,24 @@ describe('TSS Utils:', async function () { coldWalletTssUtils.supportedTxRequestVersions().should.deepEqual(['full']); }); it('should return full and lite for hot wallets', async function () { - const hotWallet = new Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'hot' }); + const hotWallet = new Wallet(bitgo, baseCoin, { + multisigType: 'tss', + multisigTypeVersion: undefined, + type: 'hot', + }); const hotTssUtils = new TssUtils(bitgo, baseCoin, hotWallet); const supportedTxRequestVersions = hotTssUtils.supportedTxRequestVersions(); supportedTxRequestVersions.should.deepEqual(['lite', 'full']); }); + it('should return only full for hot MPCv2 wallets', function () { + const hotMPCv2Wallet = new Wallet(bitgo, baseCoin, { + multisigType: 'tss', + multisigTypeVersion: 'MPCv2', + type: 'hot', + }); + const mpcv2TssUtils = new EDDSAUtils.EddsaMPCv2Utils(bitgo, baseCoin, hotMPCv2Wallet); + mpcv2TssUtils.supportedTxRequestVersions().should.deepEqual(['full']); + }); it('should return empty for trading wallets', function () { const tradingWallets = new Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'trading' }); const tradingWalletTssUtils = new TssUtils(bitgo, baseCoin, tradingWallets); diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index 199a96b568..ab26b8e2bc 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -618,6 +618,9 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil } else if (this._wallet.baseCoin.getMPCAlgorithm() === 'ecdsa') { return ['full']; } else if (this._wallet.baseCoin.getMPCAlgorithm() === 'eddsa' && this._wallet.type() === 'hot') { + if (this._wallet.multisigTypeVersion() === 'MPCv2') { + return ['full']; + } return ['lite', 'full']; } else { return ['full']; diff --git a/modules/sdk-core/src/bitgo/utils/txRequest.ts b/modules/sdk-core/src/bitgo/utils/txRequest.ts index d3ad6cf4a7..d7999ffe0a 100644 --- a/modules/sdk-core/src/bitgo/utils/txRequest.ts +++ b/modules/sdk-core/src/bitgo/utils/txRequest.ts @@ -7,8 +7,9 @@ export function validateTxRequestApiVersion(wallet: IWallet, requestedApiVersion return; } if (wallet.baseCoin.getMPCAlgorithm() === 'ecdsa') { - // ecdsa wallets can only use full, even if they are hot wallets assert(requestedApiVersion === 'full', 'For ECDSA tss wallets, parameter `apiVersion` must be `full`.'); + } else if (wallet.multisigTypeVersion() === 'MPCv2') { + assert(requestedApiVersion === 'full', 'For EdDSA MPCv2 tss wallets, parameter `apiVersion` must be `full`.'); } else if (wallet.type() !== 'hot') { // all other cases should use full! assert( @@ -30,10 +31,10 @@ export function getTxRequestApiVersion(wallet: IWallet, requestedApiVersion?: Ap validateTxRequestApiVersion(wallet, requestedApiVersion); return requestedApiVersion; } - if (wallet.baseCoin.getMPCAlgorithm() === 'ecdsa') { + if (wallet.baseCoin.getMPCAlgorithm() === 'ecdsa' || wallet.multisigTypeVersion() === 'MPCv2') { return 'full'; } else if (wallet.type() === 'hot') { - // default to lite for hot eddsa tss wallets + // default to lite for hot eddsa tss wallets (v1 only) return 'lite'; } else { // default to full for all other wallet types diff --git a/modules/sdk-core/test/unit/bitgo/utils/txRequest.ts b/modules/sdk-core/test/unit/bitgo/utils/txRequest.ts index 9e3cf292ed..61612136f3 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/txRequest.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/txRequest.ts @@ -10,6 +10,7 @@ describe('txRequest utils', () => { baseCoin: { getMPCAlgorithm: () => 'ecdsa' }, type: () => 'hot', multisigType: () => 'tss', + multisigTypeVersion: () => undefined, } as any as IWallet, requestedApiVersion: 'lite', expectedApiVersion: '', @@ -20,6 +21,7 @@ describe('txRequest utils', () => { baseCoin: { getMPCAlgorithm: () => 'eddsa' }, type: () => 'cold', multisigType: () => 'tss', + multisigTypeVersion: () => undefined, } as any as IWallet, requestedApiVersion: 'lite', expectedApiVersion: '', @@ -30,17 +32,41 @@ describe('txRequest utils', () => { baseCoin: { getMPCAlgorithm: () => 'eddsa' }, type: () => 'hot', multisigType: () => 'tss', + multisigTypeVersion: () => undefined, } as any as IWallet, requestedApiVersion: undefined, expectedApiVersion: 'lite', expectedErrorMessage: '', }, + { + wallet: { + baseCoin: { getMPCAlgorithm: () => 'eddsa' }, + type: () => 'hot', + multisigType: () => 'tss', + multisigTypeVersion: () => 'MPCv2', + } as any as IWallet, + requestedApiVersion: 'lite' as ApiVersion, + expectedApiVersion: '', + expectedErrorMessage: 'For EdDSA MPCv2 tss wallets, parameter `apiVersion` must be `full`.', + }, + { + wallet: { + baseCoin: { getMPCAlgorithm: () => 'eddsa' }, + type: () => 'hot', + multisigType: () => 'tss', + multisigTypeVersion: () => 'MPCv2', + } as any as IWallet, + requestedApiVersion: undefined, + expectedApiVersion: 'full', + expectedErrorMessage: '', + }, ...['hot', 'cold', 'custodial', 'backing'].map((walletType) => { return { wallet: { baseCoin: { getMPCAlgorithm: () => 'ecdsa' }, type: () => walletType, multisigType: () => 'tss', + multisigTypeVersion: () => undefined, } as any as IWallet, requestedApiVersion: 'full', expectedApiVersion: 'full', @@ -54,6 +80,7 @@ describe('txRequest utils', () => { baseCoin: { getMPCAlgorithm: () => 'ecdsa' }, type: () => walletType, multisigType: () => 'tss', + multisigTypeVersion: () => undefined, } as any as IWallet, requestedApiVersion: undefined, expectedApiVersion: 'full', @@ -67,6 +94,7 @@ describe('txRequest utils', () => { baseCoin: { getMPCAlgorithm: () => 'eddsa' }, type: () => walletType, multisigType: () => 'tss', + multisigTypeVersion: () => undefined, } as any as IWallet, requestedApiVersion: 'full', expectedApiVersion: 'full', @@ -80,6 +108,7 @@ describe('txRequest utils', () => { baseCoin: { getMPCAlgorithm: () => 'eddsa' }, type: () => walletType, multisigType: () => 'tss', + multisigTypeVersion: () => undefined, } as any as IWallet, requestedApiVersion: undefined, expectedApiVersion: 'full', @@ -87,6 +116,51 @@ describe('txRequest utils', () => { shouldThrow: false, }; }), + // EdDSA MPCv2: all wallet types + 'full' explicitly → returns 'full' + ...['hot', 'cold', 'custodial', 'backing'].map((walletType) => { + return { + wallet: { + baseCoin: { getMPCAlgorithm: () => 'eddsa' }, + type: () => walletType, + multisigType: () => 'tss', + multisigTypeVersion: () => 'MPCv2', + } as any as IWallet, + requestedApiVersion: 'full' as ApiVersion, + expectedApiVersion: 'full', + expectedErrorMessage: '', + shouldThrow: false, + }; + }), + // EdDSA MPCv2: non-hot wallet types + undefined → defaults to 'full' + ...['cold', 'custodial', 'backing'].map((walletType) => { + return { + wallet: { + baseCoin: { getMPCAlgorithm: () => 'eddsa' }, + type: () => walletType, + multisigType: () => 'tss', + multisigTypeVersion: () => 'MPCv2', + } as any as IWallet, + requestedApiVersion: undefined, + expectedApiVersion: 'full', + expectedErrorMessage: '', + shouldThrow: false, + }; + }), + // EdDSA MPCv2: non-hot wallet types + 'lite' → throws EdDSA MPCv2 error + ...['cold', 'custodial', 'backing'].map((walletType) => { + return { + wallet: { + baseCoin: { getMPCAlgorithm: () => 'eddsa' }, + type: () => walletType, + multisigType: () => 'tss', + multisigTypeVersion: () => 'MPCv2', + } as any as IWallet, + requestedApiVersion: 'lite' as ApiVersion, + expectedApiVersion: '', + expectedErrorMessage: 'For EdDSA MPCv2 tss wallets, parameter `apiVersion` must be `full`.', + shouldThrow: true, + }; + }), ]; testCases.forEach((testCase) => {