Skip to content

Commit b5ddafc

Browse files
committed
Rework max fee options in TransactionBuilder & add max fee per byte
1 parent 3d97005 commit b5ddafc

File tree

4 files changed

+83
-37
lines changed

4 files changed

+83
-37
lines changed

packages/cashscript/src/TransactionBuilder.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { WcTransactionObject } from './walletconnect-utils.js';
3838

3939
export interface TransactionBuilderOptions {
4040
provider: NetworkProvider;
41+
maximumFeeSatoshis?: bigint;
42+
maximumFeeSatsPerByte?: number;
4143
}
4244

4345
const DEFAULT_SEQUENCE = 0xfffffffe;
@@ -48,10 +50,9 @@ export class TransactionBuilder {
4850
public outputs: Output[] = [];
4951

5052
public locktime: number = 0;
51-
public maxFee?: bigint;
5253

5354
constructor(
54-
options: TransactionBuilderOptions,
55+
private options: TransactionBuilderOptions,
5556
) {
5657
this.provider = options.provider;
5758
}
@@ -102,26 +103,26 @@ export class TransactionBuilder {
102103
return this;
103104
}
104105

105-
setMaxFee(maxFee: bigint): this {
106-
this.maxFee = maxFee;
107-
return this;
108-
}
109-
110-
private checkMaxFee(): void {
111-
if (!this.maxFee) return;
112-
106+
private checkMaxFee(transaction: LibauthTransaction): void {
113107
const totalInputAmount = this.inputs.reduce((total, input) => total + input.satoshis, 0n);
114108
const totalOutputAmount = this.outputs.reduce((total, output) => total + output.amount, 0n);
115109
const fee = totalInputAmount - totalOutputAmount;
116110

117-
if (fee > this.maxFee) {
118-
throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.maxFee}`);
111+
if (this.options.maximumFeeSatoshis && fee > this.options.maximumFeeSatoshis) {
112+
throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.options.maximumFeeSatoshis}`);
113+
}
114+
115+
if (this.options.maximumFeeSatsPerByte) {
116+
const transactionSize = encodeTransaction(transaction).byteLength;
117+
const feePerByte = Number(fee) / transactionSize;
118+
119+
if (feePerByte > this.options.maximumFeeSatsPerByte) {
120+
throw new Error(`Transaction fee per byte of ${feePerByte} is higher than max fee per byte of ${this.options.maximumFeeSatsPerByte}`);
121+
}
119122
}
120123
}
121124

122125
buildLibauthTransaction(): LibauthTransaction {
123-
this.checkMaxFee();
124-
125126
const inputs: LibauthTransaction['inputs'] = this.inputs.map((utxo) => ({
126127
outpointIndex: utxo.vout,
127128
outpointTransactionHash: hexToBin(utxo.txid),
@@ -149,6 +150,8 @@ export class TransactionBuilder {
149150
transaction.inputs[i].unlockingBytecode = script;
150151
});
151152

153+
this.checkMaxFee(transaction);
154+
152155
return transaction;
153156
}
154157

packages/cashscript/test/TransactionBuilder.test.ts

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ describe('Transaction Builder', () => {
8080
expect(txOutputs).toEqual(expect.arrayContaining(outputs));
8181
});
8282

83-
it('should fail when fee is higher than maxFee', async () => {
83+
it('should fail when fee is higher than maximumFeeSatoshis', async () => {
8484
const fee = 2000n;
85-
const maxFee = 1000n;
85+
const maximumFeeSatoshis = 1000n;
8686
const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse();
8787

8888
const amount = p2pkhUtxos[0].satoshis - fee;
@@ -93,17 +93,16 @@ describe('Transaction Builder', () => {
9393
}
9494

9595
expect(() => {
96-
new TransactionBuilder({ provider })
96+
new TransactionBuilder({ provider, maximumFeeSatoshis })
9797
.addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv)))
9898
.addOutput({ to: p2pkhInstance.address, amount })
99-
.setMaxFee(maxFee)
10099
.build();
101-
}).toThrow(`Transaction fee of ${fee} is higher than max fee of ${maxFee}`);
100+
}).toThrow(`Transaction fee of ${fee} is higher than max fee of ${maximumFeeSatoshis}`);
102101
});
103102

