From a3ddc144008f0184a776935f24c07463c9e45cfc Mon Sep 17 00:00:00 2001 From: Ben Kelcher Date: Sat, 21 Jun 2025 22:19:22 -0400 Subject: [PATCH] fix(sdk-coin-trx): fees on freeze, vote transactions SC-2215 --- .../src/lib/freezeBalanceTxBuilder.ts | 31 +++++++++++++++++-- .../src/lib/voteWitnessTxBuilder.ts | 31 +++++++++++++++++-- .../freezeBalanceTxBuilder.ts | 18 ++++++++--- .../voteWitnessTxBuilder.ts | 15 ++++++--- 4 files changed, 81 insertions(+), 14 deletions(-) diff --git a/modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts b/modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts index ff99ad4667..37ffc0c236 100644 --- a/modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts +++ b/modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts @@ -1,9 +1,16 @@ import { createHash } from 'crypto'; -import { TransactionType, BaseKey, ExtendTransactionError, BuildTransactionError, SigningError } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { + TransactionType, + BaseKey, + ExtendTransactionError, + BuildTransactionError, + SigningError, + InvalidParameterValueError, +} from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig, TronNetwork } from '@bitgo/statics'; import { TransactionBuilder } from './transactionBuilder'; import { Transaction } from './transaction'; -import { TransactionReceipt, FreezeBalanceV2Contract } from './iface'; +import { Fee, TransactionReceipt, FreezeBalanceV2Contract } from './iface'; import { decodeTransaction, getByteArrayFromHexAddress, @@ -14,11 +21,13 @@ import { import { protocol } from '../../resources/protobuf/tron'; import ContractType = protocol.Transaction.Contract.ContractType; +import BigNumber from 'bignumber.js'; export class FreezeBalanceTxBuilder extends TransactionBuilder { protected _signingKeys: BaseKey[]; private _frozenBalance: string; private _resource: string; + private _fee: Fee; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -93,12 +102,23 @@ export class FreezeBalanceTxBuilder extends TransactionBuilder { this._refBlockHash = rawData.ref_block_hash; this._expiration = rawData.expiration; this._timestamp = rawData.timestamp; + this._fee = { feeLimit: rawData.fee_limit!.toString() }; this.transaction.setTransactionType(TransactionType.StakingActivate); const contractCall = rawData.contract[0] as FreezeBalanceV2Contract; this.initFreezeContractCall(contractCall); return this; } + fee(fee: Fee): this { + const feeLimit = new BigNumber(fee.feeLimit); + const tronNetwork = this._coinConfig.network as TronNetwork; + if (feeLimit.isNaN() || feeLimit.isLessThan(0) || feeLimit.isGreaterThan(tronNetwork.maxFeeLimit)) { + throw new InvalidParameterValueError('Invalid fee limit value'); + } + this._fee = fee; + return this; + } + /** * Initialize the freeze contract call specific data * @@ -183,6 +203,7 @@ export class FreezeBalanceTxBuilder extends TransactionBuilder { expiration: this._expiration || Date.now() + TRANSACTION_DEFAULT_EXPIRATION, timestamp: this._timestamp || Date.now(), contract: [txContract], + feeLimit: parseInt(this._fee.feeLimit, 10), }; const rawTx = protocol.Transaction.raw.create(raw); return Buffer.from(protocol.Transaction.raw.encode(rawTx).finish()).toString('hex'); @@ -239,5 +260,9 @@ export class FreezeBalanceTxBuilder extends TransactionBuilder { if (!this._refBlockBytes || !this._refBlockHash) { throw new BuildTransactionError('Missing block reference information'); } + + if (!this._fee) { + throw new BuildTransactionError('Missing fee'); + } } } diff --git a/modules/sdk-coin-trx/src/lib/voteWitnessTxBuilder.ts b/modules/sdk-coin-trx/src/lib/voteWitnessTxBuilder.ts index 6b64ffe89c..bc6298e107 100644 --- a/modules/sdk-coin-trx/src/lib/voteWitnessTxBuilder.ts +++ b/modules/sdk-coin-trx/src/lib/voteWitnessTxBuilder.ts @@ -1,9 +1,16 @@ import { createHash } from 'crypto'; -import { TransactionType, BaseKey, BuildTransactionError, SigningError, ExtendTransactionError } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { + TransactionType, + BaseKey, + BuildTransactionError, + SigningError, + ExtendTransactionError, + InvalidParameterValueError, +} from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig, TronNetwork } from '@bitgo/statics'; import { TransactionBuilder } from './transactionBuilder'; import { Transaction } from './transaction'; -import { TransactionReceipt, VoteWitnessData, VoteWitnessContract } from './iface'; +import { TransactionReceipt, VoteWitnessData, VoteWitnessContract, Fee } from './iface'; import { decodeTransaction, getHexAddressFromBase58Address, @@ -15,10 +22,12 @@ import { import { protocol } from '../../resources/protobuf/tron'; import ContractType = protocol.Transaction.Contract.ContractType; +import BigNumber from 'bignumber.js'; export class VoteWitnessTxBuilder extends TransactionBuilder { protected _signingKeys: BaseKey[]; private _votes: VoteWitnessData[]; + private _fee: Fee; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -85,12 +94,23 @@ export class VoteWitnessTxBuilder extends TransactionBuilder { this._refBlockHash = rawData.ref_block_hash; this._expiration = rawData.expiration; this._timestamp = rawData.timestamp; + this._fee = { feeLimit: rawData.fee_limit!.toString() }; this.transaction.setTransactionType(TransactionType.StakingVote); const contractCall = rawData.contract[0] as VoteWitnessContract; this.initVoteWitnessContractCall(contractCall); return this; } + fee(fee: Fee): this { + const feeLimit = new BigNumber(fee.feeLimit); + const tronNetwork = this._coinConfig.network as TronNetwork; + if (feeLimit.isNaN() || feeLimit.isLessThan(0) || feeLimit.isGreaterThan(tronNetwork.maxFeeLimit)) { + throw new InvalidParameterValueError('Invalid fee limit value'); + } + this._fee = fee; + return this; + } + /** * Initialize the votewitnesscontract call specific data * @@ -182,6 +202,7 @@ export class VoteWitnessTxBuilder extends TransactionBuilder { expiration: this._expiration || Date.now() + TRANSACTION_DEFAULT_EXPIRATION, timestamp: this._timestamp || Date.now(), contract: [txContract], + feeLimit: parseInt(this._fee.feeLimit, 10), }; const rawTx = protocol.Transaction.raw.create(raw); return Buffer.from(protocol.Transaction.raw.encode(rawTx).finish()).toString('hex'); @@ -250,5 +271,9 @@ export class VoteWitnessTxBuilder extends TransactionBuilder { if (!this._votes || this._votes.length === 0) { throw new BuildTransactionError('Missing or empty votes array'); } + + if (!this._fee) { + throw new BuildTransactionError('Missing fee'); + } } } diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/freezeBalanceTxBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/freezeBalanceTxBuilder.ts index 56f94f80be..e5481bd3b5 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/freezeBalanceTxBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/freezeBalanceTxBuilder.ts @@ -9,6 +9,7 @@ import { RESOURCE_ENERGY, FROZEN_BALANCE, FREEZE_BALANCE_V2_CONTRACT, + FEE_LIMIT, } from '../../resources'; import { getBuilder } from '../../../src/lib/builder'; import { Transaction, WrappedBuilder } from '../../../src'; @@ -20,7 +21,8 @@ describe('Tron FreezeBalanceV2 builder', function () { .source({ address: PARTICIPANTS.custodian.address }) .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) .setFrozenBalance(FROZEN_BALANCE) - .setResource(RESOURCE_ENERGY); + .setResource(RESOURCE_ENERGY) + .fee({ feeLimit: FEE_LIMIT }); return builder; }; @@ -262,9 +264,10 @@ describe('Tron FreezeBalanceV2 builder', function () { builder .source({ address: PARTICIPANTS.custodian.address }) .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .fee({ feeLimit: FEE_LIMIT }) .setFrozenBalance(FROZEN_BALANCE); builder.setResource('ENERGY'); - assert.doesNotReject(() => { + await assert.doesNotReject(() => { return builder.build(); }); }); @@ -274,9 +277,10 @@ describe('Tron FreezeBalanceV2 builder', function () { builder .source({ address: PARTICIPANTS.custodian.address }) .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .fee({ feeLimit: FEE_LIMIT }) .setFrozenBalance(FROZEN_BALANCE); builder.setResource('BANDWIDTH'); - assert.doesNotReject(() => { + await assert.doesNotReject(() => { return builder.build(); }); }); @@ -287,6 +291,7 @@ describe('Tron FreezeBalanceV2 builder', function () { builder .source({ address: PARTICIPANTS.custodian.address }) .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .fee({ feeLimit: FEE_LIMIT }) .setFrozenBalance(FROZEN_BALANCE); assert.throws(() => builder.setResource(invalidResource), `${invalidResource} is a not valid resource type.`); @@ -315,7 +320,12 @@ describe('Tron FreezeBalanceV2 builder', function () { }); txBuilder.block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }); - assert.doesNotReject(() => { + await assert.rejects(txBuilder.build(), { + message: 'Missing fee', + }); + + txBuilder.fee({ feeLimit: FEE_LIMIT }); + await assert.doesNotReject(() => { return txBuilder.build(); }); }); diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/voteWitnessTxBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/voteWitnessTxBuilder.ts index 3cbb75763e..79297df7a2 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/voteWitnessTxBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/voteWitnessTxBuilder.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import { TransactionType } from '@bitgo/sdk-core'; import { describe, it } from 'node:test'; -import { PARTICIPANTS, BLOCK_HASH, BLOCK_NUMBER, EXPIRATION, VOTE_WITNESS_CONTRACT } from '../../resources'; +import { PARTICIPANTS, BLOCK_HASH, BLOCK_NUMBER, EXPIRATION, VOTE_WITNESS_CONTRACT, FEE_LIMIT } from '../../resources'; import { getBuilder } from '../../../src/lib/builder'; import { Transaction, WrappedBuilder } from '../../../src'; @@ -22,7 +22,8 @@ describe('Tron VoteWitnessContract builder', function () { builder .source({ address: PARTICIPANTS.custodian.address }) .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) - .setVotes(voteArray); + .setVotes(voteArray) + .fee({ feeLimit: FEE_LIMIT }); return builder; }; @@ -264,9 +265,10 @@ describe('Tron VoteWitnessContract builder', function () { builder .source({ address: PARTICIPANTS.custodian.address }) .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .fee({ feeLimit: FEE_LIMIT }) .setVotes(voteArray); - assert.doesNotReject(() => { + await assert.doesNotReject(() => { return builder.build(); }); }); @@ -335,7 +337,12 @@ describe('Tron VoteWitnessContract builder', function () { }); txBuilder.setVotes(voteArray); - assert.doesNotReject(() => { + await assert.rejects(txBuilder.build(), { + message: 'Missing fee', + }); + + txBuilder.fee({ feeLimit: FEE_LIMIT }); + await assert.doesNotReject(() => { return txBuilder.build(); }); });