Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -287,28 +287,16 @@ The `executionEffort` is also supplied as an argument in the transaction. This r

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

To create the vault, the `estimate()` function calculates the amount needed:
To create the vault, the `calculateFee()` function calculates the amount needed:

```cadence
let est = FlowTransactionScheduler.estimate(
data: transactionData,
timestamp: future,
priority: pr,
executionEffort: executionEffort
let est = FlowTransactionScheduler.calculateFee(
executionEffort: executionEffort, priority: pr, dataSizeMB: 0
)
```

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.

Finally, we also `assert` that some minimums are met to ensure the transaction will be called:

```cadence
assert(
est.timestamp != nil || pr == FlowTransactionScheduler.Priority.Low,
message: est.error ?? "estimation failed"
)
```

## Use the FlowTransactionSchedulerUtils.Manager

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

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

```cadence
let estimate = FlowTransactionScheduler.estimate(
data: data,
timestamp: future,
priority: priority,
executionEffort: executionEffort
)

assert(
estimate.timestamp != nil || priority == FlowTransactionScheduler.Priority.Low,
message: estimate.error ?? "estimation failed"
let estimate = FlowTransactionScheduler.calculateFee(
executionEffort: executionEffort, priority: priority, dataSizeMB: 0
)

// Ensure a handler resource exists in the contract account storage
Expand All @@ -512,7 +492,7 @@ Then withdraw the necessary funds:
let vaultRef = CounterLoopTransactionHandler.account.storage
.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
?? panic("missing FlowToken vault on contract account")
let fees <- vaultRef.withdraw(amount: estimate.flowFee ?? 0.0) as! @FlowToken.Vault
let fees <- vaultRef.withdraw(amount: estimate ?? 0.0) as! @FlowToken.Vault
```

