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
2 changes: 1 addition & 1 deletion .iyarc
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
# - This CVE affects archive EXTRACTION (unpacking malicious symlinks/hardlinks)
# - Lerna only uses tar for PACKING
GHSA-8qq5-rm4j-mr97

GHSA-r6q2-hw4h-h46w
17 changes: 15 additions & 2 deletions modules/sdk-coin-trx/src/lib/delegateResourceTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,25 @@ export class DelegateResourceTxBuilder extends ResourceManagementTxBuilder {
* @returns {string} the delegate resource transaction raw data hex
*/
protected getResourceManagementTxRawDataHex(): string {
const rawContract = {
const rawContract: {
ownerAddress: number[];
receiverAddress: number[];
balance: string;
resource?: string;
} = {
ownerAddress: getByteArrayFromHexAddress(this._ownerAddress),
receiverAddress: getByteArrayFromHexAddress(this._receiverAddress),
balance: this._balance,
resource: this._resource,
};

// Only include resource if it's not BANDWIDTH (the default value = 0)
// In protobuf3, default values are typically not encoded in the wire format.
// TRON's node re-serializes transactions and omits default values,
// so we must match that behavior to ensure consistent transaction hashes.
if (this._resource !== 'BANDWIDTH') {
rawContract.resource = this._resource;
}

const delegateResourceContract = protocol.DelegateResourceContract.fromObject(rawContract);
const delegateResourceContractBytes = protocol.DelegateResourceContract.encode(delegateResourceContract).finish();
const txContract = {
Expand Down
16 changes: 14 additions & 2 deletions modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,23 @@ export class FreezeBalanceTxBuilder extends TransactionBuilder {
* @returns {string} the freeze balance transaction raw data hex
*/
private getFreezeRawDataHex(): string {
const rawContract = {
const rawContract: {
ownerAddress: number[];
frozenBalance: string;
resource?: string;
} = {
ownerAddress: getByteArrayFromHexAddress(this._ownerAddress),
frozenBalance: this._frozenBalance,
resource: this._resource,
};

// Only include resource if it's not BANDWIDTH (the default value = 0)
// In protobuf3, default values are typically not encoded in the wire format.
// TRON's node re-serializes transactions and omits default values,
// so we must match that behavior to ensure consistent transaction hashes.
if (this._resource !== 'BANDWIDTH') {
rawContract.resource = this._resource;
}

const freezeContract = protocol.FreezeBalanceV2Contract.fromObject(rawContract);
const freezeContractBytes = protocol.FreezeBalanceV2Contract.encode(freezeContract).finish();
const txContract = {
Expand Down
17 changes: 15 additions & 2 deletions modules/sdk-coin-trx/src/lib/undelegateResourceTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,25 @@ export class UndelegateResourceTxBuilder extends ResourceManagementTxBuilder {
* @returns {string} the undelegate resource transaction raw data hex
*/
protected getResourceManagementTxRawDataHex(): string {
const rawContract = {
const rawContract: {
ownerAddress: number[];
receiverAddress: number[];
balance: string;
resource?: string;
} = {
ownerAddress: getByteArrayFromHexAddress(this._ownerAddress),
receiverAddress: getByteArrayFromHexAddress(this._receiverAddress),
balance: this._balance,
resource: this._resource,
};

// Only include resource if it's not BANDWIDTH (the default value = 0)
// In protobuf3, default values are typically not encoded in the wire format.
// TRON's node re-serializes transactions and omits default values,
// so we must match that behavior to ensure consistent transaction hashes.
if (this._resource !== 'BANDWIDTH') {
rawContract.resource = this._resource;
}

const undelegateResourceContract = protocol.UnDelegateResourceContract.fromObject(rawContract);
const undelegateResourceContractBytes =
protocol.UnDelegateResourceContract.encode(undelegateResourceContract).finish();
Expand Down
16 changes: 14 additions & 2 deletions modules/sdk-coin-trx/src/lib/unfreezeBalanceTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,23 @@ export class UnfreezeBalanceTxBuilder extends TransactionBuilder {
* @returns {string} the freeze balance transaction raw data hex
*/
private getUnfreezeRawDataHex(): string {
const rawContract = {
const rawContract: {
ownerAddress: number[];
unfreezeBalance: string;
resource?: string;
} = {
ownerAddress: getByteArrayFromHexAddress(this._ownerAddress),
unfreezeBalance: this._unfreezeBalance,
resource: this._resource,
};

// Only include resource if it's not BANDWIDTH (the default value = 0)
// In protobuf3, default values are typically not encoded in the wire format.
// TRON's node re-serializes transactions and omits default values,
// so we must match that behavior to ensure consistent transaction hashes.
if (this._resource !== 'BANDWIDTH') {
rawContract.resource = this._resource;
}

const unfreezeContract = protocol.UnfreezeBalanceV2Contract.fromObject(rawContract);
const unfreezeContractBytes = protocol.UnfreezeBalanceV2Contract.encode(unfreezeContract).finish();
const txContract = {
Expand Down
35 changes: 16 additions & 19 deletions modules/sdk-coin-trx/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,6 @@ export function decodeFreezeBalanceV2Contract(base64: string): FreezeBalanceCont
throw new UtilsError('Owner address does not exist in this freeze contract.');
}

if (freezeContract.resource === undefined) {
throw new UtilsError('Resource type does not exist in this freeze contract.');
}

if (freezeContract.frozenBalance === undefined) {
throw new UtilsError('Frozen balance does not exist in this freeze contract.');
}
Expand All @@ -459,7 +455,12 @@ export function decodeFreezeBalanceV2Contract(base64: string): FreezeBalanceCont
getByteArrayFromHexAddress(Buffer.from(freezeContract.ownerAddress, 'base64').toString('hex'))
);

const resourceValue = freezeContract.resource === 'BANDWIDTH' ? TronResource.BANDWIDTH : TronResource.ENERGY;
// In protobuf3, default values (BANDWIDTH = 0) may be omitted from serialization.
// If resource is undefined, default to BANDWIDTH.
const resourceValue =
freezeContract.resource === undefined || freezeContract.resource === 'BANDWIDTH'
? TronResource.BANDWIDTH
: TronResource.ENERGY;

return [
{
Expand Down Expand Up @@ -549,10 +550,6 @@ export function decodeUnfreezeBalanceV2Contract(base64: string): UnfreezeBalance
throw new UtilsError('Owner address does not exist in this unfreeze contract.');
}

if (unfreezeContract.resource === undefined) {
throw new UtilsError('Resource type does not exist in this unfreeze contract.');
}

if (unfreezeContract.unfreezeBalance === undefined) {
throw new UtilsError('Unfreeze balance does not exist in this unfreeze contract.');
}
Expand All @@ -562,9 +559,13 @@ export function decodeUnfreezeBalanceV2Contract(base64: string): UnfreezeBalance
getByteArrayFromHexAddress(Buffer.from(unfreezeContract.ownerAddress, 'base64').toString('hex'))
);

// Convert ResourceCode enum value to string resource name
// In protobuf3, default values (BANDWIDTH = 0) may be omitted from serialization.
// If resource is undefined, default to BANDWIDTH.
const resourceValue = unfreezeContract.resource;
const resourceEnum = resourceValue === protocol.ResourceCode.BANDWIDTH ? TronResource.BANDWIDTH : TronResource.ENERGY;
const resourceEnum =
resourceValue === undefined || resourceValue === protocol.ResourceCode.BANDWIDTH
? TronResource.BANDWIDTH
: TronResource.ENERGY;

return [
{
Expand Down Expand Up @@ -670,10 +671,6 @@ export function decodeDelegateResourceContract(base64: string): ResourceManageme
throw new UtilsError('Receiver address does not exist in this delegate resource contract.');
}

if (delegateResourceContract.resource === undefined) {
throw new UtilsError('Resource type does not exist in this delegate resource contract.');
}

if (delegateResourceContract.balance === undefined) {
throw new UtilsError('Balance does not exist in this delegate resource contract.');
}
Expand All @@ -686,6 +683,8 @@ export function decodeDelegateResourceContract(base64: string): ResourceManageme
getByteArrayFromHexAddress(Buffer.from(delegateResourceContract.receiverAddress, 'base64').toString('hex'))
);

// In protobuf3, default values (BANDWIDTH = 0) may be omitted from serialization.
// If resource is undefined or falsy, default to BANDWIDTH.
const resourceValue = !delegateResourceContract.resource ? TronResource.BANDWIDTH : TronResource.ENERGY;

return [
Expand Down Expand Up @@ -724,10 +723,6 @@ export function decodeUnDelegateResourceContract(base64: string): ResourceManage
throw new UtilsError('Receiver address does not exist in this delegate resource contract.');
}

if (undelegateResourceContract.resource === undefined) {
throw new UtilsError('Resource type does not exist in this delegate resource contract.');
}

if (undelegateResourceContract.balance === undefined) {
throw new UtilsError('Balance does not exist in this delegate resource contract.');
}
Expand All @@ -740,6 +735,8 @@ export function decodeUnDelegateResourceContract(base64: string): ResourceManage
getByteArrayFromHexAddress(Buffer.from(undelegateResourceContract.receiverAddress, 'base64').toString('hex'))
);

// In protobuf3, default values (BANDWIDTH = 0) may be omitted from serialization.
// If resource is undefined or falsy, default to BANDWIDTH.
const resourceValue = !undelegateResourceContract.resource ? TronResource.BANDWIDTH : TronResource.ENERGY;

return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
BLOCK_NUMBER,
EXPIRATION,
RESOURCE_ENERGY,
RESOURCE_BANDWIDTH,
DELEGATION_BALANCE,
DELEGATE_RESOURCE_CONTRACT,
} from '../../resources';
Expand Down Expand Up @@ -323,4 +324,133 @@ describe('Tron DelegateResource builder', function () {
});
});
});

describe('BANDWIDTH serialization (protobuf3 compatibility)', () => {
it('should round-trip BANDWIDTH transactions correctly', async () => {
// Build a BANDWIDTH transaction
const builder = (getBuilder('ttrx') as WrappedBuilder).getDelegateResourceTxBuilder();
builder
.source({ address: PARTICIPANTS.from.address })
.setReceiverAddress({ address: PARTICIPANTS.to.address })
.block({ number: BLOCK_NUMBER, hash: BLOCK_HASH })
.setBalance(DELEGATION_BALANCE)
.setResource(RESOURCE_BANDWIDTH);

const tx = await builder.build();
const txJson = tx.toJson();

// Verify the built transaction has BANDWIDTH resource
assert.equal(
txJson.raw_data.contract[0].parameter.value.resource,
RESOURCE_BANDWIDTH,
'Built transaction should have BANDWIDTH resource'
);

// Round-trip: deserialize from broadcast format and rebuild
const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat());
const tx2 = await builder2.build();
const tx2Json = tx2.toJson();

// Verify resource is preserved after round-trip
assert.equal(
tx2Json.raw_data.contract[0].parameter.value.resource,
RESOURCE_BANDWIDTH,
'Resource should be BANDWIDTH after round-trip from broadcast format'
);

// Round-trip: deserialize from JSON and rebuild
const builder3 = getBuilder('ttrx').from(tx.toJson());
const tx3 = await builder3.build();
const tx3Json = tx3.toJson();

// Verify resource is preserved after round-trip from JSON
assert.equal(
tx3Json.raw_data.contract[0].parameter.value.resource,
RESOURCE_BANDWIDTH,
'Resource should be BANDWIDTH after round-trip from JSON'
);
});

it('should round-trip ENERGY transactions correctly', async () => {
// Build an ENERGY transaction
const builder = (getBuilder('ttrx') as WrappedBuilder).getDelegateResourceTxBuilder();
builder
.source({ address: PARTICIPANTS.from.address })
.setReceiverAddress({ address: PARTICIPANTS.to.address })
.block({ number: BLOCK_NUMBER, hash: BLOCK_HASH })
.setBalance(DELEGATION_BALANCE)
.setResource(RESOURCE_ENERGY);

const tx = await builder.build();
const txJson = tx.toJson();

// Verify the built transaction has ENERGY resource
assert.equal(
txJson.raw_data.contract[0].parameter.value.resource,
RESOURCE_ENERGY,
'Built transaction should have ENERGY resource'
);

// Round-trip: deserialize from broadcast format and rebuild
const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat());
const tx2 = await builder2.build();
const tx2Json = tx2.toJson();

// Verify resource is preserved after round-trip
assert.equal(
tx2Json.raw_data.contract[0].parameter.value.resource,
RESOURCE_ENERGY,
'Resource should be ENERGY after round-trip from broadcast format'
);
});

it('should produce consistent transaction IDs for BANDWIDTH transactions', async () => {
// Build a BANDWIDTH transaction
const builder = (getBuilder('ttrx') as WrappedBuilder).getDelegateResourceTxBuilder();
builder
.source({ address: PARTICIPANTS.from.address })
.setReceiverAddress({ address: PARTICIPANTS.to.address })
.block({ number: BLOCK_NUMBER, hash: BLOCK_HASH })
.setBalance(DELEGATION_BALANCE)
.setResource(RESOURCE_BANDWIDTH);

const tx = await builder.build();
const originalTxId = tx.toJson().txID;

// Round-trip and verify txID is consistent
const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat());
const tx2 = await builder2.build();
const roundTripTxId = tx2.toJson().txID;

assert.equal(originalTxId, roundTripTxId, 'Transaction ID should be consistent after round-trip');
});

it('should allow signing BANDWIDTH transactions after round-trip', async () => {
// Build a BANDWIDTH transaction
const builder = (getBuilder('ttrx') as WrappedBuilder).getDelegateResourceTxBuilder();
builder
.source({ address: PARTICIPANTS.from.address })
.setReceiverAddress({ address: PARTICIPANTS.to.address })
.block({ number: BLOCK_NUMBER, hash: BLOCK_HASH })
.setBalance(DELEGATION_BALANCE)
.setResource(RESOURCE_BANDWIDTH);

const tx = await builder.build();

// Round-trip and sign
const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat());
builder2.sign({ key: PARTICIPANTS.from.pk });
const signedTx = await builder2.build();

// Verify signature was added
assert.equal(signedTx.toJson().signature.length, 1, 'Transaction should have one signature');

// Verify resource is still BANDWIDTH
assert.equal(
signedTx.toJson().raw_data.contract[0].parameter.value.resource,
RESOURCE_BANDWIDTH,
'Resource should still be BANDWIDTH after signing'
);
});
});
});
Loading