Skip to content

Commit 422b5fd

Browse files
Merge pull request #7744 from BitGo/WP-7083-address-verification-for-trx
feat: addres verification for trx
2 parents acc5a73 + ed58fc3 commit 422b5fd

File tree

2 files changed

+219
-2
lines changed

2 files changed

+219
-2
lines changed

modules/sdk-coin-trx/src/trx.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getIsKrsRecovery,
1515
getIsUnsignedSweep,
1616
KeyPair,
17+
KeyIndices,
1718
MethodNotImplementedError,
1819
ParsedTransaction,
1920
ParseTransactionOptions,
@@ -29,8 +30,9 @@ import {
2930
MultisigType,
3031
multisigTypes,
3132
AuditDecryptedKeyParams,
33+
AddressCoinSpecific,
3234
} from '@bitgo/sdk-core';
33-
import { Interface, Utils, WrappedBuilder } from './lib';
35+
import { Interface, Utils, WrappedBuilder, KeyPair as TronKeyPair } from './lib';
3436
import { ValueFields, TransactionReceipt } from './lib/iface';
3537
import { getBuilder } from './lib/builder';
3638
import { isInteger, isUndefined } from 'lodash';
@@ -129,6 +131,25 @@ export interface AccountResponse {
129131
data: [Interface.AccountInfo];
130132
}
131133

134+
export interface TrxVerifyAddressOptions extends VerifyAddressOptions {
135+
index?: number | string;
136+
chain?: number;
137+
coinSpecific?: AddressCoinSpecific & {
138+
index?: number | string;
139+
chain?: number;
140+
};
141+
}
142+
143+
function isTrxVerifyAddressOptions(params: VerifyAddressOptions): params is TrxVerifyAddressOptions {
144+
return (
145+
'index' in params ||
146+
'chain' in params ||
147+
('coinSpecific' in params &&
148+
params.coinSpecific !== undefined &&
149+
('index' in params.coinSpecific || 'chain' in params.coinSpecific))
150+
);
151+
}
152+
132153
export class Trx extends BaseCoin {
133154
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
134155

@@ -246,7 +267,73 @@ export class Trx extends BaseCoin {
246267
}
247268

248269
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
249-
throw new MethodNotImplementedError();
270+
const { address, keychains } = params;
271+
272+
if (!isTrxVerifyAddressOptions(params)) {
273+
throw new Error('Invalid or missing index for address verification');
274+
}
275+
276+
const rawIndex = params.index ?? params.coinSpecific?.index;
277+
const index = Number(rawIndex);
278+
if (isNaN(index)) {
279+
throw new Error('Invalid index. index must be a number.');
280+
}
281+
282+
const chain = Number(params.chain ?? params.coinSpecific?.chain ?? 0);
283+
284+
if (!this.isValidAddress(address)) {
285+
throw new Error(`Invalid address: ${address}`);
286+
}
287+
288+
// Root address verification (Index 0)
289+
if (index === 0) {
290+
const bitgoPub = keychains && keychains.length > KeyIndices.BITGO ? keychains[KeyIndices.BITGO].pub : undefined;
291+
if (!bitgoPub) {
292+
throw new Error('BitGo public key required for root address verification');
293+
}
294+
return this.verifyRootAddress(address, bitgoPub);
295+
}
296+
297+
// Receive address verification (Index > 0)
298+
if (index > 0) {
299+
const userPub = keychains && keychains.length > KeyIndices.USER ? keychains[KeyIndices.USER].pub : undefined;
300+
if (!userPub) {
301+
throw new Error('User public key required for receive address verification');
302+
}
303+
return this.verifyReceiveAddress(address, userPub, index, chain);
304+
}
305+
306+
throw new Error('Invalid index for address verification');
307+
}
308+
309+
/**
310+
* Cryptographically verify that an address is the root address derived from BitGo's public key
311+
*/
312+
private verifyRootAddress(address: string, bitgoPub: string): boolean {
313+
if (!this.isValidXpub(bitgoPub)) {
314+
throw new Error('Invalid bitgo public key');
315+
}
316+
const uncompressedPub = this.xpubToUncompressedPub(bitgoPub);
317+
const byteArrayAddr = Utils.getByteArrayFromHexAddress(uncompressedPub);
318+
const rawAddress = Utils.getRawAddressFromPubKey(byteArrayAddr);
319+
const derivedAddress = Utils.getBase58AddressFromByteArray(rawAddress);
320+
return derivedAddress === address;
321+
}
322+
323+
/**
324+
* Cryptographically verify that an address is a receive address derived from user's key
325+
*/
326+
private verifyReceiveAddress(address: string, userPub: string, index: number, chain: number): boolean {
327+
if (!this.isValidXpub(userPub)) {
328+
throw new Error('Invalid user public key');
329+
}
330+
const derivationPath = `0/0/${chain}/${index}`;
331+
const parentKey = bip32.fromBase58(userPub);
332+
const childKey = parentKey.derivePath(derivationPath);
333+
const derivedPubKeyHex = childKey.publicKey.toString('hex');
334+
const keypair = new TronKeyPair({ pub: derivedPubKeyHex });
335+
const derivedAddress = keypair.getAddress();
336+
return derivedAddress === address;
250337
}
251338

252339
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {

modules/sdk-coin-trx/test/unit/trx.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ describe('TRON:', function () {
2121

2222
let basecoin;
2323

24+
// Test data from wallet 693b011a3ec26986f569b02140c7627e
25+
const testWalletData = {
26+
rootAddress: 'TAf36b36eqoMCzJJm3jwSsP81UvkMxrPbi',
27+
receiveAddress: 'TFaD6DeKFMcBuGuDD7LbbqxTnKunhXfdya',
28+
receiveAddressIndex: 2,
29+
keychains: [
30+
{
31+
id: '693b0110271fc3f5749754097793bb8d',
32+
pub: 'xpub661MyMwAqRbcFsVAdZyN2m8p21WHXg8NRNkqKApyS5gwmFsPdRTrmHYCnzR9vYe8DQ4uWGCBcAAsWE3r97HsFS3K2faZ2ejXNhHxdEoAEWC',
33+
},
34+
{
35+
id: '693b011065b9c4674825ce1f849b7bef',
36+
pub: 'xpub661MyMwAqRbcErKpZr9ztTFJk6fzXWatMFgRnpXfsjybWBfE9847EVGrHHBsGP8fcnzJmJuevAbPUEpjHTjEEWfYUWNMEDahvssQein848o',
37+
},
38+
{
39+
id: '693b01117c41846abb04818815b89b6c',
40+
pub: 'xpub661MyMwAqRbcFqZd7XU9DFW4f29VJzQt7UCA51ypaWa5ymhQ2pRZDTgViw3vZ56PqZ8dj1cracN3fAWhaiG1QKj9mvyt9Cba4nM2tPibNKw',
41+
},
42+
],
43+
};
44+
2445
before(function () {
2546
basecoin = bitgo.coin('ttrx');
2647
});
@@ -612,4 +633,113 @@ describe('TRON:', function () {
612633
assert.equal(Utils.getBase58AddressFromHex(value1.to_address), TestRecoverData.baseAddress);
613634
});
614635
});
636+
637+
describe('isWalletAddress', () => {
638+
it('should verify root address (index 0)', async function () {
639+
const result = await basecoin.isWalletAddress({
640+
address: testWalletData.rootAddress,
641+
keychains: testWalletData.keychains,
642+
index: 0,
643+
});
644+
assert.equal(result, true);
645+
});
646+
647+
it('should verify receive address (index > 0)', async function () {
648+
const result = await basecoin.isWalletAddress({
649+
address: testWalletData.receiveAddress,
650+
keychains: testWalletData.keychains,
651+
index: testWalletData.receiveAddressIndex,
652+
chain: 0,
653+
});
654+
assert.equal(result, true);
655+
});
656+
657+
it('should verify address with index in coinSpecific', async function () {
658+
const result = await basecoin.isWalletAddress({
659+
address: testWalletData.rootAddress,
660+
keychains: testWalletData.keychains,
661+
coinSpecific: { index: 0 },
662+
});
663+
assert.equal(result, true);
664+
});
665+
666+
it('should verify address with string index', async function () {
667+
const result = await basecoin.isWalletAddress({
668+
address: testWalletData.rootAddress,
669+
keychains: testWalletData.keychains,
670+
index: '0',
671+
});
672+
assert.equal(result, true);
673+
});
674+
675+
it('should return false for wrong address at index 0', async function () {
676+
const result = await basecoin.isWalletAddress({
677+
address: testWalletData.receiveAddress, // wrong address for index 0
678+
keychains: testWalletData.keychains,
679+
index: 0,
680+
});
681+
assert.equal(result, false);
682+
});
683+
684+
it('should return false for wrong address at receive index', async function () {
685+
const result = await basecoin.isWalletAddress({
686+
address: testWalletData.rootAddress, // wrong address for index 1
687+
keychains: testWalletData.keychains,
688+
index: testWalletData.receiveAddressIndex,
689+
chain: 0,
690+
});
691+
assert.equal(result, false);
692+
});
693+
694+
it('should throw for invalid address', async function () {
695+
await assert.rejects(
696+
basecoin.isWalletAddress({
697+
address: 'invalid-address',
698+
keychains: testWalletData.keychains,
699+
index: 0,
700+
}),
701+
{
702+
message: 'Invalid address: invalid-address',
703+
}
704+
);
705+
});
706+
707+
it('should throw for missing index', async function () {
708+
await assert.rejects(
709+
basecoin.isWalletAddress({
710+
address: testWalletData.rootAddress,
711+
keychains: testWalletData.keychains,
712+
}),
713+
{
714+
message: 'Invalid or missing index for address verification',
715+
}
716+
);
717+
});
718+
719+
it('should throw for missing bitgo key on root address verification', async function () {
720+
await assert.rejects(
721+
basecoin.isWalletAddress({
722+
address: testWalletData.rootAddress,
723+
keychains: testWalletData.keychains.slice(0, 2), // only user and backup keys
724+
index: 0,
725+
}),
726+
{
727+
message: 'BitGo public key required for root address verification',
728+
}
729+
);
730+
});
731+
732+
it('should throw for missing user key on receive address verification', async function () {
733+
await assert.rejects(
734+
basecoin.isWalletAddress({
735+
address: testWalletData.receiveAddress,
736+
keychains: [], // no keys
737+
index: testWalletData.receiveAddressIndex,
738+
}),
739+
{
740+
message: 'User public key required for receive address verification',
741+
}
742+
);
743+
});
744+
});
615745
});

0 commit comments

Comments
 (0)