Skip to content

Commit 6065178

Browse files
committed
update guidance for calculateFee
1 parent 93b1b1f commit 6065178

File tree

2 files changed

+192
-38
lines changed

2 files changed

+192
-38
lines changed

docs/blockchain-development-tutorials/forte/scheduled-transactions/scheduled-transactions-introduction.md

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -287,28 +287,16 @@ The `executionEffort` is also supplied as an argument in the transaction. This r
287287

288288
- `fees`: A [vault] containing the appropriate amount of compute unit fees needed to pay for the execution of the scheduled transaction.
289289

290-
To create the vault, the `estimate()` function calculates the amount needed:
290+
To create the vault, the `calculateFee()` function calculates the amount needed:
291291

292292
```cadence
293-
let est = FlowTransactionScheduler.estimate(
294-
data: transactionData,
295-
timestamp: future,
296-
priority: pr,
297-
executionEffort: executionEffort
293+
let est = FlowTransactionScheduler.calculateFee(
294+
executionEffort: executionEffort, priority: pr, dataSizeMB: 0
298295
)
299296
```
300297

301298
Then, an [authorized reference] to the signer's vault is created and used to `withdraw()` the needed funds and [move] them into the `fees` variable, which is then sent in the `schedule()` function call.
302299

303-
Finally, we also `assert` that some minimums are met to ensure the transaction will be called:
304-
305-
```cadence
306-
assert(
307-
est.timestamp != nil || pr == FlowTransactionScheduler.Priority.Low,
308-
message: est.error ?? "estimation failed"
309-
)
310-
```
311-
312300
## Use the FlowTransactionSchedulerUtils.Manager
313301

