Skip to content
Merged
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
6 changes: 4 additions & 2 deletions modules/sdk-coin-trx/src/lib/tokenTransferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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];
Expand Down
6 changes: 4 additions & 2 deletions modules/sdk-coin-trx/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Address } from './address';
import {
signTransaction,
isBase58Address,
isHexAddress,
decodeTransaction,
VALID_RESOURCE_TYPES,
getHexAddressFromBase58Address,
Expand Down Expand Up @@ -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.');
}
}
Expand Down
56 changes: 42 additions & 14 deletions modules/sdk-coin-trx/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
Expand All @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
);
});

Expand Down
Loading