Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions modules/sdk-coin-near/src/near.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {
EDDSAMethods,
EDDSAMethodTypes,
Environments,
InvalidAddressError,
KeyPair,
MethodNotImplementedError,
MPCAlgorithm,
MPCRecoveryOptions,
MPCSweepRecoveryOptions,
Expand All @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -984,8 +995,41 @@ export class Near extends BaseCoin {
};
}

async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
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<boolean>} 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<boolean> {
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<boolean> {
Expand Down
94 changes: 94 additions & 0 deletions modules/sdk-coin-near/test/unit/near.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down