314302
The `FlowTransactionSchedulerUtils.Manager` resource provides a safer and more convenient way to manage scheduled transactions. Instead of directly calling the `FlowTransactionScheduler` contract,
@@ -477,19 +465,11 @@ let priority = FlowTransactionScheduler.Priority.Medium
477465
let executionEffort: UInt64 = 1000
478466
```
479467

480-
Next, create the `estimate` and `assert` to validate minimums are met, and that the `Handler` exists:
468+
Next, add the `calculateFee()` call to calculate the fee for the scheduled transaction and ensure that a handler for the scheduled transaction exists.
481469

482470
```cadence
483-
let estimate = FlowTransactionScheduler.estimate(
484-
data: data,
485-
timestamp: future,
486-
priority: priority,
487-
executionEffort: executionEffort
488-
)
489-
490-
assert(
491-
estimate.timestamp != nil || priority == FlowTransactionScheduler.Priority.Low,
492-
message: estimate.error ?? "estimation failed"
471+
let estimate = FlowTransactionScheduler.calculateFee(
472+
executionEffort: executionEffort, priority: priority, dataSizeMB: 0
493473
)
494474
495475
// Ensure a handler resource exists in the contract account storage
@@ -512,7 +492,7 @@ Then withdraw the necessary funds:
512492
let vaultRef = CounterLoopTransactionHandler.account.storage
513493
.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
514494
?? panic("missing FlowToken vault on contract account")
515-
let fees <- vaultRef.withdraw(amount: estimate.flowFee ?? 0.0) as! @FlowToken.Vault
495+
let fees <- vaultRef.withdraw(amount: estimate ?? 0.0) as! @FlowToken.Vault
516496
```
517497

518498
Finally, schedule the transaction:
@@ -609,22 +589,14 @@ transaction(
609589
? FlowTransactionScheduler.Priority.Medium
610590
: FlowTransactionScheduler.Priority.Low
611591
612-
let est = FlowTransactionScheduler.estimate(
613-
data: transactionData,
614-
timestamp: future,
615-
priority: pr,
616-
executionEffort: executionEffort
617-
)
618-
619-
assert(
620-
est.timestamp != nil || pr == FlowTransactionScheduler.Priority.Low,
621-
message: est.error ?? "estimation failed"
592+
let est = FlowTransactionScheduler.calculateFee(
593+
executionEffort: executionEffort, priority: pr, dataSizeMB: 0
622594
)
623595
624596
let vaultRef = signer.storage
625597
.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
626598
?? panic("missing FlowToken vault")
627-
let fees <- vaultRef.withdraw(amount: est.flowFee ?? 0.0) as! @FlowToken.Vault
599+
let fees <- vaultRef.withdraw(amount: est ?? 0.0) as! @FlowToken.Vault
628600
629601
// if a transaction scheduler manager has not been created for this account yet, create one
630602
if !signer.storage.check<@{FlowTransactionSchedulerUtils.Manager}>(from: FlowTransactionSchedulerUtils.managerStoragePath) {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Scheduled Transactions Performance Guide
2+
3+
Since the Forte network upgrade, scheduled transactions have been a powerful and useful feature for many developers in the Flow ecosystem, providing tools to automate a ton of functionality and have extremely useful features like on-chain cron jobs.
4+
5+
This feature comes with some things that developers have to consider though. The functionality is complex, and therefore can be expensive if not used properly. Many of the use cases for scheduled transactions are in the DeFi space, which has razor-thin margins. This means that you need to make sure your transactions are efficient as possible to save money on gas fees.
6+
7+
Biggest piece of advice: Stop calling `FlowTransactionScheduler.estimate()` just to get an estimate of the fees for the transaction! Use `FlowTransactionScheduler.calculateFee()` instead and save approximately 30 percent on computation.
8+
9+
## The Problem: Double Work
10+
11+
Many developers are currently using this pattern when scheduling transactions:
12+
13+
```cadence
14+
// Get an estimate of the fee required for the transaction
15+
// also does a lot of other things
16+
let estimate = FlowTransactionScheduler.estimate(...)
17+
let fee = estimate.fee
18+
19+
// withdraw the fee amount from the account's vault
20+
let feeVault <- authorizerVaultRef.withdraw(amount: fee)
21+
22+
// schedule the transaction
23+
manager.schedule(txData, feeVault, ...)
24+
```
25+
26+
### What happens under the hood
27+
28+
`estimate()` performs these operation:
29+
- Validates transaction data
30+
- Calculates data size
31+
- finds an empty slot that the transaction will fit in
32+
- Computes fee
33+
34+
`manager.schedule()` also does:
35+
- Validates transaction data again
36+
- Calculates data size again
37+
- finds an empty slot that the transaction will fit in again
38+
- Computes the fee
39+
- Actually schedules the transaction
40+
41+
You are doing approximately 70 percent of the work in `schedule()` twice!
42+
43+
Computational Cost Comparison:
44+
- Old way: `estimate()` plus `schedule()` does double work because `schedule()` calls estimate!
45+
- New way: `calculateFee()` plus `schedule()` only calculates the fee twice, which is a trivial operation.
46+
- Result: ~30 percent reduction in computation
47+
48+
## The Solution: Use `FlowTransactionScheduler.calculateFee()`
49+
50+
The new `calculateFee()` function does exactly one thing: calculates the fee.
51+
52+
```cadence
53+
// Get an estimate of the fee required for the transaction
54+
let fee = FlowTransactionScheduler.calculateFee(...)
55+
56+
// withdraw the fee amount from the account's vault
57+
let feeVault <- authorizerVaultRef.withdraw(amount: fee)
58+
59+
manager.schedule(txData, feeVault, ...)
60+
```
61+
62+
Why this works: `manager.schedule()` does all the validation anyway, so you only need the fee upfront. Let `schedule()` handle the rest!
63+
64+
In the long run, this will save a TON on transaction fees, especially if your app is scheduling a lot of recurring transactions!
65+
66+
## Bonus Optimization: Store Known Sizes
67+
68+
Scheduled Transactions can provide an optional piece of data when scheduling to be included with the transaction. The user must pay a fee for the storage of this data, so the contract needs to know its size.
69+
70+
If your transaction data is always the same size, stop calculating it every time!
71+
72+
Wasteful approach:
73+
```cadence
74+
// calculate the size of the data, which is an expensive operation
75+
let dataSizeMB = FlowTransactionScheduler.getSizeOfData(txData)
76+
let fee = FlowTransactionScheduler.calculateFee(executionEffort, priority, dataSizeMB)
77+
```
78+
79+
If the data that you are providing when scheduling is the same size every time, you can just store that size in a variable in your contract or somewhere else and just access that field when scheduling, instead of doing redundant operations to calculate the size every time.
80+
81+
Smart approach:
82+
```cadence
83+
// get the pre-set size of the data from a field in the contract
84+
let dataSizeMB = self.standardTxDataSizeMB
85+
let fee = FlowTransactionScheduler.calculateFee(executionEffort, priority, dataSizeMB)
86+
```
87+
88+
Pro tip: If your scheduled transaction payload is standardized with same fields and similar values, calculate the size once and store it in a configurable field in your contract or resource.
89+
90+
## Real World Examples
91+
92+
### Before: Inefficient Code
93+
94+
```cadence
95+
import FlowTransactionScheduler from 0x1234
96+
import FlowTransactionSchedulerUtils from 0x1234
97+
98+
transaction(
99+
txData: {String: AnyStruct},
100+
executionEffort: UInt64,
101+
priority: FlowTransactionScheduler.Priority
102+
) {
103+
prepare(acct: AuthAccount) {
104+
let manager = acct.borrow<&FlowTransactionSchedulerUtils.Manager>(
105+
from: FlowTransactionSchedulerUtils.ManagerStoragePath
106+
) ?? panic("No manager")
107+
108+
let dataSizeMB = FlowTransactionScheduler.getSizeOfData(txData)
109+
let estimate = FlowTransactionScheduler.estimate(
110+
scheduler: manager.address,
111+
transaction: txData,
112+
...
113+
)
114+
let fee = estimate.fee
115+
116+
// withdraw the fee amount from the account's vault
117+
let feeVault <- authorizerVaultRef.withdraw(amount: fee)
118+
119+
manager.schedule(
120+
transaction: txData,
121+
fee: <-feeVault,
122+
...
123+
)
124+
}
125+
}
126+
```
127+
128+
### After: Optimized Code
129+
130+
```cadence
131+
import FlowTransactionScheduler from 0x1234
132+
import FlowTransactionSchedulerUtils from 0x1234
133+
import MyTransactionHandler from 0x5678
134+
135+
transaction(
136+
txData: {String: AnyStruct},
137+
executionEffort: UInt64,
138+
priority: FlowTransactionScheduler.Priority
139+
) {
140+
prepare(acct: AuthAccount) {
141+
let manager = acct.borrow<&FlowTransactionSchedulerUtils.Manager>(
142+
from: FlowTransactionSchedulerUtils.ManagerStoragePath
143+
) ?? panic("No manager")
144+
145+
let dataSizeMB = MyTransactionHandler.standardDataSize
146+
let fee = FlowTransactionScheduler.calculateFee(
147+
executionEffort: executionEffort,
148+
priority: priority,
149+
dataSizeMB: dataSizeMB
150+
)
151+
152+
// withdraw the fee amount from the account's vault
153+
let feeVault <- authorizerVaultRef.withdraw(amount: fee)
154+
155+
manager.schedule(
156+
transaction: txData,
157+
fee: <-feeVault,
158+
...
159+
)
160+
}
161+
}
162+
```
163+
164+
This is the fastest way if your transaction structure is consistent! Store the size in a configurable field instead of recalculating it every time.
165+
166+
## When to still use `estimate()`
167+
168+
Use `estimate()` only when you need to validate the entire transaction before scheduling, such as in a UI where users need to see validation errors before submitting. For simple fee calculation, always use `calculateFee()`.
169+
170+
## Quick Reference
171+
172+
Do This:
173+
- Use `FlowTransactionScheduler.calculateFee()` for fee estimation
174+
- Store known data sizes in config fields
175+
- Let `manager.schedule()` handle validation and scheduling
176+
177+
Do Not Do This:
178+
- Call `estimate()` just for fees
179+
- Calculate size every transaction
180+
- Validate and schedule twice unnecessarily
181+
182+
Questions? Check out the Scheduled Transactions documentation at https://developers.flow.com/blockchain-development-tutorials/forte/scheduled-transactions/scheduled-transactions-introduction for more details.

0 commit comments

Comments
 (0)