Finally, schedule the transaction:
Expand Down Expand Up @@ -609,22 +589,14 @@ transaction(
? FlowTransactionScheduler.Priority.Medium
: FlowTransactionScheduler.Priority.Low

let est = FlowTransactionScheduler.estimate(
data: transactionData,
timestamp: future,
priority: pr,
executionEffort: executionEffort
)

assert(
est.timestamp != nil || pr == FlowTransactionScheduler.Priority.Low,
message: est.error ?? "estimation failed"
let est = FlowTransactionScheduler.calculateFee(
executionEffort: executionEffort, priority: pr, dataSizeMB: 0
)

let vaultRef = signer.storage
.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
?? panic("missing FlowToken vault")
let fees <- vaultRef.withdraw(amount: est.flowFee ?? 0.0) as! @FlowToken.Vault
let fees <- vaultRef.withdraw(amount: est ?? 0.0) as! @FlowToken.Vault

// if a transaction scheduler manager has not been created for this account yet, create one
if !signer.storage.check<@{FlowTransactionSchedulerUtils.Manager}>(from: FlowTransactionSchedulerUtils.managerStoragePath) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Scheduled Transactions Performance Guide

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.

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.

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.

## The Problem: Double Work

Many developers are currently using this pattern when scheduling transactions:

```cadence
// Get an estimate of the fee required for the transaction
// also does a lot of other things
let estimate = FlowTransactionScheduler.estimate(...)
let fee = estimate.fee

// withdraw the fee amount from the account's vault
let feeVault <- authorizerVaultRef.withdraw(amount: fee)

// schedule the transaction
manager.schedule(txData, feeVault, ...)
```

### What happens under the hood

`estimate()` performs these operation:
- Validates transaction data
- Calculates data size
- finds an empty slot that the transaction will fit in
- Computes fee

`manager.schedule()` also does:
- Validates transaction data again
- Calculates data size again
- finds an empty slot that the transaction will fit in again
- Computes the fee
- Actually schedules the transaction

You are doing approximately 70 percent of the work in `schedule()` twice!

Computational Cost Comparison:
- Old way: `estimate()` plus `schedule()` does double work because `schedule()` calls estimate!
- New way: `calculateFee()` plus `schedule()` only calculates the fee twice, which is a trivial operation.
- Result: ~30 percent reduction in computation

## The Solution: Use `FlowTransactionScheduler.calculateFee()`

The new `calculateFee()` function does exactly one thing: calculates the fee.

```cadence
// Get an estimate of the fee required for the transaction
let fee = FlowTransactionScheduler.calculateFee(...)

// withdraw the fee amount from the account's vault
let feeVault <- authorizerVaultRef.withdraw(amount: fee)

manager.schedule(txData, feeVault, ...)
```

Why this works: `manager.schedule()` does all the validation anyway, so you only need the fee upfront. Let `schedule()` handle the rest!

In the long run, this will save a TON on transaction fees, especially if your app is scheduling a lot of recurring transactions!

## Bonus Optimization: Store Known Sizes

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.

If your transaction data is always the same size, stop calculating it every time!

Wasteful approach:
```cadence
// calculate the size of the data, which is an expensive operation
let dataSizeMB = FlowTransactionScheduler.getSizeOfData(txData)
let fee = FlowTransactionScheduler.calculateFee(executionEffort, priority, dataSizeMB)
```

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.

Smart approach:
```cadence
// get the pre-set size of the data from a field in the contract
let dataSizeMB = self.standardTxDataSizeMB
let fee = FlowTransactionScheduler.calculateFee(executionEffort, priority, dataSizeMB)
```

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.

## Real World Examples

### Before: Inefficient Code

```cadence
import FlowTransactionScheduler from 0x1234
import FlowTransactionSchedulerUtils from 0x1234

transaction(
txData: {String: AnyStruct},
executionEffort: UInt64,
priority: FlowTransactionScheduler.Priority
) {
prepare(acct: AuthAccount) {
let manager = acct.borrow<&FlowTransactionSchedulerUtils.Manager>(
from: FlowTransactionSchedulerUtils.ManagerStoragePath
) ?? panic("No manager")

let dataSizeMB = FlowTransactionScheduler.getSizeOfData(txData)
let estimate = FlowTransactionScheduler.estimate(
scheduler: manager.address,
transaction: txData,
...
)
let fee = estimate.fee

// withdraw the fee amount from the account's vault
let feeVault <- authorizerVaultRef.withdraw(amount: fee)

manager.schedule(
transaction: txData,
fee: <-feeVault,
...
)
}
}
```

### After: Optimized Code

```cadence
import FlowTransactionScheduler from 0x1234
import FlowTransactionSchedulerUtils from 0x1234
import MyTransactionHandler from 0x5678

transaction(
txData: {String: AnyStruct},
executionEffort: UInt64,
priority: FlowTransactionScheduler.Priority
) {
prepare(acct: AuthAccount) {
let manager = acct.borrow<&FlowTransactionSchedulerUtils.Manager>(
from: FlowTransactionSchedulerUtils.ManagerStoragePath
) ?? panic("No manager")

let dataSizeMB = MyTransactionHandler.standardDataSize
let fee = FlowTransactionScheduler.calculateFee(
executionEffort: executionEffort,
priority: priority,
dataSizeMB: dataSizeMB
)

// withdraw the fee amount from the account's vault
let feeVault <- authorizerVaultRef.withdraw(amount: fee)

manager.schedule(
transaction: txData,
fee: <-feeVault,
...
)
}
}
```

This is the fastest way if your transaction structure is consistent! Store the size in a configurable field instead of recalculating it every time.

## When to still use `estimate()`

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()`.

## Quick Reference

Do This:
- Use `FlowTransactionScheduler.calculateFee()` for fee estimation
- Store known data sizes in config fields
- Let `manager.schedule()` handle validation and scheduling

Do Not Do This:
- Call `estimate()` just for fees
- Calculate size every transaction
- Validate and schedule twice unnecessarily

Questions? Check out the Scheduled Transactions documentation at https://developers.flow.com/blockchain-development-tutorials/forte/scheduled-transactions/scheduled-transactions-introduction for more details.