diff --git a/modules/sdk-coin-trx/src/trx.ts b/modules/sdk-coin-trx/src/trx.ts index cd3fb02d49..a1eb4fb8be 100644 --- a/modules/sdk-coin-trx/src/trx.ts +++ b/modules/sdk-coin-trx/src/trx.ts @@ -14,6 +14,7 @@ import { getIsKrsRecovery, getIsUnsignedSweep, KeyPair, + KeyIndices, MethodNotImplementedError, ParsedTransaction, ParseTransactionOptions, @@ -29,8 +30,9 @@ import { MultisigType, multisigTypes, AuditDecryptedKeyParams, + AddressCoinSpecific, } from '@bitgo/sdk-core'; -import { Interface, Utils, WrappedBuilder } from './lib'; +import { Interface, Utils, WrappedBuilder, KeyPair as TronKeyPair } from './lib'; import { ValueFields, TransactionReceipt } from './lib/iface'; import { getBuilder } from './lib/builder'; import { isInteger, isUndefined } from 'lodash'; @@ -129,6 +131,25 @@ export interface AccountResponse { data: [Interface.AccountInfo]; } +export interface TrxVerifyAddressOptions extends VerifyAddressOptions { + index?: number | string; + chain?: number; + coinSpecific?: AddressCoinSpecific & { + index?: number | string; + chain?: number; + }; +} + +function isTrxVerifyAddressOptions(params: VerifyAddressOptions): params is TrxVerifyAddressOptions { + return ( + 'index' in params || + 'chain' in params || + ('coinSpecific' in params && + params.coinSpecific !== undefined && + ('index' in params.coinSpecific || 'chain' in params.coinSpecific)) + ); +} + export class Trx extends BaseCoin { protected readonly _staticsCoin: Readonly; @@ -246,7 +267,73 @@ export class Trx extends BaseCoin { } async isWalletAddress(params: VerifyAddressOptions): Promise { - throw new MethodNotImplementedError(); + const { address, keychains } = params; + + if (!isTrxVerifyAddressOptions(params)) { + throw new Error('Invalid or missing index for address verification'); + } + + const rawIndex = params.index ?? params.coinSpecific?.index; + const index = Number(rawIndex); + if (isNaN(index)) { + throw new Error('Invalid index. index must be a number.'); + } + + const chain = Number(params.chain ?? params.coinSpecific?.chain ?? 0); + + if (!this.isValidAddress(address)) { + throw new Error(`Invalid address: ${address}`); + } + + // Root address verification (Index 0) + if (index === 0) { + const bitgoPub = keychains && keychains.length > KeyIndices.BITGO ? keychains[KeyIndices.BITGO].pub : undefined; + if (!bitgoPub) { + throw new Error('BitGo public key required for root address verification'); + } + return this.verifyRootAddress(address, bitgoPub); + } + + // Receive address verification (Index > 0) + if (index > 0) { + const userPub = keychains && keychains.length > KeyIndices.USER ? keychains[KeyIndices.USER].pub : undefined; + if (!userPub) { + throw new Error('User public key required for receive address verification'); + } + return this.verifyReceiveAddress(address, userPub, index, chain); + } + + throw new Error('Invalid index for address verification'); + } + + /** + * Cryptographically verify that an address is the root address derived from BitGo's public key + */ + private verifyRootAddress(address: string, bitgoPub: string): boolean { + if (!this.isValidXpub(bitgoPub)) { + throw new Error('Invalid bitgo public key'); + } + const uncompressedPub = this.xpubToUncompressedPub(bitgoPub); + const byteArrayAddr = Utils.getByteArrayFromHexAddress(uncompressedPub); + const rawAddress = Utils.getRawAddressFromPubKey(byteArrayAddr); + const derivedAddress = Utils.getBase58AddressFromByteArray(rawAddress); + return derivedAddress === address; + } + + /** + * Cryptographically verify that an address is a receive address derived from user's key + */ + private verifyReceiveAddress(address: string, userPub: string, index: number, chain: number): boolean { + if (!this.isValidXpub(userPub)) { + throw new Error('Invalid user public key'); + } + const derivationPath = `0/0/${chain}/${index}`; + const parentKey = bip32.fromBase58(userPub); + const childKey = parentKey.derivePath(derivationPath); + const derivedPubKeyHex = childKey.publicKey.toString('hex'); + const keypair = new TronKeyPair({ pub: derivedPubKeyHex }); + const derivedAddress = keypair.getAddress(); + return derivedAddress === address; } async verifyTransaction(params: VerifyTransactionOptions): Promise { diff --git a/modules/sdk-coin-trx/test/unit/trx.ts b/modules/sdk-coin-trx/test/unit/trx.ts index c7616f300e..e54d3c1558 100644 --- a/modules/sdk-coin-trx/test/unit/trx.ts +++ b/modules/sdk-coin-trx/test/unit/trx.ts @@ -21,6 +21,27 @@ describe('TRON:', function () { let basecoin; + // Test data from wallet 693b011a3ec26986f569b02140c7627e + const testWalletData = { + rootAddress: 'TAf36b36eqoMCzJJm3jwSsP81UvkMxrPbi', + receiveAddress: 'TFaD6DeKFMcBuGuDD7LbbqxTnKunhXfdya', + receiveAddressIndex: 2, + keychains: [ + { + id: '693b0110271fc3f5749754097793bb8d', + pub: 'xpub661MyMwAqRbcFsVAdZyN2m8p21WHXg8NRNkqKApyS5gwmFsPdRTrmHYCnzR9vYe8DQ4uWGCBcAAsWE3r97HsFS3K2faZ2ejXNhHxdEoAEWC', + }, + { + id: '693b011065b9c4674825ce1f849b7bef', + pub: 'xpub661MyMwAqRbcErKpZr9ztTFJk6fzXWatMFgRnpXfsjybWBfE9847EVGrHHBsGP8fcnzJmJuevAbPUEpjHTjEEWfYUWNMEDahvssQein848o', + }, + { + id: '693b01117c41846abb04818815b89b6c', + pub: 'xpub661MyMwAqRbcFqZd7XU9DFW4f29VJzQt7UCA51ypaWa5ymhQ2pRZDTgViw3vZ56PqZ8dj1cracN3fAWhaiG1QKj9mvyt9Cba4nM2tPibNKw', + }, + ], + }; + before(function () { basecoin = bitgo.coin('ttrx'); }); @@ -612,4 +633,113 @@ describe('TRON:', function () { assert.equal(Utils.getBase58AddressFromHex(value1.to_address), TestRecoverData.baseAddress); }); }); + + describe('isWalletAddress', () => { + it('should verify root address (index 0)', async function () { + const result = await basecoin.isWalletAddress({ + address: testWalletData.rootAddress, + keychains: testWalletData.keychains, + index: 0, + }); + assert.equal(result, true); + }); + + it('should verify receive address (index > 0)', async function () { + const result = await basecoin.isWalletAddress({ + address: testWalletData.receiveAddress, + keychains: testWalletData.keychains, + index: testWalletData.receiveAddressIndex, + chain: 0, + }); + assert.equal(result, true); + }); + + it('should verify address with index in coinSpecific', async function () { + const result = await basecoin.isWalletAddress({ + address: testWalletData.rootAddress, + keychains: testWalletData.keychains, + coinSpecific: { index: 0 }, + }); + assert.equal(result, true); + }); + + it('should verify address with string index', async function () { + const result = await basecoin.isWalletAddress({ + address: testWalletData.rootAddress, + keychains: testWalletData.keychains, + index: '0', + }); + assert.equal(result, true); + }); + + it('should return false for wrong address at index 0', async function () { + const result = await basecoin.isWalletAddress({ + address: testWalletData.receiveAddress, // wrong address for index 0 + keychains: testWalletData.keychains, + index: 0, + }); + assert.equal(result, false); + }); + + it('should return false for wrong address at receive index', async function () { + const result = await basecoin.isWalletAddress({ + address: testWalletData.rootAddress, // wrong address for index 1 + keychains: testWalletData.keychains, + index: testWalletData.receiveAddressIndex, + chain: 0, + }); + assert.equal(result, false); + }); + + it('should throw for invalid address', async function () { + await assert.rejects( + basecoin.isWalletAddress({ + address: 'invalid-address', + keychains: testWalletData.keychains, + index: 0, + }), + { + message: 'Invalid address: invalid-address', + } + ); + }); + + it('should throw for missing index', async function () { + await assert.rejects( + basecoin.isWalletAddress({ + address: testWalletData.rootAddress, + keychains: testWalletData.keychains, + }), + { + message: 'Invalid or missing index for address verification', + } + ); + }); + + it('should throw for missing bitgo key on root address verification', async function () { + await assert.rejects( + basecoin.isWalletAddress({ + address: testWalletData.rootAddress, + keychains: testWalletData.keychains.slice(0, 2), // only user and backup keys + index: 0, + }), + { + message: 'BitGo public key required for root address verification', + } + ); + }); + + it('should throw for missing user key on receive address verification', async function () { + await assert.rejects( + basecoin.isWalletAddress({ + address: testWalletData.receiveAddress, + keychains: [], // no keys + index: testWalletData.receiveAddressIndex, + }), + { + message: 'User public key required for receive address verification', + } + ); + }); + }); });