From a298876b30219b301bff1f5295d1c20a24f4a585 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Tue, 16 Dec 2025 12:10:44 -0500 Subject: [PATCH 1/4] feat: address verification for substrate coins (tao, polyx) TICKET: WP-7086 --- .../src/abstractSubstrateCoin.ts | 38 +++++- modules/sdk-coin-tao/test/unit/tao.ts | 118 ++++++++++++++++++ 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/modules/abstract-substrate/src/abstractSubstrateCoin.ts b/modules/abstract-substrate/src/abstractSubstrateCoin.ts index 728eab274a..c582e41ffd 100644 --- a/modules/abstract-substrate/src/abstractSubstrateCoin.ts +++ b/modules/abstract-substrate/src/abstractSubstrateCoin.ts @@ -1,11 +1,11 @@ import { + AddressCoinSpecific, AuditDecryptedKeyParams, BaseCoin, BitGoBase, EDDSAMethods, EDDSAMethodTypes, KeyPair, - MethodNotImplementedError, MPCAlgorithm, MPCConsolidationRecoveryOptions, MPCRecoveryOptions, @@ -20,6 +20,8 @@ import { ParseTransactionOptions, RecoveryTxRequest, SignedTransaction, + TssVerifyAddressOptions, + verifyEddsaTssWalletAddress, VerifyAddressOptions, VerifyTransactionOptions, } from '@bitgo/sdk-core'; @@ -34,6 +36,13 @@ import { ApiPromise } from '@polkadot/api'; export const DEFAULT_SCAN_FACTOR = 20; +export interface SubstrateVerifyAddressOptions extends VerifyAddressOptions { + index?: number | string; + coinSpecific?: AddressCoinSpecific & { + index?: number | string; + }; +} + export class SubstrateCoin extends BaseCoin { protected readonly _staticsCoin: Readonly; readonly MAX_VALIDITY_DURATION = 2400; @@ -110,8 +119,31 @@ export class SubstrateCoin extends BaseCoin { } /** @inheritDoc **/ - isWalletAddress(params: VerifyAddressOptions): Promise { - throw new MethodNotImplementedError(); + async isWalletAddress(params: SubstrateVerifyAddressOptions): Promise { + const { address, keychains } = params; + + const index = Number(params.index ?? params.coinSpecific?.index); + if (isNaN(index) || index < 0) { + throw new Error('Invalid or missing index. index must be a non-negative number.'); + } + + const tssParams: TssVerifyAddressOptions = { + address, + keychains: keychains as TssVerifyAddressOptions['keychains'], + index, + }; + + const isValid = await verifyEddsaTssWalletAddress( + tssParams, + (addr) => this.isValidAddress(addr), + (pubKey) => this.getAddressFromPublicKey(pubKey) + ); + + if (!isValid) { + throw new Error(`Address verification failed: address ${address} is not a wallet address at index ${index}`); + } + + return true; } /** @inheritDoc **/ diff --git a/modules/sdk-coin-tao/test/unit/tao.ts b/modules/sdk-coin-tao/test/unit/tao.ts index ce5411187e..bd2ff5585e 100644 --- a/modules/sdk-coin-tao/test/unit/tao.ts +++ b/modules/sdk-coin-tao/test/unit/tao.ts @@ -11,6 +11,15 @@ describe('Tao:', function () { let bitgo: TestBitGoAPI; let baseCoin; + // Test data from wallet 694042b5efbee757e47ec2771cf58a45 + const isWalletAddressTestData = { + commonKeychain: + '6e2235aee215f3909b42bf67c360f5bc6ba7087cbf0ed5ba841dd044ae7c3051722f9be974e8e79fa6c4c93d109dbc618672e36571925df0b5a7c5b015bcd382', + rootAddress: '5GU55E8X2YVSpd4LApeJR5RsXwQ8AnPdjM37Qz1drLTKh7as', + receiveAddress: '5EN6LFnhFRtWiwZfFncqBxaNUabTASKnSxoDt2zQvpVX4qVy', + receiveAddressIndex: 1, + }; + before(function () { bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); bitgo.safeRegister('tao', Tao.createInstance); @@ -19,6 +28,115 @@ describe('Tao:', function () { baseCoin = bitgo.coin('ttao') as Ttao; }); + describe('isWalletAddress', function () { + it('should verify root address (index 0)', async function () { + const keychains = [ + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + ]; + + const result = await baseCoin.isWalletAddress({ + address: isWalletAddressTestData.rootAddress, + keychains, + index: 0, + }); + + result.should.be.true(); + }); + + it('should verify receive address (index > 0)', async function () { + const keychains = [ + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + ]; + + const result = await baseCoin.isWalletAddress({ + address: isWalletAddressTestData.receiveAddress, + keychains, + index: isWalletAddressTestData.receiveAddressIndex, + }); + + result.should.be.true(); + }); + + it('should throw for address mismatch', async function () { + const keychains = [ + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + ]; + + await baseCoin + .isWalletAddress({ + address: isWalletAddressTestData.receiveAddress, + keychains, + index: 0, // Wrong index for this address + }) + .should.be.rejectedWith(/Address verification failed/); + }); + + it('should throw for missing index', async function () { + const keychains = [ + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + ]; + + await baseCoin + .isWalletAddress({ + address: isWalletAddressTestData.rootAddress, + keychains, + }) + .should.be.rejectedWith(/Invalid or missing index/); + }); + + it('should throw for invalid address', async function () { + const keychains = [ + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + ]; + + await baseCoin + .isWalletAddress({ + address: 'invalidaddress', + keychains, + index: 0, + }) + .should.be.rejectedWith(/invalid address/); + }); + + it('should throw for missing keychains', async function () { + await baseCoin + .isWalletAddress({ + address: isWalletAddressTestData.rootAddress, + keychains: [], + index: 0, + }) + .should.be.rejectedWith(/missing required param keychains/); + }); + + it('should accept index from coinSpecific', async function () { + const keychains = [ + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + { commonKeychain: isWalletAddressTestData.commonKeychain }, + ]; + + const result = await baseCoin.isWalletAddress({ + address: isWalletAddressTestData.receiveAddress, + keychains, + coinSpecific: { + index: isWalletAddressTestData.receiveAddressIndex, + }, + }); + + result.should.be.true(); + }); + }); + describe.skip('Recover Transactions:', function () { const sandBox = sinon.createSandbox(); const recoveryDestination = '5FJ18ywfrWuRifNyc8aPwQ5ium19Fefwmx18H4XYkDc36F2A'; From 369387395c0e60f57dae5b8d9bc087d8036bba80 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Wed, 17 Dec 2025 15:45:44 -0500 Subject: [PATCH 2/4] refactor: simplify TICKET: WP-7114 --- .../src/abstractSubstrateCoin.ts | 29 +++---------------- modules/sdk-coin-tao/test/unit/tao.ts | 20 +------------ 2 files changed, 5 insertions(+), 44 deletions(-) diff --git a/modules/abstract-substrate/src/abstractSubstrateCoin.ts b/modules/abstract-substrate/src/abstractSubstrateCoin.ts index c582e41ffd..f21794a9bc 100644 --- a/modules/abstract-substrate/src/abstractSubstrateCoin.ts +++ b/modules/abstract-substrate/src/abstractSubstrateCoin.ts @@ -1,5 +1,4 @@ import { - AddressCoinSpecific, AuditDecryptedKeyParams, BaseCoin, BitGoBase, @@ -21,8 +20,8 @@ import { RecoveryTxRequest, SignedTransaction, TssVerifyAddressOptions, + UnexpectedAddressError, verifyEddsaTssWalletAddress, - VerifyAddressOptions, VerifyTransactionOptions, } from '@bitgo/sdk-core'; import { CoinFamily, BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; @@ -36,13 +35,6 @@ import { ApiPromise } from '@polkadot/api'; export const DEFAULT_SCAN_FACTOR = 20; -export interface SubstrateVerifyAddressOptions extends VerifyAddressOptions { - index?: number | string; - coinSpecific?: AddressCoinSpecific & { - index?: number | string; - }; -} - export class SubstrateCoin extends BaseCoin { protected readonly _staticsCoin: Readonly; readonly MAX_VALIDITY_DURATION = 2400; @@ -119,28 +111,15 @@ export class SubstrateCoin extends BaseCoin { } /** @inheritDoc **/ - async isWalletAddress(params: SubstrateVerifyAddressOptions): Promise { - const { address, keychains } = params; - - const index = Number(params.index ?? params.coinSpecific?.index); - if (isNaN(index) || index < 0) { - throw new Error('Invalid or missing index. index must be a non-negative number.'); - } - - const tssParams: TssVerifyAddressOptions = { - address, - keychains: keychains as TssVerifyAddressOptions['keychains'], - index, - }; - + async isWalletAddress(params: TssVerifyAddressOptions): Promise { const isValid = await verifyEddsaTssWalletAddress( - tssParams, + params, (addr) => this.isValidAddress(addr), (pubKey) => this.getAddressFromPublicKey(pubKey) ); if (!isValid) { - throw new Error(`Address verification failed: address ${address} is not a wallet address at index ${index}`); + throw new UnexpectedAddressError(); } return true; diff --git a/modules/sdk-coin-tao/test/unit/tao.ts b/modules/sdk-coin-tao/test/unit/tao.ts index bd2ff5585e..2cd33b2e12 100644 --- a/modules/sdk-coin-tao/test/unit/tao.ts +++ b/modules/sdk-coin-tao/test/unit/tao.ts @@ -74,7 +74,7 @@ describe('Tao:', function () { keychains, index: 0, // Wrong index for this address }) - .should.be.rejectedWith(/Address verification failed/); + .should.be.rejectedWith(/address validation failure/); }); it('should throw for missing index', async function () { @@ -117,24 +117,6 @@ describe('Tao:', function () { }) .should.be.rejectedWith(/missing required param keychains/); }); - - it('should accept index from coinSpecific', async function () { - const keychains = [ - { commonKeychain: isWalletAddressTestData.commonKeychain }, - { commonKeychain: isWalletAddressTestData.commonKeychain }, - { commonKeychain: isWalletAddressTestData.commonKeychain }, - ]; - - const result = await baseCoin.isWalletAddress({ - address: isWalletAddressTestData.receiveAddress, - keychains, - coinSpecific: { - index: isWalletAddressTestData.receiveAddressIndex, - }, - }); - - result.should.be.true(); - }); }); describe.skip('Recover Transactions:', function () { From 19cb81b06434d28016620e888335091ec131ae7b Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Wed, 17 Dec 2025 16:37:28 -0500 Subject: [PATCH 3/4] refactor: fix failing ut TICKET: WP-7114 --- modules/sdk-coin-tao/test/unit/tao.ts | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/modules/sdk-coin-tao/test/unit/tao.ts b/modules/sdk-coin-tao/test/unit/tao.ts index 2cd33b2e12..4d00533d70 100644 --- a/modules/sdk-coin-tao/test/unit/tao.ts +++ b/modules/sdk-coin-tao/test/unit/tao.ts @@ -74,22 +74,9 @@ describe('Tao:', function () { keychains, index: 0, // Wrong index for this address }) - .should.be.rejectedWith(/address validation failure/); - }); - - it('should throw for missing index', async function () { - const keychains = [ - { commonKeychain: isWalletAddressTestData.commonKeychain }, - { commonKeychain: isWalletAddressTestData.commonKeychain }, - { commonKeychain: isWalletAddressTestData.commonKeychain }, - ]; - - await baseCoin - .isWalletAddress({ - address: isWalletAddressTestData.rootAddress, - keychains, - }) - .should.be.rejectedWith(/Invalid or missing index/); + .should.be.rejectedWith( + `address validation failure: ${isWalletAddressTestData.receiveAddress} is not a wallet address` + ); }); it('should throw for invalid address', async function () { @@ -105,7 +92,7 @@ describe('Tao:', function () { keychains, index: 0, }) - .should.be.rejectedWith(/invalid address/); + .should.be.rejectedWith('invalid address: invalidaddress'); }); it('should throw for missing keychains', async function () { @@ -115,7 +102,7 @@ describe('Tao:', function () { keychains: [], index: 0, }) - .should.be.rejectedWith(/missing required param keychains/); + .should.be.rejectedWith('missing required param keychains'); }); }); From 0fce50913a55ffe90cb1cf5e46d5a8293a913c23 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Wed, 17 Dec 2025 16:39:19 -0500 Subject: [PATCH 4/4] refactor: include substrate coin changes TICKET: WP-7114 --- modules/abstract-substrate/src/abstractSubstrateCoin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/abstract-substrate/src/abstractSubstrateCoin.ts b/modules/abstract-substrate/src/abstractSubstrateCoin.ts index f21794a9bc..0bfab281b2 100644 --- a/modules/abstract-substrate/src/abstractSubstrateCoin.ts +++ b/modules/abstract-substrate/src/abstractSubstrateCoin.ts @@ -119,7 +119,7 @@ export class SubstrateCoin extends BaseCoin { ); if (!isValid) { - throw new UnexpectedAddressError(); + throw new UnexpectedAddressError(`address validation failure: ${params.address} is not a wallet address`); } return true;