104-
it('should succeed when fee is lower than maxFee', async () => {
103+
it('should succeed when fee is lower than maximumFeeSatoshis', async () => {
105104
const fee = 1000n;
106-
const maxFee = 2000n;
105+
const maximumFeeSatoshis = 2000n;
107106
const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse();
108107

109108
const amount = p2pkhUtxos[0].satoshis - fee;
@@ -113,10 +112,49 @@ describe('Transaction Builder', () => {
113112
throw new Error('Not enough funds to send transaction');
114113
}
115114

116-
const tx = new TransactionBuilder({ provider })
115+
const tx = new TransactionBuilder({ provider, maximumFeeSatoshis })
116+
.addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv)))
117+
.addOutput({ to: p2pkhInstance.address, amount })
118+
.build();
119+
120+
expect(tx).toBeDefined();
121+
});
122+
123+
it('should fail when fee per byte is higher than maximumFeeSatsPerByte', async () => {
124+
const fee = 2000n;
125+
const maximumFeeSatsPerByte = 1.0;
126+
const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse();
127+
128+
const amount = p2pkhUtxos[0].satoshis - fee;
129+
const dustAmount = calculateDust({ to: p2pkhInstance.address, amount });
130+
131+
if (amount < dustAmount) {
132+
throw new Error('Not enough funds to send transaction');
133+
}
134+
135+
expect(() => {
136+
new TransactionBuilder({ provider, maximumFeeSatsPerByte })
137+
.addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv)))
138+
.addOutput({ to: p2pkhInstance.address, amount })
139+
.build();
140+
}).toThrow(`Transaction fee per byte of 9.05 is higher than max fee per byte of ${maximumFeeSatsPerByte}`);
141+
});
142+
143+
it('should succeed when fee per byte is lower than maximumFeeSatsPerByte', async () => {
144+
const fee = 1000n;
145+
const maximumFeeSatsPerByte = 10.0;
146+
const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse();
147+
148+
const amount = p2pkhUtxos[0].satoshis - fee;
149+
const dustAmount = calculateDust({ to: p2pkhInstance.address, amount });
150+
151+
if (amount < dustAmount) {
152+
throw new Error('Not enough funds to send transaction');
153+
}
154+
155+
const tx = new TransactionBuilder({ provider, maximumFeeSatsPerByte })
117156
.addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv)))
118157
.addOutput({ to: p2pkhInstance.address, amount })
119-
.setMaxFee(maxFee)
120158
.build();
121159

122160
expect(tx).toBeDefined();

website/docs/releases/release-notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ title: Release Notes
99
- :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor.
1010
- :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`).
1111
- :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs.
12+
- :boom: **BREAKING**: Replace `setMaxFee()` method on `TransactionBuilder` with `TransactionBuilderOptions` on the constructor.
13+
- :sparkles: Add `maximumFeeSatsPerByte` option to `TransactionBuilder` constructor.
1214
- :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`.
1315
- :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters.
1416
- :sparkles: Add `signMessageHash()` method to `SignatureTemplate` to allow for signing of non-transaction messages.

website/docs/sdk/transaction-builder.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ Defining the inputs and outputs requires careful consideration because the diffe
1313
new TransactionBuilder(options: TransactionBuilderOptions)
1414
```
1515

16-
To start, you need to instantiate a transaction builder and pass in a `NetworkProvider` instance.
16+
To start, you need to instantiate a transaction builder and pass in a `NetworkProvider` instance and other options.
1717

1818
```ts
1919
interface TransactionBuilderOptions {
2020
provider: NetworkProvider;
21+
maximumFeeSatoshis?: bigint;
22+
maximumFeeSatsPerByte?: number;
2123
}
2224
```
2325

24-
2526
#### Example
2627
```ts
2728
import { ElectrumNetworkProvider, TransactionBuilder, Network } from 'cashscript';
@@ -30,6 +31,20 @@ const provider = new ElectrumNetworkProvider(Network.MAINNET);
3031
const transactionBuilder = new TransactionBuilder({ provider });
3132
```
3233

34+
### Constructor Options
35+
36+
#### provider
37+
38+
The `provider` option is used to specify the network provider to use when sending the transaction.
39+
40+
#### maximumFeeSatoshis
41+
42+
The `maximumFeeSatoshis` option is used to specify the maximum fee for the transaction in satoshis. If this fee is exceeded, an error will be thrown when building the transaction.
43+
44+
#### maximumFeeSatsPerByte
45+
46+
The `maximumFeeSatsPerByte` option is used to specify the maximum fee per byte for the transaction. If this fee is exceeded, an error will be thrown when building the transaction.
47+
3348
## Transaction Building
3449

3550
### addInput()
@@ -159,18 +174,6 @@ Sets the locktime for the transaction to set a transaction-level absolute timelo
159174
transactionBuilder.setLocktime(((Date.now() / 1000) + 24 * 60 * 60) * 1000);
160175
```
161176

162-
### setMaxFee()
163-
```ts
164-
transactionBuilder.setMaxFee(maxFee: bigint): this
165-
```
166-
167-
Sets a max fee for the transaction. Because the transaction builder does not automatically add a change output, you can set a max fee as a safety measure to make sure you don't accidentally pay too much in fees. If the transaction fee exceeds the max fee, an error will be thrown when building the transaction.
168-
169-
#### Example
170-
```ts
171-
transactionBuilder.setMaxFee(1000n);
172-
```
173-
174177
## Completing the Transaction
175178
### send()
176179
```ts

0 commit comments

Comments
 (0)