Transactions
Transactions let you send Cadence code to the Flow blockchain that permanently alters its state.
We assume you have read the Scripts Documentation: ./scripts.md before this, as transactions are sort of scripts with more required things.
While query is used to send scripts to the chain, mutate is used to build and send transactions. Just like scripts, fcl.mutate is a JavaScript Tagged Template Literal that we can pass Cadence code into.
Unlike scripts, they require a little more information, things like a proposer, authorizations and a payer, which may be a little confusing and overwhelming.
Send your first transaction
There is a lot to unpack in the following code snippet. It sends a transaction to the Flow blockchain. For the transaction, the current user authorizes it as both the proposer and the payer.
Something that is unique to Flow is the one who pays for the transaction doesn't always need to be the one who performs the transaction. Proposers and Payers are special kinds of authorizations that are always required for a transaction.
- The
proposeracts similar to thenoncein Ethereum transactions, and helps prevent repeat attacks. - The
payeris who will be paying for the transaction. If these are not set, Flow Client Library (FCL) defaults to the current user for all roles.
fcl.mutate will return a transactionId. We can pass the response directly to fcl.tx and then use the onceExecuted method which resolves a promise when a transaction result is available.
_17import * as fcl from '@onflow/fcl';_17_17const transactionId = await fcl.mutate({_17 cadence: `_17 transaction {_17 execute {_17 log("Hello from execute")_17 }_17 }_17 `,_17 proposer: fcl.currentUser,_17 payer: fcl.currentUser,_17 limit: 50,_17});_17_17const transaction = await fcl.tx(transactionId).onceExecuted();_17console.log(transaction); // The transactions status and events after being executed
Authorize a transaction
The below code snippet is the same as the above one, except for one extremely important difference. Our Cadence code this time has a prepare statement, and we use the fcl.currentUser when constructing our transaction.
The prepare statement's arguments directly map to the order of the authorizations in the authorizations array. Four authorizations means four &Accounts as arguments passed to prepare. In this case though there is only one, and it is the currentUser.
These authorizations are important as you can only access or modify an account's storage if you have that account's authorization.
_21import * as fcl from '@onflow/fcl';_21_21const transactionId = await fcl.mutate({_21 cadence: `_21 transaction {_21 prepare(acct: &Account) {_21 log("Hello from prepare")_21 }_21 execute {_21 log("Hello from execute")_21 }_21 }_21 `,_21 proposer: fcl.currentUser,_21 payer: fcl.currentUser,_21 authorizations: [fcl.currentUser],_21 limit: 50,_21});_21_21const transaction = await fcl.tx(transactionId).onceExecuted();_21console.log(transaction); // The transactions status and events after being executed
To learn more about mutate, check out the API documentation.
Query transaction results
When you query transaction results (for example, via HTTP/REST endpoints like GET /v1/transaction_results/{id}), you can provide either:
- A transaction ID (256-bit hash as hex string).
- A scheduled transaction ID (UInt64 as decimal string).
The returned result always includes transaction_id as the underlying native transaction ID. For scheduled transactions, this will be the system transaction ID that executed the scheduled callback.
Learn more about Scheduled Transactions.
Transaction finality
As of FCL v1.15.0, it is now recommended to use use onceExecuted in most cases, which leads to a 2.5x reduction in latency when you wait for a transaction result. For example, the following code snippet should be updated from:
_10import * as fcl from '@onflow/fcl';_10const result = await fcl.tx(txId).onceSealed();
to:
_10import * as fcl from '@onflow/fcl';_10const result = await fcl.tx(txId).onceExecuted();
Developers who manually subscribe to transaction statuses should update their listeners to treat "executed" as the final status (see the release notes). For example, the following code snippet should be updated from:
_10import * as fcl from '@onflow/fcl';_10import { TransactionExecutionStatus } from '@onflow/typedefs';_10_10fcl.tx(txId).subscribe((txStatus) => {_10 if (txStatus.status === TransactionExecutionStatus.SEALED) {_10 console.log('Transaction executed!');_10 }_10});
_11import * as fcl from '@onflow/fcl';_11import { TransactionExecutionStatus } from '@onflow/typedefs';_11_11fcl.tx(txId).subscribe((txStatus) => {_11 if (_11 // SEALED status is no longer necessary_11 txStatus.status === TransactionExecutionStatus.EXECUTED_11 ) {_11 console.log('Transaction executed!');_11 }_11});
The "executed" status corresponds to soft finality, which indicates that the transaction has been included in a block and a transaction status is available, backed by a cryptographic proof. Only in rare cases should a developer need to wait for "sealed" status in their applications and you can learn more about the different transaction statuses on Flow here.
See the following video for demonstration of how to update your code to wait for "executed" status: