Skip to content

Commit d36a56b

Browse files
committed
fix: ensure consistency in validation as well
The previous fix validated hex during build but not during verify. This pr fixes that Ticket: COINS-1575
1 parent a0575ad commit d36a56b

6 files changed

Lines changed: 160 additions & 24 deletions

File tree

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,15 @@ export function isBase58Address(address: string): boolean {
6262
}
6363

6464
/**
65-
* Detects hex representations of a TRON address: the 20-byte EVM-style form
66-
* (bare or 0x-prefixed) and the 21-byte form with the 0x41 version prefix.
67-
* These encode the same 21-byte address as the base58 form.
65+
* Detects hex representations of a TRON address: 0x-prefixed 20-byte EVM-style,
66+
* or 41-prefixed 21-byte form. Bare 20-byte hex (no 0x/41) is rejected — it is not
67+
* an on-chain / API address encoding.
6868
*
6969
* @param address
7070
*/
7171
export function isHexAddress(address: string): boolean {
72-
const bare = address.toLowerCase().replace(/^0x/, '');
73-
return /^[0-9a-f]{40}$/.test(bare) || /^41[0-9a-f]{40}$/.test(bare);
72+
const lower = address.toLowerCase();
73+
return /^0x[0-9a-f]{40}$/.test(lower) || /^41[0-9a-f]{40}$/.test(lower);
7474
}
7575

7676
/**

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -232,23 +232,35 @@ export class Trx extends BaseCoin {
232232
}
233233

234234
/**
235-
* Checks if this is a valid base58
236-
* @param address
235+
* Checks if this is a valid TRON address in any accepted representation:
236+
* base58 (T...), 41-prefixed hex, or 0x-prefixed EVM-style hex (COINS-1575).
237237
*/
238238
isValidAddress(address: string): boolean {
239239
if (!address) {
240240
return false;
241241
}
242242

243-
return Utils.isBase58Address(address);
243+
return Utils.isBase58Address(address) || Utils.isHexAddress(address);
244244
}
245245

246246
/**
247-
* Checks if this is a valid hex address
247+
* Checks if this is a valid hex representation of a TRON address
248+
* (0x-prefixed 20-byte, or 41-prefixed 21-byte).
248249
* @param address hex address
249250
*/
250251
isValidHexAddress(address: string): boolean {
251-
return /^41[0-9a-f]{40}$/i.test(address);
252+
return Utils.isHexAddress(address);
253+
}
254+
255+
/**
256+
* Convert any accepted TRON address form to canonical base58.
257+
* Hex (0x... / 41...) and base58 all encode the same 21-byte address.
258+
*/
259+
canonicalAddress(address: string): string {
260+
if (!this.isValidAddress(address)) {
261+
return address;
262+
}
263+
return Utils.getBase58AddressFromHexAddress(address);
252264
}
253265

