Transactions
Transactions are objects that are signed with keys of one or more accounts and are sent to the chain to interact with it and perform state changes.
Transaction can import any number of types from any account using the import syntax.
_10import FungibleToken from 0x01
A transaction is declared using the transaction
keyword
and its contents are contained in curly braces.
The body of the transaction can declare local variables that are valid throughout the whole of the transaction.
_10transaction {_10 // transaction contents_10 let localVar: Int_10_10 // ..._10}
Transaction parameters
Transactions can have parameters. Transaction parameters are declared like function parameters. The arguments for the transaction are passed along with the transaction.
Transaction parameters are accessible throughout the whole of the transaction.
_10// Declare a transaction which has one parameter named `amount`_10// that has the type `UFix64`_10//_10transaction(amount: UFix64) {_10_10}
Transaction phases
Transactions are executed in four phases: preparation, pre-conditions, execution, and post-conditions, in that order. The preparation and execution phases are blocks of code that execute sequentially. The pre-conditions and post-condition are just like conditions in functions.
The following empty Cadence transaction has no logic, but demonstrates the syntax for each phase, in the order these phases are executed:
_17transaction {_17 prepare(signer1: &Account, signer2: &Account) {_17 // ..._17 }_17_17 pre {_17 // ..._17 }_17_17 execute {_17 // ..._17 }_17_17 post {_17 // ..._17 }_17}
Although optional, each phase serves a specific purpose when executing a transaction and it is recommended that developers use these phases when creating their transactions.
Prepare phase
The prepare
phase is used when the transaction needs access
to the accounts which signed (authorized) the transaction.
Access to the signing accounts is only possible inside the prepare
phase.
For each signer of the transaction,
a reference to the signing account is passed as an argument to the prepare
phase.
The reference may be authorized, requesting certain access to the account.
For example, if the transaction has two signers,
the prepare must have two parameters of type &Account
.
_10prepare(signer1: &Account, signer2: &Account) {_10 // ..._10}
For instance, to request write access to an account's storage, the transaction can request an authorized reference:
_10prepare(signer: auth(Storage) &Account) {_10 // ..._10}
As a best practice, only use the prepare
phase to define and execute logic
that requires write access to the signing accounts,
and move all other logic elsewhere.
Modifications to accounts can have significant implications, so keep this phase clear of unrelated logic. This ensures that users can easily read and understand the logic of the transaction and how it affects their account.
The prepare phase serves a similar purpose as the initializer of a composite.
For example, if a transaction performs a token transfer, put the withdrawal in the prepare
phase,
as it requires access to the account storage, but perform the deposit in the execute
phase.
Pre-conditions
Transaction pre-conditions are just like pre-conditions of functions.
Pre-conditions are optional and are declared in a pre
block.
They are executed after the prepare
phase,
and are used for checking if explicit conditions hold before executing the remainder of the transaction.
The block can have zero or more conditions.
For example, a pre-condition might check the balance before transferring tokens between accounts.
_10pre {_10 sendingAccount.balance > 0_10}
If any of the pre-conditions fail, then the remainder of the transaction is not executed and it is completely reverted.
Execute phase
The execute
block executes the main logic of the transaction.
This phase is optional, but it is a best practice to add your main transaction logic in the section,
so it is explicit.
It is impossible to access the references to the transaction's signing accounts in the execute
phase.
_12transaction {_12 prepare(signer: auth(LoadValue) &Account) {}_12_12 execute {_12 // Invalid: Cannot access the `signer` account reference, as it is not in scope_12 let resource <- signer.storage.load<@Resource>(from: /storage/resource)_12 destroy resource_12_12 // Valid: Can obtain an unauthorized reference to any account_12 let otherAccount = getAccount(0x3)_12 }_12}
Post-conditions
Transaction post-conditions are just like post-conditions of functions.
Post-conditions are optional and are declared in a post
block.
They are executed after the execution phase,
and are used to verify that the transaction logic has been executed properly.
The block can have zero or more conditions.
For example, a token transfer transaction can ensure that the final balance has a certain value:
_10post {_10 signer.balance == 30.0: "Balance after transaction is incorrect!"_10}
If any of the post-conditions fail, then the transaction fails and is completely reverted.
Pre-conditions and post-conditions
Another function of the pre-conditions and post-conditions is to describe the effects of a transaction on the involved accounts. They are essential for users to verify what a transaction does before submitting it. The conditions an easy way to introspect transactions before they are executed.
For example, the software that a user uses to sign and send a transaction could analyze and interpret the transaction into a human-readable description, like "This transaction will transfer 30 tokens from A to B. The balance of A will decrease by 30 tokens and the balance of B will increase by 30 tokens."
Summary
Transactions use phases to make the transaction's code / intent more readable. They provide a way for developers to separate the transaction logic. Transactions also provide a way to check the state prior / after transaction execution, to prevent the transaction from running, or reverting changes made by the transaction if needed.
The following is a brief summary of how to use the prepare
, pre
, execute
,
and post
blocks in a transaction to implement the transaction's phases:
_33transaction {_33 prepare(signer1: &Account) {_33 // Access signing accounts of the transaction._33 //_33 // Avoid logic that does not need access to the signing accounts._33 //_33 // The signing accounts can't be accessed anywhere else in the transaction._33 }_33_33 pre {_33 // Define conditions that must be true_33 // for the transaction to execute._33 //_33 // Define the expected state of things_33 // as they should be before the transaction is executed._33 }_33_33 execute {_33 // The main transaction logic goes here, but you can access_33 // any public information or resources published by any account._33 }_33_33 post {_33 // Define conditions that must be true_33 // for the transaction to be committed._33 //_33 // Define the expected state of things_33 // as they should be after the transaction executed._33 //_33 // Also used to provide information about what changes_33 // the transaction will make to the signing accounts._33 }_33}