diff --git a/modules/sdk-coin-trx/src/lib/tokenTransferBuilder.ts b/modules/sdk-coin-trx/src/lib/tokenTransferBuilder.ts index 50baa6a498..432df08252 100644 --- a/modules/sdk-coin-trx/src/lib/tokenTransferBuilder.ts +++ b/modules/sdk-coin-trx/src/lib/tokenTransferBuilder.ts @@ -2,7 +2,7 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { BaseKey } from '@bitgo/sdk-core'; import { ContractCallBuilder } from './contractCallBuilder'; import { Transaction } from './transaction'; -import { getHexAddressFromBase58Address, encodeDataParams } from './utils'; +import { getHexAddressFromBase58Address, getBase58AddressFromHexAddress, encodeDataParams } from './utils'; // the first 4 bytes of the Keccak-256 encoded function selector used in token transfers, 'transfer(address,uint256)' // this must be concatenated with the encoded parameters, recipientAddress and amount @@ -29,7 +29,9 @@ export class TokenTransferBuilder extends ContractCallBuilder { */ tokenTransferData(recipientAddress: string, amount: string): this { this.validateAddress({ address: recipientAddress }); - const recipientHex = getHexAddressFromBase58Address(recipientAddress); + // accept base58 or hex form — hex inputs must be canonicalized before base58 decoding + const recipientBase58 = getBase58AddressFromHexAddress(recipientAddress); + const recipientHex = getHexAddressFromBase58Address(recipientBase58); const types = ['address', 'uint256']; const values = [recipientHex, amount]; diff --git a/modules/sdk-coin-trx/src/lib/transactionBuilder.ts b/modules/sdk-coin-trx/src/lib/transactionBuilder.ts index 36f88fbcbd..adc1c88908 100644 --- a/modules/sdk-coin-trx/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-trx/src/lib/transactionBuilder.ts @@ -18,6 +18,7 @@ import { Address } from './address'; import { signTransaction, isBase58Address, + isHexAddress, decodeTransaction, VALID_RESOURCE_TYPES, getHexAddressFromBase58Address, @@ -205,8 +206,9 @@ export class TransactionBuilder extends BaseTransactionBuilder { /** @inheritdoc */ validateAddress(address: Address): void { - // assumes a base 58 address for our addresses - if (!isBase58Address(address.address)) { + // TRON addresses are accepted in base58 (T...) or hex (0x... / 41...) form; + // they are alternative encodings of the same 21-byte address. + if (!isBase58Address(address.address) && !isHexAddress(address.address)) { throw new Error(address.address + ' is not a valid base58 address.'); } } diff --git a/modules/sdk-coin-trx/src/lib/utils.ts b/modules/sdk-coin-trx/src/lib/utils.ts index 501fc6b30d..4ebdc77815 100644 --- a/modules/sdk-coin-trx/src/lib/utils.ts +++ b/modules/sdk-coin-trx/src/lib/utils.ts @@ -61,6 +61,34 @@ export function isBase58Address(address: string): boolean { return tronweb.utils.crypto.isAddressValid(address); } +/** + * 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. + * + * @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); +} + +/** + * Converts any valid TRON address representation (base58, 0x-prefixed hex, + * or 41-prefixed hex) to the canonical base58 form. + * + * @param address + */ +export function getBase58AddressFromHexAddress(address: string): string { + if (isBase58Address(address)) { + return address; + } + const bare = address.toLowerCase().replace(/^0x/, ''); + // The 20-byte EVM-style form lacks the 0x41 TRON version prefix. + const hex = bare.length === 40 ? '41' + bare : bare; + return getBase58AddressFromHex(hex); +} + /** * @param str */ @@ -75,6 +103,20 @@ export function getHexAddressFromByteArray(arr: ByteArray): string { return tronweb.utils.code.byteArray2hexStr(arr); } +export function getHexAddressFromBase58Address(base58: string): string { + // pulled from: https://github.com/TRON-US/tronweb/blob/dcb8efa36a5ebb65c4dab3626e90256a453f3b0d/src/utils/help.js#L17 + // but they don't surface this call in index.js + // canonicalizes hex-form inputs (0x... / 41...) to base58 first, so all builders + // accept every valid representation of a TRON address (COINS-1575) + const canonical = getBase58AddressFromHexAddress(base58); + const bytes = tronweb.utils.crypto.decodeBase58Address(canonical); + // Ensure bytes is a ByteArray (number[]) + if (Array.isArray(bytes)) { + return getHexAddressFromByteArray(bytes); + } + throw new UtilsError('Failed to decode base58 address to byte array'); +} + /** * @param messageToVerify * @param base58Address @@ -101,20 +143,6 @@ export function verifySignature( return tronweb.Trx.verifySignature(messageToVerify, base58Address, sigHex, useTronHeader); } - -/** - * @param base58 - */ -export function getHexAddressFromBase58Address(base58: string): string { - // pulled from: https://github.com/TRON-US/tronweb/blob/dcb8efa36a5ebb65c4dab3626e90256a453f3b0d/src/utils/help.js#L17 - // but they don't surface this call in index.js - const bytes = tronweb.utils.crypto.decodeBase58Address(base58); - // Ensure bytes is a ByteArray (number[]) - if (Array.isArray(bytes)) { - return getHexAddressFromByteArray(bytes); - } - throw new UtilsError('Failed to decode base58 address to byte array'); -} /** * @param privateKey */ diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/accountCreateTxBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/accountCreateTxBuilder.ts index 8c2c0d9e47..7a73a971b9 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/accountCreateTxBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/accountCreateTxBuilder.ts @@ -306,7 +306,8 @@ describe('Tron AccountCreate builder', function () { const txBuilder = (getBuilder('ttrx') as WrappedBuilder).getAccountCreateTxBuilder(); assert.throws( () => { - txBuilder.setAccountAddress({ address: '4173a5993cd182ae152adad8203163f780c65a8aa5' }); + // neither base58 nor hex (contains non-hex characters) + txBuilder.setAccountAddress({ address: 'zz73a5993cd182ae152adad8203163f780c65a8aa5' }); }, (e: any) => e.message.includes('is not a valid base58 address') ); diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/tokenTransferBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/tokenTransferBuilder.ts index 468e624df0..db08f3a5bf 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/tokenTransferBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/tokenTransferBuilder.ts @@ -64,6 +64,42 @@ describe('TRX Token Transfer Builder', () => { const rawData = txJson.raw_data; assert.deepStrictEqual(rawData.contract, TOKEN_TX_CONTRACT_2); }); + + // Hex and base58 are alternative encodings of the same TRON address, so every accepted + // form must produce identical transaction data — this is the COINS-1575 regression. + it('accepts a 0x-prefixed hex recipient and encodes the same data as the base58 form', async () => { + const buildData = async (recipient: string) => { + const txBuilder = initTxBuilder(); + txBuilder.tokenTransferData(recipient, '1000000000').sign({ key: PARTICIPANTS.custodian.pk }); + const tx = await txBuilder.build(); + return tx.toJson().raw_data.contract[0].parameter.value.data; + }; + const fromBase58 = await buildData(TOKEN_TRANSFER_RECIPIENT); + // 0x-form of TOKEN_TRANSFER_RECIPIENT (TGai5uHgBcoLERrzDXMepqZB8Et7D8nV8K) + const fromHex = await buildData('0x4887974f42a789ef6d4dfc7ba28b1583219434b3'); + assert.equal(fromHex, fromBase58); + }); + + it('accepts a 41-prefixed hex recipient and encodes the same data as the base58 form', async () => { + const buildData = async (recipient: string) => { + const txBuilder = initTxBuilder(); + txBuilder.tokenTransferData(recipient, '1000000000').sign({ key: PARTICIPANTS.custodian.pk }); + const tx = await txBuilder.build(); + return tx.toJson().raw_data.contract[0].parameter.value.data; + }; + const fromBase58 = await buildData(TOKEN_TRANSFER_RECIPIENT); + // 41-prefixed hex of TOKEN_TRANSFER_RECIPIENT + const fromHex = await buildData('414887974f42a789ef6d4dfc7ba28b1583219434b3'); + assert.equal(fromHex, fromBase58); + }); + + it('still rejects addresses in neither base58 nor hex form with the same error', () => { + const txBuilder = initTxBuilder(); + assert.throws( + () => txBuilder.tokenTransferData('not-an-address', '1000000000'), + (e: unknown) => e instanceof Error && e.message === 'not-an-address is not a valid base58 address.' + ); + }); }); }); }); diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/wrappedBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/wrappedBuilder.ts index d76d16b799..547e8db732 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/wrappedBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/wrappedBuilder.ts @@ -22,21 +22,21 @@ describe('Trx Contract call Builder', () => { ); }); - it('a hex address', () => { - assert.throws( - () => { - builder.validateAddress({ address: '4173a5993cd182ae152adad8203163f780c65a8aa5' }); - }, - (e: any) => e.message === '4173a5993cd182ae152adad8203163f780c65a8aa5 is not a valid base58 address.' - ); + // hex (0x-prefixed / 41-prefixed) and base58 are encodings of the same TRON address + it('a hex address (0x-prefixed)', () => { + assert.doesNotThrow(() => builder.validateAddress({ address: '0x73a5993cd182ae152adad8203163f780c65a8aa5' })); + }); + + it('a hex address (41-prefixed)', () => { + assert.doesNotThrow(() => builder.validateAddress({ address: '4173a5993cd182ae152adad8203163f780c65a8aa5' })); }); - it('a hex address', () => { + it('an address that is neither base58 nor hex', () => { assert.throws( () => { - builder.validateAddress({ address: '4173a5993cd182ae152adad8203163f780c65a8aa5' }); + builder.validateAddress({ address: 'zz73a5993cd182ae152adad8203163f780c65a8aa5' }); }, - (e: any) => e.message === '4173a5993cd182ae152adad8203163f780c65a8aa5 is not a valid base58 address.' + (e: any) => e.message === 'zz73a5993cd182ae152adad8203163f780c65a8aa5 is not a valid base58 address.' ); });