diff --git a/modules/sdk-coin-near/src/near.ts b/modules/sdk-coin-near/src/near.ts index 6876b2b255..8b53776110 100644 --- a/modules/sdk-coin-near/src/near.ts +++ b/modules/sdk-coin-near/src/near.ts @@ -19,7 +19,6 @@ import { EDDSAMethodTypes, Environments, KeyPair, - MethodNotImplementedError, MPCAlgorithm, MPCRecoveryOptions, MPCSweepRecoveryOptions, @@ -38,7 +37,9 @@ import { TokenEnablementConfig, TransactionParams, TransactionType, - VerifyAddressOptions, + TssVerifyAddressOptions, + UnexpectedAddressError, + verifyEddsaTssWalletAddress, VerifyTransactionOptions, } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, CoinFamily, coins, Nep141Token, Networks } from '@bitgo/statics'; @@ -82,6 +83,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 +994,38 @@ export class Near extends BaseCoin { }; } - async isWalletAddress(params: VerifyAddressOptions): Promise { - throw new MethodNotImplementedError(); + /** + * Verifies if the given address belongs to a TSS wallet for NEAR. + * For NEAR, the address is the public key directly (implicit accounts). + * + * @param {TssVerifyNearAddressOptions} params - Verification parameters + * @returns {Promise} True if address belongs to wallet + * @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; + + // 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; + if (index !== 0) { + throw new Error(`Root address verification requires index 0, but got index ${params.index}.`); + } + } + + const result = await verifyEddsaTssWalletAddress( + params, + (address) => this.isValidAddress(address), + (publicKey) => publicKey + ); + + if (!result) { + throw new UnexpectedAddressError(`address validation failure: address ${params.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';