Skip to content
Merged
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
91 changes: 89 additions & 2 deletions modules/sdk-coin-trx/src/trx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getIsKrsRecovery,
getIsUnsignedSweep,
KeyPair,
KeyIndices,
MethodNotImplementedError,
ParsedTransaction,
ParseTransactionOptions,
Expand All @@ -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';
Expand Down Expand Up @@ -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<StaticsBaseCoin>;

Expand Down Expand Up @@ -246,7 +267,73 @@ export class Trx extends BaseCoin {
}

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