Introduction to Scheduled Transactions
Scheduled transactions are a new feature that is under development and is a part of FLIP 330. Currently, they only work in the emulator and testnet. The specific implementation is closed to being finished but may change as a part of the development process.
These tutorials will be updated, but you may need to refactor your code if the implementation changes.
Flow, EVM, and other blockchains are a form of a single shared computer that anyone can use and no one has admin privileges, super user roles, or complete control. For this to work, one of the requirements is that it needs to be impossible for any user to freeze the computer, on purpose or by accident.
As a result, most blockchain computers, including EVM and Solana, are not Turing Complete, because they can't run an unbounded loop. Each transaction must take place within one block, and cannot consume more gas than the limit.
While this limitation prevents infinite loops, it makes it so that you can't do anything 100% onchain if you need it to happen at a later time or after a trigger. As a result, developers must often build products that involve a fair amount of traditional infrastructure and requires users to give those developers a great amount of trust that their backend will execute the promised task.
Flow fixes this problem with scheduled transactions. Scheduled Transactions let smart contracts execute code at (or after) a chosen time without an external transaction. You schedule work now; the network executes it later. This enables recurring jobs, deferred actions, and autonomous workflows.
Learning Objectives
After completing this tutorial, you will be able to:
- Understand the concept of scheduled transactions and how they solve blockchain limitations
- Explain the key components of the FlowTransactionScheduler system
- Implement a basic scheduled transaction using the provided scaffold
- Analyze the structure and flow of scheduled transaction transactions
- Create custom scheduled transaction contracts and handlers
- Evaluate the benefits and use cases of scheduled transactions in DeFi applications
Prerequisites
Cadence Programming Language
This tutorial assumes you have a modest knowledge of Cadence. If you don't, you'll be able to follow along, but you'll get more out of it if you complete our series of Cadence tutorials. Most developers find it more pleasant than other blockchain languages and it's not hard to pick up.
Getting Started
Begin by creating a new repo using the Scheduled Transactions Scaffold as a template.
This repository has a robust quickstart in the readme. Complete that first. It doesn't seem like much at first. The counter was at 0
, you ran a transaction, now it's at 1
. What's the big deal?
Let's try again to make it clearer what's happening. Open cadence/transactions/ScheduleIncrementIn.cdc
and look at the arguments for the transaction:
_10transaction(_10 delaySeconds: UFix64,_10 priority: UInt8,_10 executionEffort: UInt64,_10 transactionData: AnyStruct?_10)
The first parameter is the delay in seconds for the scheduled transaction. Let's try running it again. You'll need to be quick on the keyboard, so feel free to use a higher number of delaySeconds
if you need to. You're going to:
- Call the script to view the counter
- Call the transaction to schedule the counter to increment after 10 seconds
- Call the script to view the counter again and verify that it hasn't changed yet
- Wait 10 seconds, call it again, and confirm the counter incremented
For your convenience, the updated transaction call is:
_10flow transactions send cadence/transactions/ScheduleIncrementIn.cdc \_10 --network emulator --signer emulator-account \_10 --args-json '[_10 {"type":"UFix64","value":"20.0"},_10 {"type":"UInt8","value":"1"},_10 {"type":"UInt64","value":"1000"},_10 {"type":"Optional","value":null}_10 ]'
And the call to run the script to get the count is:
_10flow scripts execute cadence/scripts/GetCounter.cdc --network emulator
The result in your terminal should be similar to:
_37briandoyle@Mac scheduled-transactions-scaffold % flow scripts execute cadence/scripts/GetCounter.cdc --network emulator_37_37Result: 2_37_37briandoyle@Mac scheduled-transactions-scaffold % flow transactions send cadence/transactions/ScheduleIncrementIn.cdc \_37 --network emulator --signer emulator-account \_37 --args-json '[_37 {"type":"UFix64","value":"10.0"},_37 {"type":"UInt8","value":"1"},_37 {"type":"UInt64","value":"1000"},_37 {"type":"Optional","value":null}_37 ]'_37Transaction ID: 61cc304cee26ad1311cc1b0bbcde23bf2b3a399485c2b6b8ab621e429abce976_37Waiting for transaction to be sealed...⠹_37_37Block ID 6b9f5138901cd0d299adea28e96d44a6d8b131ef58a9a14a072a0318da0ad16b_37Block Height 671_37Status ✅ SEALED_37ID 61cc304cee26ad1311cc1b0bbcde23bf2b3a399485c2b6b8ab621e429abce976_37Payer f8d6e0586b0a20c7_37Authorizers [f8d6e0586b0a20c7]_37_37# Output omitted for brevity_37_37briandoyle@Mac scheduled-transactions-scaffold % flow scripts execute cadence/scripts/GetCounter.cdc --network emulator_37_37Result: 2_37_37_37briandoyle@Mac scheduled-transactions-scaffold % flow scripts execute cadence/scripts/GetCounter.cdc --network emulator_37_37Result: 2_37_37_37briandoyle@Mac scheduled-transactions-scaffold % flow scripts execute cadence/scripts/GetCounter.cdc --network emulator_37_37Result: 3
Review of the Existing Contract and Transactions
If you're not familiar with it, review cadence/contracts/Counter.cdc
. This is the standard contract created by default when you run flow init
. It's very simple, with a counter and public functions to increment or decrement it.
Transaction Handler
Next, open cadence/contracts/CounterTransactionHandler.cdc
_34import "FlowTransactionScheduler"_34import "Counter"_34_34access(all) contract CounterTransactionHandler {_34_34 /// Handler resource that implements the Scheduled Transaction interface_34 access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {_34 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {_34 Counter.increment()_34 let newCount = Counter.getCount()_34 log("Transaction executed (id: ".concat(id.toString()).concat(") newCount: ").concat(newCount.toString()))_34 }_34_34 access(all) view fun getViews(): [Type] {_34 return [Type<StoragePath>(), Type<PublicPath>()]_34 }_34_34 access(all) fun resolveView(_ view: Type): AnyStruct? {_34 switch view {_34 case Type<StoragePath>():_34 return /storage/CounterTransactionHandler_34 case Type<PublicPath>():_34 return /public/CounterTransactionHandler_34 default:_34 return nil_34 }_34 }_34 }_34_34 /// Factory for the handler resource_34 access(all) fun createHandler(): @Handler {_34 return <- create Handler()_34 }_34}
This contract is simple. It contains a resource that has a function with the FlowTransactionScheduler.Execute
entitlement. This function contains the code that will be called by the scheduled transaction. It:
- Calls the
increment
function in theCounter
contract - Fetches the current value in the counter
- Logs that value to the console for the emulator
It also contains functions to get metadata about the handler and a function, createHandler
, which creates and returns an instance of the Handler
resource. There are other metadata views that could be good to include in your Handler, but we are sticking to the basic ones for now.
Initializing the Transaction Handler
Next, take a look at cadence/transactions/InitCounterTransactionHandler.cdc
:
_23import "CounterTransactionHandler"_23import "FlowTransactionScheduler"_23_23transaction() {_23 prepare(signer: auth(Storage, Capabilities) &Account) {_23 // Save a handler resource to storage if not already present_23 if signer.storage.borrow<&AnyResource>(from: /storage/CounterTransactionHandler) == nil {_23 let handler <- CounterTransactionHandler.createHandler()_23 signer.storage.save(<-handler, to: /storage/CounterTransactionHandler)_23 }_23_23 // Validation/example that we can create an issue a handler capability with correct entitlement for FlowTransactionScheduler_23 let _ = signer.capabilities.storage_23 .issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(/storage/CounterTransactionHandler)_23_23 // Issue a non-entitled public capability for the handler that is publicly accessible_23 let publicCap = signer.capabilities.storage_23 .issue<&{FlowTransactionScheduler.TransactionHandler}>(/storage/CounterTransactionHandler)_23_23 // publish the capability_23 signer.capabilities.publish(publicCap, at: /public/CounterTransactionHandler)_23 }_23}
This transaction saves an instance of the Handler
resource to the user's storage. It also tests out/demonstrates how to issue the handler [capability] with the FlowTransactionScheduler.Execute
entitlement and how to publish an un-entitled capability to the handler so it can be publicly accessible. The use of the name _
is convention to name a variable we don't intend to use for anything.
Scheduling the Transaction
Finally, open cadence/transactions/ScheduleIncrementIn.cdc
again. This is the most complicated transaction, so we'll break it down. The final call other than the log
is what actually schedules the transaction:
_10manager.schedule(_10 handlerCap: handlerCap,_10 data: transactionData,_10 timestamp: future,_10 priority: pr,_10 executionEffort: executionEffort,_10 fees: <-fees_10)
It calls the schedule
function from the FlowTransactionSchedulerUtils.Manager
contract. This function has parameters for:
handlerCap
: The handler [capability] for the code that should be executed.
This was created above as a part of the previous transaction with:
_10let handlerCap = signer.capabilities.storage_10 .issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(/storage/CounterTransactionHandler)
That line creates a capability with the FlowTransactionScheduler.Execute
entitlement. That entitlement permits calling the function (executeTransaction()
) from the Handler
resource in CounterTransactionHandler.cdc
that you created and stored an instance of in the InitCounterTransactionHandler
transaction.
Then, in the schedule transaction, we retrieve the handler capability that we created before. We created two separate handlers, a public and a private one, so we have to make sure we're getting the private one:
_13// Get the entitled capability that will be used to create the transaction_13// Need to check both controllers because the order of controllers is not guaranteed_13var handlerCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>? = nil_13_13if let cap = account.capabilities.storage_13 .getControllers(forPath: /storage/CounterTransactionHandler)[0]_13 .capability as? Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}> {_13 handlerCap = cap_13} else {_13 handlerCap = account.capabilities.storage_13 .getControllers(forPath: /storage/CounterTransactionHandler)[1]_13 .capability as! Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>_13}
data
: The arguments required by the transaction function.
In this example, transactionData
is passed in as a prop on the transaction and is null
.
timestamp
: The timestamp for the time in thefuture
that this transaction should be run.
The transaction call has an argument for delaySeconds
, which is then converted to a future
timestamp:
_10let future = getCurrentBlock().timestamp + delaySeconds
priority
: The priority this transaction will be given in the event of network congestion. A higher priority means a higher fee for higher precedence.
The priority
argument is supplied in the transaction as a UInt8
for convenience, then converted into the appropriate enum type:
_10let pr = priority == 0_10 ? FlowTransactionScheduler.Priority.High_10 : priority == 1_10 ? FlowTransactionScheduler.Priority.Medium_10 : FlowTransactionScheduler.Priority.Low
The executionEffort
is also supplied as an argument in the transaction. This represents the gas limit for your transaction and is used to prepare the estimate for the gas fees that must be paid for the transaction, and directly in the call to schedule()
the transaction.
fees
: A vault containing the appropriate amount of gas fees needed to pay for the execution of the scheduled transaction.
To create the vault, the estimate()
function is first used to calculate the amount needed:
_10let est = FlowTransactionScheduler.estimate(_10 data: transactionData,_10 timestamp: future,_10 priority: pr,_10 executionEffort: executionEffort_10)
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:
_10assert(_10 est.timestamp != nil || pr == FlowTransactionScheduler.Priority.Low,_10 message: est.error ?? "estimation failed"_10)
Using 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,
you can use the Manager resource that manages all your scheduled transactions from a single place and handles many of the common patterns to reduce boilerplate code.
It also provides many convenient functions to get detailed information about all the transactions you have scheduled by timestamp, handler, etc.
When setting up a manager, you also publish a capability for it so it is easy for scripts
to query your account and also see what transactions you have scheduled!
Setting Up the Manager
First, you need to create and store a Manager resource in your account:
_16import "FlowTransactionSchedulerUtils"_16import "FlowToken"_16import "FungibleToken"_16_16transaction() {_16 prepare(signer: auth(Storage, Capabilities) &Account) {_16 // Create and save the Manager resource_16 let manager <- FlowTransactionSchedulerUtils.createManager()_16 signer.storage.save(<-manager, to: FlowTransactionSchedulerUtils.managerStoragePath)_16 _16 // Create a capability for the Manager_16 let managerCap = signer.capabilities.storage.issue<&FlowTransactionSchedulerUtils.Manager>(FlowTransactionSchedulerUtils.managerStoragePath)_16_16 signer.capabilities.publish(managerCap, at: FlowTransactionSchedulerUtils.managerPublicPath)_16 }_16}
Scheduling Transactions with the Manager
The Manager provides a schedule
method that simplifies the scheduling process:
_10manager.schedule(_10 handlerCap: handlerCap,_10 data: transactionData,_10 timestamp: future,_10 priority: priority,_10 executionEffort: executionEffort,_10 fees: <-fees_10)
The Manager also provides utility methods for:
- Scheduling another transaction with a previously used handler
- Getting scheduled transaction information in many different ways
- Canceling scheduled transactions
- Managing transaction handlers
- Querying transaction status
Writing a New Scheduled Transaction
With this knowledge, we can create our own scheduled transaction. For this demo, we'll simply display a hello from an old friend in the emulator's console logs.
Creating the Contracts
Start by using the Flow CLI to create a new contract called RickRoll.cdc
and one called RickRollTransactionHandler.cdc
:
_10flow generate contract RickRoll_10flow generate contract RickRollTransactionHandler
Open the RickRoll
contract, and add functions to log a fun message to the emulator console, and a variable to track which message to call:
_29access(all)_29contract RickRoll {_29_29 access(all) var messageNumber: UInt8_29_29 init() {_29 self.messageNumber = 0_29 }_29_29 // Reminder: Anyone can call these functions!_29 access(all) fun message1() {_29 log("Never gonna give you up")_29 self.messageNumber = 1_29 }_29_29 access(all) fun message2() {_29 log("Never gonna let you down")_29 self.messageNumber = 2_29 }_29_29 access(all) fun message3() {_29 log("Never gonna run around and desert you")_29 self.messageNumber = 3_29 }_29_29 access(all) fun resetMessageNumber() {_29 self.messageNumber = 0_29 }_29}
Next, open RickRollTransactionHandler.cdc
. Start by importing the RickRoll
contract, FlowToken
, FungibleToken
, and FlowTransactionScheduler
, and stubbing out the Handler
and factory:
_17import "FlowTransactionScheduler"_17import "RickRoll"_17import "FlowToken"_17import "FungibleToken"_17_17access(all)_17contract RickRollTransactionHandler {_17 /// Handler resource that implements the Scheduled Transaction interface_17 access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {_17 // TODO_17 }_17_17 /// Factory for the handler resource_17 access(all) fun createHandler(): @Handler {_17 return <- create Handler()_17 }_17}
Next, add a switch to call the appropriate function based on what the current messageNumber
is and add the metadata getters:
_31access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {_31 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {_31 switch (RickRoll.messageNumber) {_31 case 0:_31 RickRoll.message1()_31 case 1:_31 RickRoll.message2()_31 case 2:_31 RickRoll.message3()_31 case 3:_31 return_31 default:_31 panic("Invalid message number")_31 }_31 }_31_31 access(all) view fun getViews(): [Type] {_31 return [Type<StoragePath>(), Type<PublicPath>()]_31 }_31_31 access(all) fun resolveView(_ view: Type): AnyStruct? {_31 switch view {_31 case Type<StoragePath>():_31 return /storage/RickRollTransactionHandler_31 case Type<PublicPath>():_31 return /public/RickRollTransactionHandler_31 default:_31 return nil_31 }_31 }_31}
We could move forward with this, but it would be more fun to have each transaction schedule the follow transaction to share the next message. You can do this by moving most of the code found in the transaction to the handler. Start with configuring the delay
, future
, priority
, and executionEffort
. We'll hardcode these for simplicity:
_10var delay: UFix64 = 5.0_10let future = getCurrentBlock().timestamp + delay_10let priority = FlowTransactionScheduler.Priority.Medium_10let executionEffort: UInt64 = 1000
Next, create the estimate
and assert
to validate minimums are met, and that the Handler
exists:
_24let estimate = FlowTransactionScheduler.estimate(_24 data: data,_24 timestamp: future,_24 priority: priority,_24 executionEffort: executionEffort_24)_24_24assert(_24 estimate.timestamp != nil || priority == FlowTransactionScheduler.Priority.Low,_24 message: estimate.error ?? "estimation failed"_24)_24_24 // Ensure a handler resource exists in the contract account storage_24if RickRollTransactionHandler.account.storage.borrow<&AnyResource>(from: /storage/RickRollTransactionHandler) == nil {_24 let handler <- RickRollTransactionHandler.createHandler()_24 RickRollTransactionHandler.account.storage.save(<-handler, to: /storage/RickRollTransactionHandler)_24 _24 // Issue a non-entitled public capability for the handler that is publicly accessible_24 let publicCap = RickRollTransactionHandler.account.capabilities.storage_24 .issue<&{FlowTransactionScheduler.TransactionHandler}>(/storage/RickRollTransactionHandler)_24_24 // publish the capability_24 RickRollTransactionHandler.capabilities.publish(publicCap, at: /public/RickRollTransactionHandler)_24}
Then withdraw the necessary funds:
_10let vaultRef = CounterLoopTransactionHandler.account.storage_10 .borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)_10 ?? panic("missing FlowToken vault on contract account")_10let fees <- vaultRef.withdraw(amount: estimate.flowFee ?? 0.0) as! @FlowToken.Vault
Finally, schedule the transaction:
_16_16// borrow a reference to the scheduled transaction manager_16let manager = RickRollTransactionHandler.account.storage.borrow<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>(from: FlowTransactionSchedulerUtils.managerStoragePath)_16 ?? panic("Could not borrow a Manager reference from \(FlowTransactionSchedulerUtils.managerStoragePath)")_16_16let handlerTypeIdentifier = manager.getHandlerTypes().keys[0]!_16_16manager.scheduleByHandler(_16 handlerTypeIdentifier: handlerTypeIdentifier,_16 handlerUUID: nil,_16 data: data,_16 timestamp: future,_16 priority: priority,_16 executionEffort: executionEffort,_16 fees: <-fees_16)
As you can see, this time, we didn't have to get the handler capability. This is because the manager stores a history of handlers that you have used in the past so that you can easily just specify the type of the handler that you want to schedule for and it will schedule it for you.
Setting Up the Transactions
Next, you need to add transactions to initialize the new transaction handler, and another to fire off the sequence.
Start by adding InitRickRollHandler.cdc
:
_10flow generate transaction InitRickRollHandler
The transaction itself is nearly identical to the one we reviewed:
_24import "RickRollTransactionHandler"_24import "FlowTransactionScheduler"_24_24transaction() {_24 prepare(signer: auth(Storage, Capabilities) &Account) {_24 // Save a handler resource to storage if not already present_24 if signer.storage.borrow<&AnyResource>(from: /storage/RickRollTransactionHandler) == nil {_24 let handler <- RickRollTransactionHandler.createHandler()_24 signer.storage.save(<-handler, to: /storage/RickRollTransactionHandler)_24_24 // Validation/example that we can create an issue a handler capability with correct entitlement for FlowTransactionScheduler_24 signer.capabilities.storage_24 .issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(/storage/RickRollTransactionHandler)_24_24 // Issue a non-entitled public capability for the handler that is publicly accessible_24 let publicCap = signer.capabilities.storage_24 .issue<&{FlowTransactionScheduler.TransactionHandler}>(/storage/RickRollTransactionHandler)_24_24 // publish the capability_24 signer.capabilities.publish(publicCap, at: /public/RickRollTransactionHandler)_24_24 }_24 }_24}
Next, add ScheduleRickRoll
:
_10flow generate transaction ScheduleRickRoll
This transaction is essentially identical as well, it just uses the handlerCap
stored in RickRollTransaction
:
_78import "FlowTransactionScheduler"_78import "FlowToken"_78import "FungibleToken"_78_78/// Schedule a Rick Roll with a delay of delaySeconds_78transaction(_78 delaySeconds: UFix64,_78 priority: UInt8,_78 executionEffort: UInt64,_78 transactionData: AnyStruct?_78) {_78 prepare(signer: auth(Storage, Capabilities) &Account) {_78 let future = getCurrentBlock().timestamp + delaySeconds_78_78 let pr = priority == 0_78 ? FlowTransactionScheduler.Priority.High_78 : priority == 1_78 ? FlowTransactionScheduler.Priority.Medium_78 : FlowTransactionScheduler.Priority.Low_78_78 let est = FlowTransactionScheduler.estimate(_78 data: transactionData,_78 timestamp: future,_78 priority: pr,_78 executionEffort: executionEffort_78 )_78_78 assert(_78 est.timestamp != nil || pr == FlowTransactionScheduler.Priority.Low,_78 message: est.error ?? "estimation failed"_78 )_78_78 let vaultRef = signer.storage_78 .borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)_78 ?? panic("missing FlowToken vault")_78 let fees <- vaultRef.withdraw(amount: est.flowFee ?? 0.0) as! @FlowToken.Vault_78_78 // if a transaction scheduler manager has not been created for this account yet, create one_78 if !signer.storage.check<@{FlowTransactionSchedulerUtils.Manager}>(from: FlowTransactionSchedulerUtils.managerStoragePath) {_78 let manager <- FlowTransactionSchedulerUtils.createManager()_78 signer.storage.save(<-manager, to: FlowTransactionSchedulerUtils.managerStoragePath)_78_78 // create a public capability to the scheduled transaction manager_78 let managerRef = signer.capabilities.storage.issue<&{FlowTransactionSchedulerUtils.Manager}>(FlowTransactionSchedulerUtils.managerStoragePath)_78 signer.capabilities.publish(managerRef, at: FlowTransactionSchedulerUtils.managerPublicPath)_78 }_78_78 // Get a capability to the handler stored in this contract account_78 // Get the entitled capability that will be used to create the transaction_78 // Need to check both controllers because the order of controllers is not guaranteed_78 var handlerCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>? = nil_78_78 if let cap = signer.capabilities.storage_78 .getControllers(forPath: /storage/RickRollTransactionHandler)[0]_78 .capability as? Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}> {_78 handlerCap = cap_78 } else {_78 handlerCap = signer.capabilities.storage_78 .getControllers(forPath: /storage/RickRollTransactionHandler)[1]_78 .capability as! Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>_78 }_78_78 // borrow a reference to the scheduled transaction manager_78 let manager = signer.storage.borrow<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>(from: FlowTransactionSchedulerUtils.managerStoragePath)_78 ?? panic("Could not borrow a Manager reference from \(FlowTransactionSchedulerUtils.managerStoragePath)")_78_78 manager.schedule(_78 handlerCap: handlerCap,_78 data: transactionData,_78 timestamp: future,_78 priority: pr,_78 executionEffort: executionEffort,_78 fees: <-fees_78 )_78_78 log("Scheduled transaction at \(future)")_78 }_78}
Deployment and Testing
It's now time to deploy and test the new scheduled transaction!: First, add the new contracts to the emulator account in flow.json
(other contracts may be present):
_10"deployments": {_10 "emulator": {_10 "emulator-account": [_10 "RickRoll",_10 "RickRollTransactionHandler"_10 ]_10 }_10}
Then, deploy the contracts to the emulator:
_10flow project deploy --network emulator
And execute the transaction to initialize the new scheduled transaction handler:
_10flow transactions send cadence/transactions/InitRickRollHandler.cdc \_10 --network emulator --signer emulator-account
Finally, get ready to quickly switch to the emulator console and call the transaction to schedule the transaction!:
_10flow transactions send cadence/transactions/ScheduleRickRoll.cdc \_10 --network emulator --signer emulator-account \_10 --args-json '[_10 {"type":"UFix64","value":"2.0"},_10 {"type":"UInt8","value":"1"},_10 {"type":"UInt64","value":"1000"},_10 {"type":"Optional","value":null}_10 ]'
In the logs, you'll see similar to:
_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "Scheduled transaction at 1755099632.00000000"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.execute_transaction] executing transaction 4"_2611:40AM INF LOG: "Never gonna give you up"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.execute_transaction] executing transaction 5"_2611:40AM INF LOG: "Never gonna let you down"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.execute_transaction] executing transaction 6"_2611:40AM INF LOG: "Never gonna run around and desert you"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"_2611:40AM INF LOG: "[system.process_transactions] processing transactions"
The last case return
s the function, so it doesn't set a new scheduled transaction.
Conclusion
In this tutorial, you learned about scheduled transactions, a powerful feature that enables smart contracts to execute code at future times without external transactions. You explored how scheduled transactions solve the fundamental limitation of blockchain computers being unable to run unbounded loops or execute time-delayed operations.
Now that you have completed this tutorial, you should be able to:
- Understand the concept of scheduled transactions and how they solve blockchain limitations
- Explain the key components of the FlowTransactionScheduler system
- Understand the benefits of the Transaction Scheduler Manager
- Implement a basic scheduled transaction using the provided scaffold
- Analyze the structure and flow of scheduled transaction transactions
- Create custom scheduled transaction contracts and handlers
- Evaluate the benefits and use cases of scheduled transactions in DeFi applications
Scheduled transactions open up new possibilities for DeFi applications, enabling recurring jobs, deferred actions, and autonomous workflows that were previously impossible on blockchain. This feature represents a significant step forward in making blockchain more practical for real-world applications that require time-based execution.