254266
/**
@@ -479,9 +491,12 @@ export class Trx extends BaseCoin {
479491
if (txParams.recipients && txParams.recipients.length === 1) {
480492
const recipient = txParams.recipients[0];
481493
const expectedAmount = recipient.amount.toString();
482-
const expectedDestination = recipient.address;
494+
// Canonicalize client-supplied address (base58 / 0x... / 41...) before comparing (COINS-1575).
495+
const expectedDestination = Utils.getBase58AddressFromHexAddress(recipient.address);
483496
const actualAmount = value.amount.toString();
484-
const actualDestination = addressesInBase58 ? value.to_address : Utils.getBase58AddressFromHex(value.to_address);
497+
const actualDestination = addressesInBase58
498+
? Utils.getBase58AddressFromHexAddress(value.to_address)
499+
: Utils.getBase58AddressFromHex(value.to_address);
485500

486501
if (expectedAmount !== actualAmount) {
487502
throw new Error('transaction amount in txPrebuild does not match the value given by client');

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,11 @@ export class TrxToken extends Trx {
140140
throw new Error('invalid required property recipients');
141141
}
142142

143-
// recipientHex has '41' hex prefix; convert to base58 for comparison
143+
// recipientHex has '41' hex prefix; convert to base58 for comparison.
144+
// Canonicalize the client-supplied address too so 0x... / 41... match base58 outputs (COINS-1575).
144145
const actualDestination = Utils.getBase58AddressFromHex(recipientHex);
145146
const actualAmount = transferAmount.toString();
146-
const expectedDestination = recipients[0].address;
147+
const expectedDestination = Utils.getBase58AddressFromHexAddress(recipients[0].address);
147148
const expectedAmount = recipients[0].amount.toString();
148149

149150
if (actualAmount !== expectedAmount) {
@@ -167,7 +168,10 @@ export class TrxToken extends Trx {
167168
throw new Error('missing required property recipients');
168169
}
169170

170-
if (recipients[0].address === tx.outputs[0].address && recipients[0].amount === tx.outputs[0].value) {
171+
// Outputs are base58; clients may pass base58, 0x..., or 41... — compare in canonical form.
172+
const expectedAddress = Utils.getBase58AddressFromHexAddress(recipients[0].address);
173+
const actualAddress = Utils.getBase58AddressFromHexAddress(tx.outputs[0].address);
174+
if (expectedAddress === actualAddress && recipients[0].amount.toString() === tx.outputs[0].value.toString()) {
171175
return true;
172176
} else {
173177
throw new Error('Tx outputs does not match with expected txParams recipients');

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,28 +79,43 @@ describe('TRON:', function () {
7979
null,
8080
'xxxx',
8181
'YZ09fd-',
82-
'412C2BA4A9FF6C53207DC5B686BFECF75EA7B805772',
83-
'412C2BA4A9FF6C53207DC5B686BFECF75EA7B80',
84-
'TBChwKYNaTo4a4N68Me1qEiiKsRDspXqLLZ',
82+
'412C2BA4A9FF6C53207DC5B686BFECF75EA7B805772', // 43 hex chars — too long
83+
'412C2BA4A9FF6C53207DC5B686BFECF75EA7B80', // too short
84+
'TBChwKYNaTo4a4N68Me1qEiiKsRDspXqLLZ', // invalid base58 checksum
85+
'0x341qg3922b1', // non-hex
86+
'96be113992bdc3be24c11f6017085b605d253649', // bare 20-byte — not an on-chain address form
87+
];
88+
// base58 and hex (0x / 41) are alternative encodings of the same address (COINS-1575)
89+
const goodAddresses = [
90+
'TBChwKYNaTo4a4N68Me1qEiiKsRDspXqLp',
91+
'TPcf5jtYUhCN1X14tN577zF4NepbDZbxT7',
8592
'0x96be113992bdc3be24c11f6017085b605d253649',
86-
'0x341qg3922b1',
8793
'41E0C0F581D7D02D40826C1C6CBEE71F625D6344D0',
8894
'412C2BA4A9FF6C53207DC5B686BFECF75EA7B80577',
8995
'418840E6C55B9ADA326D211D818C34A994AECED808',
9096
'412A2B9F7641D0750C1E822D0E49EF765C8106524B',
9197
'41A614F803B6FD780986A42C78EC9C7F77E6DED13C',
92-
'418840E6C55B9ADA326D211D818C34A994AECED808',
9398
];
94-
const goodAddresses = ['TBChwKYNaTo4a4N68Me1qEiiKsRDspXqLp', 'TPcf5jtYUhCN1X14tN577zF4NepbDZbxT7'];
9599

96100
badAddresses.map((addr) => {
97-
assert.equal(basecoin.isValidAddress(addr), false);
101+
assert.equal(basecoin.isValidAddress(addr as string), false);
98102
});
99103
goodAddresses.map((addr) => {
100104
assert.equal(basecoin.isValidAddress(addr), true);
101105
});
102106
});
103107

108+
it('should canonicalize hex addresses to base58 (COINS-1575)', function () {
109+
const base58 = 'TGai5uHgBcoLERrzDXMepqZB8Et7D8nV8K';
110+
const hex41 = '414887974f42a789ef6d4dfc7ba28b1583219434b3';
111+
const hex0x = '0x4887974f42a789ef6d4dfc7ba28b1583219434b3';
112+
113+
assert.equal(basecoin.canonicalAddress(base58), base58);
114+
assert.equal(basecoin.canonicalAddress(hex41), base58);
115+
assert.equal(basecoin.canonicalAddress(hex0x), base58);
116+
assert.equal(basecoin.canonicalAddress('not-an-address'), 'not-an-address');
117+
});
118+
104119
it('should throw if the params object is missing parameters', async function () {
105120
const explainParams = {
106121
feeInfo: { fee: 1 },

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,28 @@ describe('TrxToken verifyTransaction:', function () {
5959
assert.strictEqual(result, true);
6060
});
6161

62+
it('should validate when recipient is supplied as 0x-prefixed hex (COINS-1575)', async function () {
63+
const recipientEvm = '0x' + TRC20_RECIPIENT_HEX.slice(2).toLowerCase();
64+
65+
const result = await tokenCoin.verifyTransaction({
66+
txPrebuild: { txHex: TRC20_RAW_DATA_HEX },
67+
txParams: { recipients: [{ address: recipientEvm, amount: TRC20_AMOUNT }] },
68+
walletType: 'tss',
69+
} as any);
70+
71+
assert.strictEqual(result, true);
72+
});
73+
74+
it('should validate when recipient is supplied as 41-prefixed hex (COINS-1575)', async function () {
75+
const result = await tokenCoin.verifyTransaction({
76+
txPrebuild: { txHex: TRC20_RAW_DATA_HEX },
77+
txParams: { recipients: [{ address: TRC20_RECIPIENT_HEX, amount: TRC20_AMOUNT }] },
78+
walletType: 'tss',
79+
} as any);
80+
81+
assert.strictEqual(result, true);
82+
});
83+
6284
it('should throw when amount does not match', async function () {
6385
const recipientBase58 = Utils.getBase58AddressFromHex(TRC20_RECIPIENT_HEX);
6486

@@ -127,11 +149,12 @@ describe('TrxToken verifyTransaction:', function () {
127149
});
128150

129151
describe('non-TSS wallet — builder-based validation (existing path)', () => {
152+
const txHex =
153+
'{"raw_data":{"contractType":2,"contract":[{"parameter":{"value":{"data":"a9059cbb0000000000000000000000008483618ca85c35a9b923d98bebca718f5a1db2790000000000000000000000000000000000000000000000000000000005f5e100","owner_address":"41c51fbeea78910b15b1d3e8a9b62914ca94d1a4ac","contract_address":"4142a1e39aefa49290f2b3f9ed688d7cecf86cd6e0"},"type_url":"type.googleapis.com/protocol.TriggerSmartContract"},"type":"TriggerSmartContract"}],"expiration":1674581767432,"timestamp":1674578167432,"ref_block_bytes":"578b","ref_block_hash":"6113bb9ac351432b","fee_limit":15000000},"raw_data_hex":"0a02578b22086113bb9ac351432b4088eae7a6de305aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a1541c51fbeea78910b15b1d3e8a9b62914ca94d1a4ac12154142a1e39aefa49290f2b3f9ed688d7cecf86cd6e02244a9059cbb0000000000000000000000008483618ca85c35a9b923d98bebca718f5a1db2790000000000000000000000000000000000000000000000000000000005f5e10070888d8ca5de309001c0c39307","txID":"fe21c49f4febd9089125e3a006943c145721d8fcb7ab84136f8c6663ff92f8ed","signature":["0775cde302689eb8293883c66a89b31e80d608bfc3ad3c283b64a490ea4cc712c55a2fd2e62c75843dd7e77d8c4cb52e0f371fbb29b332c259f8cb63c2e6195301"]}';
154+
130155
it('should validate a correct non-TSS TRC20 transfer using txBuilder', async function () {
131156
// The non-TSS path uses getBuilder().from(rawTx).build() and checks tx.outputs[0]
132157
// This test uses the full JSON tx format that the builder understands.
133-
const txHex =
134-
'{"raw_data":{"contractType":2,"contract":[{"parameter":{"value":{"data":"a9059cbb0000000000000000000000008483618ca85c35a9b923d98bebca718f5a1db2790000000000000000000000000000000000000000000000000000000005f5e100","owner_address":"41c51fbeea78910b15b1d3e8a9b62914ca94d1a4ac","contract_address":"4142a1e39aefa49290f2b3f9ed688d7cecf86cd6e0"},"type_url":"type.googleapis.com/protocol.TriggerSmartContract"},"type":"TriggerSmartContract"}],"expiration":1674581767432,"timestamp":1674578167432,"ref_block_bytes":"578b","ref_block_hash":"6113bb9ac351432b","fee_limit":15000000},"raw_data_hex":"0a02578b22086113bb9ac351432b4088eae7a6de305aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a1541c51fbeea78910b15b1d3e8a9b62914ca94d1a4ac12154142a1e39aefa49290f2b3f9ed688d7cecf86cd6e02244a9059cbb0000000000000000000000008483618ca85c35a9b923d98bebca718f5a1db2790000000000000000000000000000000000000000000000000000000005f5e10070888d8ca5de309001c0c39307","txID":"fe21c49f4febd9089125e3a006943c145721d8fcb7ab84136f8c6663ff92f8ed","signature":["0775cde302689eb8293883c66a89b31e80d608bfc3ad3c283b64a490ea4cc712c55a2fd2e62c75843dd7e77d8c4cb52e0f371fbb29b332c259f8cb63c2e6195301"]}';
135158
const recipientBase58 = Utils.getBase58AddressFromHex(TRC20_RECIPIENT_HEX);
136159

137160
const result = await tokenCoin.verifyTransaction({
@@ -141,5 +164,25 @@ describe('TrxToken verifyTransaction:', function () {
141164

142165
assert.strictEqual(result, true);
143166
});
167+
168+
it('should validate when recipient is 0x-prefixed hex (hot wallet sendmany / COINS-1575)', async function () {
169+
const recipientEvm = '0x' + TRC20_RECIPIENT_HEX.slice(2).toLowerCase();
170+
171+
const result = await tokenCoin.verifyTransaction({
172+
txPrebuild: { txHex },
173+
txParams: { recipients: [{ address: recipientEvm, amount: TRC20_AMOUNT }] },
174+
} as any);
175+
176+
assert.strictEqual(result, true);
177+
});
178+
179+
it('should validate when recipient is 41-prefixed hex (hot wallet sendmany / COINS-1575)', async function () {
180+
const result = await tokenCoin.verifyTransaction({
181+
txPrebuild: { txHex },
182+
txParams: { recipients: [{ address: TRC20_RECIPIENT_HEX, amount: TRC20_AMOUNT }] },
183+
} as any);
184+
185+
assert.strictEqual(result, true);
186+
});
144187
});
145188
});

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,65 @@ describe('TRON Verify Transaction:', function () {
131131
assert.strictEqual(result, true);
132132
});
133133

134+
it('should validate TransferContract when recipient is 0x-prefixed hex (COINS-1575)', async function () {
135+
const timestamp = Date.now();
136+
const toAddressHex = '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882';
137+
const transferContract = {
138+
parameter: {
139+
value: {
140+
amount: 1000000,
141+
owner_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
142+
to_address: toAddressHex,
143+
},
144+
type_url: 'type.googleapis.com/protocol.TransferContract',
145+
},
146+
type: 'TransferContract',
147+
};
148+
149+
const rawData = {
150+
contract: [transferContract],
151+
ref_block_bytes: 'c8cf',
152+
ref_block_hash: '89177fd84c5d9196',
153+
expiration: timestamp + 3600000,
154+
timestamp: timestamp,
155+
fee_limit: 150000000,
156+
};
157+
158+
const transformedRawData = {
159+
contract: rawData.contract as any,
160+
refBlockBytes: rawData.ref_block_bytes,
161+
refBlockHash: rawData.ref_block_hash,
162+
expiration: rawData.expiration,
163+
timestamp: rawData.timestamp,
164+
feeLimit: rawData.fee_limit,
165+
};
166+
167+
const rawDataHex = Utils.generateRawDataHex(transformedRawData);
168+
const txID = createHash('sha256').update(Buffer.from(rawDataHex, 'hex')).digest('hex');
169+
170+
const params = {
171+
txParams: {
172+
recipients: [
173+
{
174+
address: '0x' + toAddressHex.slice(2),
175+
amount: '1000000',
176+
},
177+
],
178+
},
179+
txPrebuild: {
180+
txHex: JSON.stringify({
181+
txID,
182+
raw_data: rawData,
183+
raw_data_hex: rawDataHex,
184+
}),
185+
},
186+
wallet: {},
187+
};
188+
189+
const result = await basecoin.verifyTransaction(params);
190+
assert.strictEqual(result, true);
191+
});
192+
134193
it('should fail with missing owner address', async function () {
135194
const timestamp = Date.now();
136195
const txID = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';

0 commit comments

Comments
 (0)