diff --git a/modules/sdk-coin-trx/src/lib/utils.ts b/modules/sdk-coin-trx/src/lib/utils.ts index 4ebdc77815..db175c1be0 100644 --- a/modules/sdk-coin-trx/src/lib/utils.ts +++ b/modules/sdk-coin-trx/src/lib/utils.ts @@ -62,15 +62,15 @@ export function isBase58Address(address: string): boolean { } /** - * Detects hex representations of a TRON address: the 20-byte EVM-style form - * (bare or 0x-prefixed) and the 21-byte form with the 0x41 version prefix. - * These encode the same 21-byte address as the base58 form. + * Detects hex representations of a TRON address: 0x-prefixed 20-byte EVM-style, + * or 41-prefixed 21-byte form. Bare 20-byte hex (no 0x/41) is rejected — it is not + * an on-chain / API address encoding. * * @param address */ export function isHexAddress(address: string): boolean { - const bare = address.toLowerCase().replace(/^0x/, ''); - return /^[0-9a-f]{40}$/.test(bare) || /^41[0-9a-f]{40}$/.test(bare); + const lower = address.toLowerCase(); + return /^0x[0-9a-f]{40}$/.test(lower) || /^41[0-9a-f]{40}$/.test(lower); } /** diff --git a/modules/sdk-coin-trx/src/trx.ts b/modules/sdk-coin-trx/src/trx.ts index 0a00781e88..3032989cb1 100644 --- a/modules/sdk-coin-trx/src/trx.ts +++ b/modules/sdk-coin-trx/src/trx.ts @@ -232,23 +232,35 @@ export class Trx extends BaseCoin { } /** - * Checks if this is a valid base58 - * @param address + * Checks if this is a valid TRON address in any accepted representation: + * base58 (T...), 41-prefixed hex, or 0x-prefixed EVM-style hex (COINS-1575). */ isValidAddress(address: string): boolean { if (!address) { return false; } - return Utils.isBase58Address(address); + return Utils.isBase58Address(address) || Utils.isHexAddress(address); } /** - * Checks if this is a valid hex address + * Checks if this is a valid hex representation of a TRON address + * (0x-prefixed 20-byte, or 41-prefixed 21-byte). * @param address hex address */ isValidHexAddress(address: string): boolean { - return /^41[0-9a-f]{40}$/i.test(address); + return Utils.isHexAddress(address); + } + + /** + * Convert any accepted TRON address form to canonical base58. + * Hex (0x... / 41...) and base58 all encode the same 21-byte address. + */ + canonicalAddress(address: string): string { + if (!this.isValidAddress(address)) { + return address; + } + return Utils.getBase58AddressFromHexAddress(address); } /** @@ -479,9 +491,12 @@ export class Trx extends BaseCoin { if (txParams.recipients && txParams.recipients.length === 1) { const recipient = txParams.recipients[0]; const expectedAmount = recipient.amount.toString(); - const expectedDestination = recipient.address; + // Canonicalize client-supplied address (base58 / 0x... / 41...) before comparing (COINS-1575). + const expectedDestination = Utils.getBase58AddressFromHexAddress(recipient.address); const actualAmount = value.amount.toString(); - const actualDestination = addressesInBase58 ? value.to_address : Utils.getBase58AddressFromHex(value.to_address); + const actualDestination = addressesInBase58 + ? Utils.getBase58AddressFromHexAddress(value.to_address) + : Utils.getBase58AddressFromHex(value.to_address); if (expectedAmount !== actualAmount) { throw new Error('transaction amount in txPrebuild does not match the value given by client'); diff --git a/modules/sdk-coin-trx/src/trxToken.ts b/modules/sdk-coin-trx/src/trxToken.ts index 75bfa659cc..8aabf3b7cd 100644 --- a/modules/sdk-coin-trx/src/trxToken.ts +++ b/modules/sdk-coin-trx/src/trxToken.ts @@ -140,10 +140,11 @@ export class TrxToken extends Trx { throw new Error('invalid required property recipients'); } - // recipientHex has '41' hex prefix; convert to base58 for comparison + // recipientHex has '41' hex prefix; convert to base58 for comparison. + // Canonicalize the client-supplied address too so 0x... / 41... match base58 outputs (COINS-1575). const actualDestination = Utils.getBase58AddressFromHex(recipientHex); const actualAmount = transferAmount.toString(); - const expectedDestination = recipients[0].address; + const expectedDestination = Utils.getBase58AddressFromHexAddress(recipients[0].address); const expectedAmount = recipients[0].amount.toString(); if (actualAmount !== expectedAmount) { @@ -167,7 +168,10 @@ export class TrxToken extends Trx { throw new Error('missing required property recipients'); } - if (recipients[0].address === tx.outputs[0].address && recipients[0].amount === tx.outputs[0].value) { + // Outputs are base58; clients may pass base58, 0x..., or 41... — compare in canonical form. + const expectedAddress = Utils.getBase58AddressFromHexAddress(recipients[0].address); + const actualAddress = Utils.getBase58AddressFromHexAddress(tx.outputs[0].address); + if (expectedAddress === actualAddress && recipients[0].amount.toString() === tx.outputs[0].value.toString()) { return true; } else { throw new Error('Tx outputs does not match with expected txParams recipients'); diff --git a/modules/sdk-coin-trx/test/unit/trx.ts b/modules/sdk-coin-trx/test/unit/trx.ts index a8cccb56f6..8eed8f0d64 100644 --- a/modules/sdk-coin-trx/test/unit/trx.ts +++ b/modules/sdk-coin-trx/test/unit/trx.ts @@ -79,28 +79,43 @@ describe('TRON:', function () { null, 'xxxx', 'YZ09fd-', - '412C2BA4A9FF6C53207DC5B686BFECF75EA7B805772', - '412C2BA4A9FF6C53207DC5B686BFECF75EA7B80', - 'TBChwKYNaTo4a4N68Me1qEiiKsRDspXqLLZ', + '412C2BA4A9FF6C53207DC5B686BFECF75EA7B805772', // 43 hex chars — too long + '412C2BA4A9FF6C53207DC5B686BFECF75EA7B80', // too short + 'TBChwKYNaTo4a4N68Me1qEiiKsRDspXqLLZ', // invalid base58 checksum + '0x341qg3922b1', // non-hex + '96be113992bdc3be24c11f6017085b605d253649', // bare 20-byte — not an on-chain address form + ]; + // base58 and hex (0x / 41) are alternative encodings of the same address (COINS-1575) + const goodAddresses = [ + 'TBChwKYNaTo4a4N68Me1qEiiKsRDspXqLp', + 'TPcf5jtYUhCN1X14tN577zF4NepbDZbxT7', '0x96be113992bdc3be24c11f6017085b605d253649', - '0x341qg3922b1', '41E0C0F581D7D02D40826C1C6CBEE71F625D6344D0', '412C2BA4A9FF6C53207DC5B686BFECF75EA7B80577', '418840E6C55B9ADA326D211D818C34A994AECED808', '412A2B9F7641D0750C1E822D0E49EF765C8106524B', '41A614F803B6FD780986A42C78EC9C7F77E6DED13C', - '418840E6C55B9ADA326D211D818C34A994AECED808', ]; - const goodAddresses = ['TBChwKYNaTo4a4N68Me1qEiiKsRDspXqLp', 'TPcf5jtYUhCN1X14tN577zF4NepbDZbxT7']; badAddresses.map((addr) => { - assert.equal(basecoin.isValidAddress(addr), false); + assert.equal(basecoin.isValidAddress(addr as string), false); }); goodAddresses.map((addr) => { assert.equal(basecoin.isValidAddress(addr), true); }); }); + it('should canonicalize hex addresses to base58 (COINS-1575)', function () { + const base58 = 'TGai5uHgBcoLERrzDXMepqZB8Et7D8nV8K'; + const hex41 = '414887974f42a789ef6d4dfc7ba28b1583219434b3'; + const hex0x = '0x4887974f42a789ef6d4dfc7ba28b1583219434b3'; + + assert.equal(basecoin.canonicalAddress(base58), base58); + assert.equal(basecoin.canonicalAddress(hex41), base58); + assert.equal(basecoin.canonicalAddress(hex0x), base58); + assert.equal(basecoin.canonicalAddress('not-an-address'), 'not-an-address'); + }); + it('should throw if the params object is missing parameters', async function () { const explainParams = { feeInfo: { fee: 1 }, diff --git a/modules/sdk-coin-trx/test/unit/trxToken.ts b/modules/sdk-coin-trx/test/unit/trxToken.ts index 7238289ef1..d7358c6587 100644 --- a/modules/sdk-coin-trx/test/unit/trxToken.ts +++ b/modules/sdk-coin-trx/test/unit/trxToken.ts @@ -59,6 +59,28 @@ describe('TrxToken verifyTransaction:', function () { assert.strictEqual(result, true); }); + it('should validate when recipient is supplied as 0x-prefixed hex (COINS-1575)', async function () { + const recipientEvm = '0x' + TRC20_RECIPIENT_HEX.slice(2).toLowerCase(); + + const result = await tokenCoin.verifyTransaction({ + txPrebuild: { txHex: TRC20_RAW_DATA_HEX }, + txParams: { recipients: [{ address: recipientEvm, amount: TRC20_AMOUNT }] }, + walletType: 'tss', + } as any); + + assert.strictEqual(result, true); + }); + + it('should validate when recipient is supplied as 41-prefixed hex (COINS-1575)', async function () { + const result = await tokenCoin.verifyTransaction({ + txPrebuild: { txHex: TRC20_RAW_DATA_HEX }, + txParams: { recipients: [{ address: TRC20_RECIPIENT_HEX, amount: TRC20_AMOUNT }] }, + walletType: 'tss', + } as any); + + assert.strictEqual(result, true); + }); + it('should throw when amount does not match', async function () { const recipientBase58 = Utils.getBase58AddressFromHex(TRC20_RECIPIENT_HEX); @@ -127,11 +149,12 @@ describe('TrxToken verifyTransaction:', function () { }); describe('non-TSS wallet — builder-based validation (existing path)', () => { + const txHex = + '{"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"]}'; + it('should validate a correct non-TSS TRC20 transfer using txBuilder', async function () { // The non-TSS path uses getBuilder().from(rawTx).build() and checks tx.outputs[0] // This test uses the full JSON tx format that the builder understands. - const txHex = - '{"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"]}'; const recipientBase58 = Utils.getBase58AddressFromHex(TRC20_RECIPIENT_HEX); const result = await tokenCoin.verifyTransaction({ @@ -141,5 +164,25 @@ describe('TrxToken verifyTransaction:', function () { assert.strictEqual(result, true); }); + + it('should validate when recipient is 0x-prefixed hex (hot wallet sendmany / COINS-1575)', async function () { + const recipientEvm = '0x' + TRC20_RECIPIENT_HEX.slice(2).toLowerCase(); + + const result = await tokenCoin.verifyTransaction({ + txPrebuild: { txHex }, + txParams: { recipients: [{ address: recipientEvm, amount: TRC20_AMOUNT }] }, + } as any); + + assert.strictEqual(result, true); + }); + + it('should validate when recipient is 41-prefixed hex (hot wallet sendmany / COINS-1575)', async function () { + const result = await tokenCoin.verifyTransaction({ + txPrebuild: { txHex }, + txParams: { recipients: [{ address: TRC20_RECIPIENT_HEX, amount: TRC20_AMOUNT }] }, + } as any); + + assert.strictEqual(result, true); + }); }); }); diff --git a/modules/sdk-coin-trx/test/unit/verifyTransaction.ts b/modules/sdk-coin-trx/test/unit/verifyTransaction.ts index bd6173ff4d..a95bf9d8dc 100644 --- a/modules/sdk-coin-trx/test/unit/verifyTransaction.ts +++ b/modules/sdk-coin-trx/test/unit/verifyTransaction.ts @@ -131,6 +131,65 @@ describe('TRON Verify Transaction:', function () { assert.strictEqual(result, true); }); + it('should validate TransferContract when recipient is 0x-prefixed hex (COINS-1575)', async function () { + const timestamp = Date.now(); + const toAddressHex = '41d6cd6a2c0ff35a319e6abb5b9503ba0278679882'; + const transferContract = { + parameter: { + value: { + amount: 1000000, + owner_address: '4173a5993cd182ae152adad8203163f780c65a8aa5', + to_address: toAddressHex, + }, + type_url: 'type.googleapis.com/protocol.TransferContract', + }, + type: 'TransferContract', + }; + + const rawData = { + contract: [transferContract], + ref_block_bytes: 'c8cf', + ref_block_hash: '89177fd84c5d9196', + expiration: timestamp + 3600000, + timestamp: timestamp, + fee_limit: 150000000, + }; + + const transformedRawData = { + contract: rawData.contract as any, + refBlockBytes: rawData.ref_block_bytes, + refBlockHash: rawData.ref_block_hash, + expiration: rawData.expiration, + timestamp: rawData.timestamp, + feeLimit: rawData.fee_limit, + }; + + const rawDataHex = Utils.generateRawDataHex(transformedRawData); + const txID = createHash('sha256').update(Buffer.from(rawDataHex, 'hex')).digest('hex'); + + const params = { + txParams: { + recipients: [ + { + address: '0x' + toAddressHex.slice(2), + amount: '1000000', + }, + ], + }, + txPrebuild: { + txHex: JSON.stringify({ + txID, + raw_data: rawData, + raw_data_hex: rawDataHex, + }), + }, + wallet: {}, + }; + + const result = await basecoin.verifyTransaction(params); + assert.strictEqual(result, true); + }); + it('should fail with missing owner address', async function () { const timestamp = Date.now(); const txID = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';