Skip to content
Open
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
10 changes: 5 additions & 5 deletions modules/sdk-coin-trx/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
29 changes: 22 additions & 7 deletions modules/sdk-coin-trx/src/trx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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');
Expand Down
10 changes: 7 additions & 3 deletions modules/sdk-coin-trx/src/trxToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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');
Expand Down
29 changes: 22 additions & 7 deletions modules/sdk-coin-trx/test/unit/trx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
47 changes: 45 additions & 2 deletions modules/sdk-coin-trx/test/unit/trxToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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({
Expand All @@ -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);
});
});
});
59 changes: 59 additions & 0 deletions modules/sdk-coin-trx/test/unit/verifyTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading