From a3065e18d51030ff06ed12e217514f7626a2a8ca Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Mon, 8 Dec 2025 13:30:08 -0500 Subject: [PATCH 1/2] feat: address verification for near TICKET: WP-7082 --- modules/sdk-coin-near/src/near.ts | 52 ++++++++++++-- modules/sdk-coin-near/test/unit/near.ts | 94 +++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/modules/sdk-coin-near/src/near.ts b/modules/sdk-coin-near/src/near.ts index 6876b2b255..ee0416f76f 100644 --- a/modules/sdk-coin-near/src/near.ts +++ b/modules/sdk-coin-near/src/near.ts @@ -18,8 +18,8 @@ import { EDDSAMethods, EDDSAMethodTypes, Environments, + InvalidAddressError, KeyPair, - MethodNotImplementedError, MPCAlgorithm, MPCRecoveryOptions, MPCSweepRecoveryOptions, @@ -38,7 +38,9 @@ import { TokenEnablementConfig, TransactionParams, TransactionType, - VerifyAddressOptions, + TssVerifyAddressOptions, + UnexpectedAddressError, + verifyMPCWalletAddress, VerifyTransactionOptions, } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, CoinFamily, coins, Nep141Token, Networks } from '@bitgo/statics'; @@ -82,6 +84,15 @@ export interface NearParseTransactionOptions extends BaseParseTransactionOptions }; } +/** + * Options for verifying NEAR TSS/MPC wallet addresses. + * Extends base TssVerifyAddressOptions with NEAR-specific fields. + */ +export interface TssVerifyNearAddressOptions extends TssVerifyAddressOptions { + /** The root address of the wallet (for root address verification) */ + rootAddress?: string; +} + interface TransactionOutput { address: string; amount: string; @@ -984,8 +995,41 @@ export class Near extends BaseCoin { }; } - async isWalletAddress(params: VerifyAddressOptions): Promise { - throw new MethodNotImplementedError(); + /** + * Verify if an address belongs to a NEAR wallet using EdDSA TSS MPC derivation. + * For NEAR, the address is the public key directly (implicit accounts). + * + * @param {TssVerifyAddressOptions} params - Verification parameters + * @returns {Promise} True if address belongs to wallet + * @throws {InvalidAddressError} If address format is invalid or doesn't match derived address + * @throws {Error} If invalid parameters + */ + async isWalletAddress(params: TssVerifyNearAddressOptions): Promise { + const { address, rootAddress } = params; + + if (!this.isValidAddress(address)) { + throw new InvalidAddressError(`invalid address: ${address}`); + } + + const isVerifyingRootAddress = rootAddress && address === rootAddress; + if (isVerifyingRootAddress) { + const index = typeof params.index === 'string' ? parseInt(params.index, 10) : params.index; + if (index !== 0) { + throw new Error(`Root address verification requires index 0, but got index ${params.index}.`); + } + } + + const result = await verifyMPCWalletAddress( + { ...params, keyCurve: 'ed25519' }, + this.isValidAddress.bind(this), + (pubKey) => pubKey + ); + + if (!result) { + throw new UnexpectedAddressError(`address validation failure: address ${address} is not a wallet address`); + } + + return true; } async verifyTransaction(params: VerifyTransactionOptions): Promise { diff --git a/modules/sdk-coin-near/test/unit/near.ts b/modules/sdk-coin-near/test/unit/near.ts index dd8c1cdc1b..166151f6b6 100644 --- a/modules/sdk-coin-near/test/unit/near.ts +++ b/modules/sdk-coin-near/test/unit/near.ts @@ -151,6 +151,100 @@ describe('NEAR:', function () { }); }); + describe('Address verification', () => { + const addressVerificationData = { + commonKeychain: + '43d3f6a94d7e3faf4dd390a7e26f554eaa98c8f0813e3f0ae959d61d8acd012e0504e552a5c311260f2fbaef3a817dfa5b85b984cd43b161bebad9ded25764cc', + rootAddress: '98908af363d3e99d87b1d6dce4f80a28bbfe64fee22dbb8a36dada25ba30d027', + receiveAddress: '6aa21569736f6ebaf925fef8ece219c2b703098cc358ce34a97f2c2a2e099659', + receiveAddressIndex: 2, + }; + + let keychains; + + before(function () { + keychains = [ + { commonKeychain: addressVerificationData.commonKeychain }, + { commonKeychain: addressVerificationData.commonKeychain }, + { commonKeychain: addressVerificationData.commonKeychain }, + ]; + }); + + it('should verify a valid TSS root address (index 0)', async function () { + const params = { + address: addressVerificationData.rootAddress, + rootAddress: addressVerificationData.rootAddress, + keychains: keychains, + index: 0, + }; + const result = await basecoin.isWalletAddress(params); + result.should.equal(true); + }); + + it('should verify a valid TSS receive address (index > 0)', async function () { + const params = { + address: addressVerificationData.receiveAddress, + rootAddress: addressVerificationData.rootAddress, + keychains: keychains, + index: addressVerificationData.receiveAddressIndex, + }; + const result = await basecoin.isWalletAddress(params); + result.should.equal(true); + }); + + it('should throw error for invalid address format', async function () { + const invalidAddress = 'invalid-address'; + const params = { + address: invalidAddress, + keychains: keychains, + index: 0, + }; + await basecoin.isWalletAddress(params).should.be.rejected(); + }); + + it('should throw error when verifying root address with wrong index', async function () { + const params = { + address: addressVerificationData.rootAddress, + rootAddress: addressVerificationData.rootAddress, + keychains: keychains, + index: 1, + }; + await basecoin + .isWalletAddress(params) + .should.be.rejectedWith('Root address verification requires index 0, but got index 1.'); + }); + + it('should throw error when keychains is missing', async function () { + const params = { + address: addressVerificationData.rootAddress, + keychains: [], + index: 0, + }; + await basecoin.isWalletAddress(params).should.be.rejectedWith('missing required param keychains'); + }); + + it('should throw error for address that does not match derivation', async function () { + const wrongAddress = '0000000000000000000000000000000000000000000000000000000000000000'; + const params = { + address: wrongAddress, + keychains: keychains, + index: 0, + }; + await basecoin.isWalletAddress(params).should.be.rejected(); + }); + + it('should handle string index', async function () { + const params = { + address: addressVerificationData.rootAddress, + rootAddress: addressVerificationData.rootAddress, + keychains: keychains, + index: '0', + }; + const result = await basecoin.isWalletAddress(params); + result.should.equal(true); + }); + }); + describe('Verify transaction: ', () => { const amount = '1000000'; const gas = '125000000000000'; From 640929ca311a485d7c46dc64f19500ddc294c592 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Mon, 15 Dec 2025 10:24:38 -0500 Subject: [PATCH 2/2] refactor: use verifyEddsaTssWalletAddress TICKET: WP-7082 --- modules/sdk-coin-near/src/near.ts | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/modules/sdk-coin-near/src/near.ts b/modules/sdk-coin-near/src/near.ts index ee0416f76f..8b53776110 100644 --- a/modules/sdk-coin-near/src/near.ts +++ b/modules/sdk-coin-near/src/near.ts @@ -18,7 +18,6 @@ import { EDDSAMethods, EDDSAMethodTypes, Environments, - InvalidAddressError, KeyPair, MPCAlgorithm, MPCRecoveryOptions, @@ -40,7 +39,7 @@ import { TransactionType, TssVerifyAddressOptions, UnexpectedAddressError, - verifyMPCWalletAddress, + verifyEddsaTssWalletAddress, VerifyTransactionOptions, } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, CoinFamily, coins, Nep141Token, Networks } from '@bitgo/statics'; @@ -996,21 +995,18 @@ export class Near extends BaseCoin { } /** - * Verify if an address belongs to a NEAR wallet using EdDSA TSS MPC derivation. + * Verifies if the given address belongs to a TSS wallet for NEAR. * For NEAR, the address is the public key directly (implicit accounts). * - * @param {TssVerifyAddressOptions} params - Verification parameters + * @param {TssVerifyNearAddressOptions} params - Verification parameters * @returns {Promise} True if address belongs to wallet - * @throws {InvalidAddressError} If address format is invalid or doesn't match derived address - * @throws {Error} If invalid parameters + * @throws {UnexpectedAddressError} If address doesn't match derived address + * @throws {Error} If invalid parameters or root address verification with wrong index */ async isWalletAddress(params: TssVerifyNearAddressOptions): Promise { const { address, rootAddress } = params; - if (!this.isValidAddress(address)) { - throw new InvalidAddressError(`invalid address: ${address}`); - } - + // Root address verification requires index 0 const isVerifyingRootAddress = rootAddress && address === rootAddress; if (isVerifyingRootAddress) { const index = typeof params.index === 'string' ? parseInt(params.index, 10) : params.index; @@ -1019,14 +1015,14 @@ export class Near extends BaseCoin { } } - const result = await verifyMPCWalletAddress( - { ...params, keyCurve: 'ed25519' }, - this.isValidAddress.bind(this), - (pubKey) => pubKey + const result = await verifyEddsaTssWalletAddress( + params, + (address) => this.isValidAddress(address), + (publicKey) => publicKey ); if (!result) { - throw new UnexpectedAddressError(`address validation failure: address ${address} is not a wallet address`); + throw new UnexpectedAddressError(`address validation failure: address ${params.address} is not a wallet address`); } return true;