Skip to content

Commit f8a0a7f

Browse files
authored
Fix TransactionBuilder to exclude resource fee when deriving base fee in cloneFrom (#1478)
1 parent 7cdbe5a commit f8a0a7f

4 files changed

Lines changed: 101 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ A breaking change will get clearly marked in this log.
66

77
## Unreleased
88

9+
### Fixed
10+
- `TransactionBuilder.cloneFrom` now accounts for Soroban transactions: the
11+
resource fee is excluded from the total fee before deriving the per-operation
12+
base fee (previously it was treated as part of the inclusion fee and re-added
13+
on `build()`, doubling it), and the transaction's `sorobanData` is carried
14+
over into the new builder. If the declared resource fee equals or exceeds the
15+
total fee (a malformed transaction), the subtraction is skipped and the full
16+
fee is used as-is.
17+
918
## [v16.0.0-rc.2](https://github.com/stellar/js-stellar-sdk/compare/v16.0.0-rc.1...v16.0.0-rc.2)
1019

1120
### Changed

docs/reference/core-transactions.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2964,7 +2964,7 @@ static buildFeeBumpTransaction(feeSource: string | Keypair, baseFee: string, inn
29642964

29652965
- https://developers.stellar.org/docs/glossary/fee-bumps/#replace-by-fee
29662966

2967-
**Source:** [src/base/transaction_builder.ts:1092](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L1092)
2967+
**Source:** [src/base/transaction_builder.ts:1114](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L1114)
29682968

29692969
### `TransactionBuilder.cloneFrom(tx, opts)`
29702970

@@ -3014,7 +3014,7 @@ static fromXDR(envelope: string | TransactionEnvelope, networkPassphrase: string
30143014
Stellar network (e.g. "Public Global Stellar Network ; September
30153015
2015"), see `Networks`.
30163016

3017-
**Source:** [src/base/transaction_builder.ts:1203](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L1203)
3017+
**Source:** [src/base/transaction_builder.ts:1225](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L1225)
30183018

30193019
### `transactionBuilder.baseFee`
30203020

@@ -3124,7 +3124,7 @@ addMemo(memo: Memo): TransactionBuilder;
31243124

31253125
- **`memo`**`Memo` (required) — `Memo` object
31263126

3127-
**Source:** [src/base/transaction_builder.ts:394](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L394)
3127+
**Source:** [src/base/transaction_builder.ts:416](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L416)
31283128

31293129
### `transactionBuilder.addOperation(operation)`
31303130

@@ -3138,7 +3138,7 @@ addOperation(operation: Operation2): TransactionBuilder;
31383138

31393139
- **`operation`**`Operation2` (required) — The xdr operation object, use `Operation` static methods.
31403140

3141-
**Source:** [src/base/transaction_builder.ts:356](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L356)
3141+
**Source:** [src/base/transaction_builder.ts:378](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L378)
31423142

31433143
### `transactionBuilder.addOperationAt(operation, index)`
31443144

@@ -3153,7 +3153,7 @@ addOperationAt(operation: Operation2, index: number): TransactionBuilder;
31533153
- **`operation`**`Operation2` (required) — The xdr operation object to add, use `Operation` static methods.
31543154
- **`index`**`number` (required) — The index at which to insert the operation.
31553155

3156-
**Source:** [src/base/transaction_builder.ts:367](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L367)
3156+
**Source:** [src/base/transaction_builder.ts:389](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L389)
31573157

31583158
### `transactionBuilder.addSacTransferOperation(destination, asset, amount, sorobanFees)`
31593159

@@ -3172,7 +3172,7 @@ addSacTransferOperation(destination: string, asset: Asset, amount: string | bigi
31723172
- **`amount`**`string | bigint` (required) — the amount of tokens to be transferred in 7 decimals. IE 1 token with 7 decimals of precision would be represented as "1_0000000"
31733173
- **`sorobanFees`**`SorobanFees` (optional) — optional Soroban fees for the transaction to override the default fees used
31743174

3175-
**Source:** [src/base/transaction_builder.ts:701](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L701)
3175+
**Source:** [src/base/transaction_builder.ts:723](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L723)
31763176

31773177
### `transactionBuilder.build()`
31783178

@@ -3183,7 +3183,7 @@ number by 1.
31833183
build(): Transaction;
31843184
```
31853185

3186-
**Source:** [src/base/transaction_builder.ts:921](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L921)
3186+
**Source:** [src/base/transaction_builder.ts:943](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L943)
31873187

31883188
### `transactionBuilder.clearOperationAt(index)`
31893189

@@ -3197,7 +3197,7 @@ clearOperationAt(index: number): TransactionBuilder;
31973197

31983198
- **`index`**`number` (required) — The index of the operation to remove.
31993199

3200-
**Source:** [src/base/transaction_builder.ts:385](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L385)
3200+
**Source:** [src/base/transaction_builder.ts:407](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L407)
32013201

32023202
### `transactionBuilder.clearOperations()`
32033203

@@ -3207,7 +3207,7 @@ Removes the operations from the builder (useful when cloning).
32073207
clearOperations(): TransactionBuilder;
32083208
```
32093209

3210-
**Source:** [src/base/transaction_builder.ts:375](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L375)
3210+
**Source:** [src/base/transaction_builder.ts:397](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L397)
32113211

32123212
### `transactionBuilder.hasV2Preconditions()`
32133213

@@ -3217,7 +3217,7 @@ Checks whether any v2 preconditions have been set on this builder.
32173217
hasV2Preconditions(): boolean;
32183218
```
32193219

3220-
**Source:** [src/base/transaction_builder.ts:1060](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L1060)
3220+
**Source:** [src/base/transaction_builder.ts:1082](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L1082)
32213221

32223222
### `transactionBuilder.setExtraSigners(extraSigners)`
32233223

@@ -3234,7 +3234,7 @@ setExtraSigners(extraSigners: string[]): TransactionBuilder;
32343234

32353235
- **`extraSigners`**`string[]` (required) — required extra signers (as `StrKey`s)
32363236

3237-
**Source:** [src/base/transaction_builder.ts:636](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L636)
3237+
**Source:** [src/base/transaction_builder.ts:658](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L658)
32383238

32393239
### `transactionBuilder.setLedgerbounds(minLedger, maxLedger)`
32403240

@@ -3255,7 +3255,7 @@ setLedgerbounds(minLedger: number, maxLedger: number): TransactionBuilder;
32553255
before. Cannot be negative. If the value is `0`, the transaction is
32563256
valid indefinitely.
32573257

3258-
**Source:** [src/base/transaction_builder.ts:524](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L524)
3258+
**Source:** [src/base/transaction_builder.ts:546](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L546)
32593259

32603260
### `transactionBuilder.setMinAccountSequence(minAccountSequence)`
32613261

@@ -3279,7 +3279,7 @@ setMinAccountSequence(minAccountSequence: string): TransactionBuilder;
32793279
default), the transaction is valid when `sourceAccount`'s sequence
32803280
number `== tx.seqNum - 1`.
32813281

3282-
**Source:** [src/base/transaction_builder.ts:561](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L561)
3282+
**Source:** [src/base/transaction_builder.ts:583](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L583)
32833283

32843284
### `transactionBuilder.setMinAccountSequenceAge(durationInSeconds)`
32853285

@@ -3298,7 +3298,7 @@ setMinAccountSequenceAge(durationInSeconds: bigint): TransactionBuilder;
32983298
will become valid. If the value is `0`, the transaction is unrestricted
32993299
by the account sequence age. Cannot be negative.
33003300

3301-
**Source:** [src/base/transaction_builder.ts:583](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L583)
3301+
**Source:** [src/base/transaction_builder.ts:605](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L605)
33023302

33033303
### `transactionBuilder.setMinAccountSequenceLedgerGap(gap)`
33043304

@@ -3317,7 +3317,7 @@ setMinAccountSequenceLedgerGap(gap: number): TransactionBuilder;
33173317
If the value is `0`, the transaction is unrestricted by the account
33183318
sequence ledger. Cannot be negative.
33193319

3320-
**Source:** [src/base/transaction_builder.ts:612](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L612)
3320+
**Source:** [src/base/transaction_builder.ts:634](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L634)
33213321

33223322
### `transactionBuilder.setNetworkPassphrase(networkPassphrase)`
33233323

@@ -3332,7 +3332,7 @@ setNetworkPassphrase(networkPassphrase: string): TransactionBuilder;
33323332
- **`networkPassphrase`**`string` (required) — passphrase of the target Stellar
33333333
network (e.g. "Public Global Stellar Network ; September 2015").
33343334

3335-
**Source:** [src/base/transaction_builder.ts:662](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L662)
3335+
**Source:** [src/base/transaction_builder.ts:684](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L684)
33363336

33373337
### `transactionBuilder.setSorobanData(sorobanData)`
33383338

@@ -3360,7 +3360,7 @@ setSorobanData(sorobanData: string | SorobanTransactionData): TransactionBuilder
33603360

33613361
- `SorobanDataBuilder`
33623362

3363-
**Source:** [src/base/transaction_builder.ts:684](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L684)
3363+
**Source:** [src/base/transaction_builder.ts:706](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L706)
33643364

33653365
### `transactionBuilder.setTimebounds(minEpochOrDate, maxEpochOrDate)`
33663366

@@ -3384,7 +3384,7 @@ setTimebounds(minEpochOrDate: number | Date, maxEpochOrDate: number | Date): Tra
33843384
Can't be negative. If the value is `0`, the transaction is valid
33853385
indefinitely.
33863386

3387-
**Source:** [src/base/transaction_builder.ts:475](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L475)
3387+
**Source:** [src/base/transaction_builder.ts:497](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L497)
33883388

33893389
### `transactionBuilder.setTimeout(timeoutSeconds)`
33903390

@@ -3424,7 +3424,7 @@ setTimeout(timeoutSeconds: number): TransactionBuilder;
34243424
- - `TimeoutInfinite`
34253425
- https://developers.stellar.org/docs/tutorials/handling-errors/
34263426

3427-
**Source:** [src/base/transaction_builder.ts:428](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L428)
3427+
**Source:** [src/base/transaction_builder.ts:450](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/transaction_builder.ts#L450)
34283428

34293429
## Uint128
34303430

src/base/transaction_builder.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,16 +306,38 @@ export class TransactionBuilder {
306306
);
307307
}
308308

309+
// if this is a Soroban transaction, the resource fee is folded into the
310+
// total fee and gets re-added on build(), so it has to be excluded before
311+
// deriving the per-operation base fee
312+
let sorobanData: xdr.SorobanTransactionData | undefined;
313+
const envelope = tx.toEnvelope();
314+
if (envelope.switch() === xdr.EnvelopeType.envelopeTypeTx()) {
315+
sorobanData = envelope.v1().tx().ext().value() ?? undefined;
316+
}
317+
let totalFee = parseInt(tx.fee, 10);
318+
if (sorobanData) {
319+
const resourceFee = Number(sorobanData.resourceFee().toBigInt());
320+
// only subtract if it leaves a positive inclusion fee: a malformed tx
321+
// can declare a resource fee that eats the entire fee (or more)
322+
if (totalFee - resourceFee > 0) {
323+
totalFee -= resourceFee;
324+
}
325+
}
326+
309327
// the initial fee passed to the builder gets scaled up based on the number
310328
// of operations at the end, so we have to down-scale first
311-
const unscaledFee = Math.floor(parseInt(tx.fee, 10) / tx.operations.length);
329+
const unscaledFee = Math.floor(totalFee / tx.operations.length);
312330

313331
const builderOpts: TransactionBuilderOptions = {
314332
fee: (unscaledFee || BASE_FEE).toString(),
315333
memo: tx.memo,
316334
networkPassphrase: tx.networkPassphrase,
317335
};
318336

337+
if (sorobanData) {
338+
builderOpts.sorobanData = sorobanData;
339+
}
340+
319341
if (tx.timeBounds) {
320342
builderOpts.timebounds = tx.timeBounds;
321343
}

test/unit/base/transaction_builder.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,56 @@ describe("TransactionBuilder.cloneFrom", () => {
17041704
);
17051705
});
17061706

1707+
it("excludes the resource fee when deriving the base fee", () => {
1708+
const resourceFee = "50000";
1709+
const tx = new TransactionBuilder(source, {
1710+
fee: "150",
1711+
timebounds: { minTime: 0, maxTime: 0 },
1712+
networkPassphrase,
1713+
})
1714+
.addOperation(op)
1715+
.setSorobanData(
1716+
new SorobanDataBuilder().setResourceFee(resourceFee).build(),
1717+
)
1718+
.build();
1719+
1720+
// total fee = baseFee * ops + resourceFee
1721+
expect(tx.fee).toBe("50150");
1722+
1723+
const builder = TransactionBuilder.cloneFrom(tx);
1724+
// base fee must be derived from the inclusion fee only
1725+
expect(builder.baseFee).toBe("150");
1726+
1727+
const cloneTx = builder.build();
1728+
expect(cloneTx.fee).toBe(tx.fee);
1729+
expect(
1730+
expectDefined(cloneTx.toEnvelope().v1().tx().ext().value())
1731+
.resourceFee()
1732+
.toString(),
1733+
).toBe(resourceFee);
1734+
});
1735+
1736+
it("skips the resource fee subtraction when it would zero out the fee", () => {
1737+
const tx = new TransactionBuilder(source, {
1738+
fee: "150",
1739+
timebounds: { minTime: 0, maxTime: 0 },
1740+
networkPassphrase,
1741+
})
1742+
.addOperation(op)
1743+
.setSorobanData(new SorobanDataBuilder().setResourceFee("50000").build())
1744+
.build();
1745+
1746+
// simulate a malformed tx whose declared resource fee exceeds the total
1747+
// fee by patching the XDR fee field directly
1748+
const envelope = tx.toEnvelope();
1749+
envelope.v1().tx().fee(100);
1750+
const malformedTx = new Transaction(envelope, networkPassphrase);
1751+
1752+
const builder = TransactionBuilder.cloneFrom(malformedTx);
1753+
// subtraction would go negative, so the full fee is used instead
1754+
expect(builder.baseFee).toBe("100");
1755+
});
1756+
17071757
it("preserves extraSigners", () => {
17081758
const extraSigner =
17091759
"GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ";

0 commit comments

Comments
 (0)