From ee66fb5c436b6202fcd923771fb2ab80c37dafcc Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Wed, 26 Nov 2025 10:09:06 -0500 Subject: [PATCH 1/4] fix: pass wallet version during creation to determine if onchain or tss TICKET: WP-6461 --- modules/express/src/clientRoutes.ts | 3 ++- modules/sdk-core/src/bitgo/wallet/wallet.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 4a8ac2f330..81665da7f2 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -28,6 +28,7 @@ import { TssEcdsaStep1ReturnMessage, TssEcdsaStep2ReturnMessage, UnsupportedCoinError, + VerifyAddressOptions, Wallet, } from '@bitgo/sdk-core'; import { BitGo, BitGoOptions, Coin, CustomSigningFunction, SignedTransaction, SignedTransactionRequest } from 'bitgo'; @@ -670,7 +671,7 @@ export async function handleV2IsWalletAddress( const bitgo = req.bitgo; const coin = bitgo.coin(req.decoded.coin); const wallet = await coin.wallets().get({ id: req.decoded.id }); - return await wallet.baseCoin.isWalletAddress(req.decoded as any); + return await wallet.baseCoin.isWalletAddress(req.decoded as VerifyAddressOptions); } /** diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index ed0670754d..61ae23a458 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -1380,7 +1380,10 @@ export class Wallet implements IWallet { newAddress.baseAddress = baseAddress ?? _.get(this._wallet, 'coinSpecific.baseAddress'); newAddress.format = addressParams.format; - const verificationData: VerifyAddressOptions = _.merge({}, newAddress, { rootAddress }); + const verificationData: VerifyAddressOptions = _.merge({}, newAddress, { + rootAddress, + walletVersion: _.get(this._wallet, 'coinSpecific.walletVersion'), + }); if (verificationData.error) { throw new AddressGenerationError(verificationData.error); From 48b83ab6ac8de3ddbf10bd9b2e3e8ed0821b9f0e Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Wed, 26 Nov 2025 10:13:17 -0500 Subject: [PATCH 2/4] refactor: remove unneeded changes TICKET: WP-6461 --- modules/express/src/clientRoutes.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 81665da7f2..4a8ac2f330 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -28,7 +28,6 @@ import { TssEcdsaStep1ReturnMessage, TssEcdsaStep2ReturnMessage, UnsupportedCoinError, - VerifyAddressOptions, Wallet, } from '@bitgo/sdk-core'; import { BitGo, BitGoOptions, Coin, CustomSigningFunction, SignedTransaction, SignedTransactionRequest } from 'bitgo'; @@ -671,7 +670,7 @@ export async function handleV2IsWalletAddress( const bitgo = req.bitgo; const coin = bitgo.coin(req.decoded.coin); const wallet = await coin.wallets().get({ id: req.decoded.id }); - return await wallet.baseCoin.isWalletAddress(req.decoded as VerifyAddressOptions); + return await wallet.baseCoin.isWalletAddress(req.decoded as any); } /** From 113cb89e1e6acca88d69a84dae4515ac4f038e27 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Wed, 26 Nov 2025 10:51:25 -0500 Subject: [PATCH 3/4] test: add tests for wallet creation TICKET: WP-6461 --- modules/sdk-coin-eth/test/unit/eth.ts | 216 ++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/modules/sdk-coin-eth/test/unit/eth.ts b/modules/sdk-coin-eth/test/unit/eth.ts index b9ee890f23..2514aaca4f 100644 --- a/modules/sdk-coin-eth/test/unit/eth.ts +++ b/modules/sdk-coin-eth/test/unit/eth.ts @@ -1299,6 +1299,222 @@ describe('ETH:', function () { }); }); + describe('Address Creation', function () { + it('should pass walletVersion 6 to isWalletAddress during address creation', async function () { + const bgUrl = common.Environments[bitgo.getEnv()].uri; + const ethCoin = bitgo.coin('hteth') as Hteth; + const walletDataV6 = { + id: '598f606cd8fc24710d2ebadb1d9459bb', + coinSpecific: { + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + walletVersion: 6, + }, + coin: 'hteth', + keys: [ + '598f606cd8fc24710d2ebad89dce86c2', + '598f606cc8e43aef09fcb785221d9dd2', + '5935d59cf660764331bafcade1855fd7', + ], + receiveAddress: { + address: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + }, + }; + const ethWalletV6 = new Wallet(bitgo, ethCoin, walletDataV6); + const isWalletAddressSpy = sinon.spy(ethCoin, 'isWalletAddress'); + + // Mock keychain requests + nock(bgUrl).get(`/api/v2/hteth/key/598f606cd8fc24710d2ebad89dce86c2`).reply(200, { + id: '598f606cd8fc24710d2ebad89dce86c2', + pub: 'xpub661MyMwAqRbcFXDcWD2vxuebcT1ZpTF4Vke6qmMW8yzddwNYpAPjvYEEL5jLfyYXW2fuxtAxY8TgjPUJLcf1C8qz9N6VgZxArKX4EwB8rH5', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + source: 'user', + type: 'tss', + }); + nock(bgUrl).get(`/api/v2/hteth/key/598f606cc8e43aef09fcb785221d9dd2`).reply(200, { + id: '598f606cc8e43aef09fcb785221d9dd2', + pub: 'xpub661MyMwAqRbcGhSaXikpuTC9KU88Xx9LrjKSw1JKsvXNgabpTdgjy7LSovh9ZHhcqhAHQu7uthu7FguNGdcC4aXTKK5gqTcPe4WvLYRbCSG', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + source: 'backup', + type: 'tss', + }); + nock(bgUrl).get(`/api/v2/hteth/key/5935d59cf660764331bafcade1855fd7`).reply(200, { + id: '5935d59cf660764331bafcade1855fd7', + pub: 'xpub661MyMwAqRbcFsXShW8R3hJsHNTYTUwzcejnLkY7KCtaJbDqcGkcBF99BrEJSjNZHeHveiYUrsAdwnjUMGwpgmEbiKcZWRuVA9HxnRaA3r3', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + source: 'bitgo', + type: 'tss', + }); + + // Mock address creation API + nock(bgUrl) + .post(`/api/v2/hteth/wallet/${ethWalletV6.id()}/address`) + .reply(200, { + id: '638a48c6c3dba40007a3497fa49a080c', + address: '0xc012041dac143a59fa491db3a2b67b69bd78b685', + chain: 0, + index: 0, + coin: 'hteth', + wallet: ethWalletV6.id(), + coinSpecific: { + forwarderVersion: 4, + salt: '0x0', + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + }); + + await ethWalletV6.createAddress({ chain: 0 }); + + isWalletAddressSpy.calledOnce.should.be.true(); + const calledParams = isWalletAddressSpy.firstCall.args[0]; + calledParams.should.have.property('walletVersion', 6); + }); + + it('should pass walletVersion 5 to isWalletAddress during address creation', async function () { + const bgUrl = common.Environments[bitgo.getEnv()].uri; + const ethCoin = bitgo.coin('hteth') as Hteth; + const walletDataV5 = { + id: '598f606cd8fc24710d2ebadb1d9459bb', + coinSpecific: { + baseAddress: '0xf1e3d30798acdf3a12fa5beb5fad8efb23d5be11', + walletVersion: 5, + }, + coin: 'hteth', + keys: [ + '598f606cd8fc24710d2ebad89dce86c2', + '598f606cc8e43aef09fcb785221d9dd2', + '5935d59cf660764331bafcade1855fd7', + ], + receiveAddress: { + address: '0xf1e3d30798acdf3a12fa5beb5fad8efb23d5be11', + }, + }; + const ethWalletV5 = new Wallet(bitgo, ethCoin, walletDataV5); + const isWalletAddressSpy = sinon.spy(ethCoin, 'isWalletAddress'); + + // Mock keychain requests + nock(bgUrl).get(`/api/v2/hteth/key/598f606cd8fc24710d2ebad89dce86c2`).reply(200, { + id: '598f606cd8fc24710d2ebad89dce86c2', + pub: 'xpub661MyMwAqRbcFXDcWD2vxuebcT1ZpTF4Vke6qmMW8yzddwNYpAPjvYEEL5jLfyYXW2fuxtAxY8TgjPUJLcf1C8qz9N6VgZxArKX4EwB8rH5', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + source: 'user', + type: 'tss', + }); + nock(bgUrl).get(`/api/v2/hteth/key/598f606cc8e43aef09fcb785221d9dd2`).reply(200, { + id: '598f606cc8e43aef09fcb785221d9dd2', + pub: 'xpub661MyMwAqRbcGhSaXikpuTC9KU88Xx9LrjKSw1JKsvXNgabpTdgjy7LSovh9ZHhcqhAHQu7uthu7FguNGdcC4aXTKK5gqTcPe4WvLYRbCSG', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + source: 'backup', + type: 'tss', + }); + nock(bgUrl).get(`/api/v2/hteth/key/5935d59cf660764331bafcade1855fd7`).reply(200, { + id: '5935d59cf660764331bafcade1855fd7', + pub: 'xpub661MyMwAqRbcFsXShW8R3hJsHNTYTUwzcejnLkY7KCtaJbDqcGkcBF99BrEJSjNZHeHveiYUrsAdwnjUMGwpgmEbiKcZWRuVA9HxnRaA3r3', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + source: 'bitgo', + type: 'tss', + }); + + // Mock address creation API + nock(bgUrl) + .post(`/api/v2/hteth/wallet/${ethWalletV5.id()}/address`) + .reply(200, { + id: '638a48c6c3dba40007a3497fa49a080c', + address: '0xd63b5e2b8d1b4fba3625460508900bf2a0499a4d', + chain: 0, + index: 117, + coin: 'hteth', + wallet: ethWalletV5.id(), + coinSpecific: { + forwarderVersion: 4, + salt: '0x75', + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + }); + + await ethWalletV5.createAddress({ chain: 0 }); + + // Verify isWalletAddress was called with walletVersion 5 + isWalletAddressSpy.calledOnce.should.be.true(); + const calledParams = isWalletAddressSpy.firstCall.args[0]; + calledParams.should.have.property('walletVersion', 5); + }); + + it('should pass walletVersion 2 to isWalletAddress during address creation', async function () { + const bgUrl = common.Environments[bitgo.getEnv()].uri; + const ethCoin = bitgo.coin('hteth') as Hteth; + const walletDataV2 = { + id: '598f606cd8fc24710d2ebadb1d9459bb', + coinSpecific: { + baseAddress: '0xdc485da076ed4a2b19584e9a1fdbb974f89b60f4', + walletVersion: 2, + }, + coin: 'hteth', + keys: [ + '598f606cd8fc24710d2ebad89dce86c2', + '598f606cc8e43aef09fcb785221d9dd2', + '5935d59cf660764331bafcade1855fd7', + ], + receiveAddress: { + address: '0xdc485da076ed4a2b19584e9a1fdbb974f89b60f4', + }, + }; + const ethWalletV2 = new Wallet(bitgo, ethCoin, walletDataV2); + const isWalletAddressSpy = sinon.spy(ethCoin, 'isWalletAddress'); + + // Mock keychain requests + nock(bgUrl).get(`/api/v2/hteth/key/598f606cd8fc24710d2ebad89dce86c2`).reply(200, { + id: '598f606cd8fc24710d2ebad89dce86c2', + pub: 'xpub661MyMwAqRbcGrCxCX39zb3TvYjTqfUGwEUZHjnraRFm1WeMw9gfCD1wwc2wUDmBBZ2TkccJMwf5eBTja8r3z6HMxoTZGW6nvyoJMQFsecv', + ethAddress: '0x9d16bb867b792c5e3bf636a0275f2db8601bd7d4', + source: 'user', + type: 'independent', + }); + nock(bgUrl).get(`/api/v2/hteth/key/598f606cc8e43aef09fcb785221d9dd2`).reply(200, { + id: '598f606cc8e43aef09fcb785221d9dd2', + pub: 'xpub661MyMwAqRbcGKhdeC4nr1ta8d27xThtfFFHgbxWMrVb595meMS8i3fBMrTz8EdQMWBKHHKzxapGgheoMymVvRcQmaGDykRTBbtXqbiu9ps', + ethAddress: '0x2dfce5cfeb5c03fbe680cd39ac0d2b25399b7d22', + source: 'backup', + type: 'independent', + }); + nock(bgUrl).get(`/api/v2/hteth/key/5935d59cf660764331bafcade1855fd7`).reply(200, { + id: '5935d59cf660764331bafcade1855fd7', + pub: 'xpub661MyMwAqRbcGzTn5eyNGDkb18R43nH79HokYLc5PXZM19V8UrbuLdVRaCQMs4EeCAjnqmoYXqfyusTU46WoZMDyLpmTzoUX66ZBwGFjt1a', + ethAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + source: 'bitgo', + type: 'independent', + }); + + // Mock address creation API + nock(bgUrl) + .post(`/api/v2/hteth/wallet/${ethWalletV2.id()}/address`) + .reply(200, { + id: '638a48c6c3dba40007a3497fa49a080c', + address: '0xf636ceddffe41d106586875c0e56dc8feb6268f7', + chain: 0, + index: 23, + coin: 'hteth', + wallet: ethWalletV2.id(), + coinSpecific: { + forwarderVersion: 2, + salt: '0x17', + }, + }); + + await ethWalletV2.createAddress({ chain: 0 }); + + // Verify isWalletAddress was called with walletVersion 2 + isWalletAddressSpy.calledOnce.should.be.true(); + const calledParams = isWalletAddressSpy.firstCall.args[0]; + calledParams.should.have.property('walletVersion', 2); + }); + }); + describe('EVM Cross Chain Recovery', function () { const baseUrl = common.Environments.test.etherscanBaseUrl as string; it('should build a recovery transaction for hot wallet', async function () { From 21302c3bb90afc0141ada14cdaf791e4b68edb52 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Wed, 26 Nov 2025 12:11:39 -0500 Subject: [PATCH 4/4] test: add test to show old failing behaviour TICKET: WP-6461 --- modules/sdk-coin-eth/test/unit/eth.ts | 80 +++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/modules/sdk-coin-eth/test/unit/eth.ts b/modules/sdk-coin-eth/test/unit/eth.ts index 2514aaca4f..81cffa017e 100644 --- a/modules/sdk-coin-eth/test/unit/eth.ts +++ b/modules/sdk-coin-eth/test/unit/eth.ts @@ -1513,6 +1513,86 @@ describe('ETH:', function () { const calledParams = isWalletAddressSpy.firstCall.args[0]; calledParams.should.have.property('walletVersion', 2); }); + + it('should fail v6 address creation if walletVersion was not passed (simulates old bug)', async function () { + const bgUrl = common.Environments[bitgo.getEnv()].uri; + const ethCoin = bitgo.coin('hteth') as Hteth; + const walletDataV6 = { + id: '598f606cd8fc24710d2ebadb1d9459bb', + coinSpecific: { + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + walletVersion: 6, + }, + coin: 'hteth', + keys: [ + '598f606cd8fc24710d2ebad89dce86c2', + '598f606cc8e43aef09fcb785221d9dd2', + '5935d59cf660764331bafcade1855fd7', + ], + receiveAddress: { + address: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + }, + }; + const ethWalletV6 = new Wallet(bitgo, ethCoin, walletDataV6); + + // Stub isWalletAddress to simulate the OLD bug where walletVersion was not passed + const originalIsWalletAddress = ethCoin.isWalletAddress.bind(ethCoin); + const isWalletAddressStub = sinon.stub(ethCoin, 'isWalletAddress').callsFake(async (params) => { + // Remove walletVersion to simulate the old bug + const paramsWithoutWalletVersion = { ...params }; + delete (paramsWithoutWalletVersion as any).walletVersion; + return originalIsWalletAddress(paramsWithoutWalletVersion); + }); + + // Mock keychain requests + nock(bgUrl).get(`/api/v2/hteth/key/598f606cd8fc24710d2ebad89dce86c2`).reply(200, { + id: '598f606cd8fc24710d2ebad89dce86c2', + pub: 'xpub661MyMwAqRbcFXDcWD2vxuebcT1ZpTF4Vke6qmMW8yzddwNYpAPjvYEEL5jLfyYXW2fuxtAxY8TgjPUJLcf1C8qz9N6VgZxArKX4EwB8rH5', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + source: 'user', + type: 'tss', + }); + nock(bgUrl).get(`/api/v2/hteth/key/598f606cc8e43aef09fcb785221d9dd2`).reply(200, { + id: '598f606cc8e43aef09fcb785221d9dd2', + pub: 'xpub661MyMwAqRbcGhSaXikpuTC9KU88Xx9LrjKSw1JKsvXNgabpTdgjy7LSovh9ZHhcqhAHQu7uthu7FguNGdcC4aXTKK5gqTcPe4WvLYRbCSG', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + source: 'backup', + type: 'tss', + }); + nock(bgUrl).get(`/api/v2/hteth/key/5935d59cf660764331bafcade1855fd7`).reply(200, { + id: '5935d59cf660764331bafcade1855fd7', + pub: 'xpub661MyMwAqRbcFsXShW8R3hJsHNTYTUwzcejnLkY7KCtaJbDqcGkcBF99BrEJSjNZHeHveiYUrsAdwnjUMGwpgmEbiKcZWRuVA9HxnRaA3r3', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + source: 'bitgo', + type: 'tss', + }); + + // Mock address creation API + nock(bgUrl) + .post(`/api/v2/hteth/wallet/${ethWalletV6.id()}/address`) + .reply(200, { + id: '638a48c6c3dba40007a3497fa49a080c', + address: '0xc012041dac143a59fa491db3a2b67b69bd78b685', + chain: 0, + index: 0, + coin: 'hteth', + wallet: ethWalletV6.id(), + coinSpecific: { + forwarderVersion: 4, + salt: '0x0', + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + }); + + // Without walletVersion, address creation should fail because + // the code doesn't know to use TSS verification for v6 wallets + await assert.rejects(async () => ethWalletV6.createAddress({ chain: 0 }), UnexpectedAddressError); + + isWalletAddressStub.restore(); + }); }); describe('EVM Cross Chain Recovery', function () {