Hello! Welcome back to our journey into client-side Solana development.
In our last lesson, we successfully integrated the Solana Wallet Adapter into a React dApp, enabling a user to sign and send a transaction. We used Anchor's convenient .rpc() method, which handles the entire process. However, sending a transaction is only half the story. After we ask the user to sign and we broadcast the transaction to the network, a critical question remains: What happened to it? Did it succeed? Is it permanently part of the blockchain's history?
This lesson dives into the heart of transaction confirmation on Solana. We'll explore the different levels of certainty you can request from the network and how to handle them in your dApp. Understanding this is crucial for building reliable applications that give users clear and accurate feedback.
By the end of this lesson, you will be able to confirm a transaction and handle the different commitment levels: processed, confirmed, and finalized.
1. The Lifecycle of a Transaction: From Processed to Finalized
When you send a transaction to the Solana network, it doesn't become instantly and irreversibly permanent. It goes through several stages of validation by the network's validators. Solana exposes these stages through "commitment levels," which allow you, the developer, to choose a balance between speed and certainty.
You can request information based on a specific commitment level. For example, you can ask, "Has my transaction been processed by a validator?" or "Has my transaction been finalized by the entire network?" The answers to these questions come at different speeds and carry different levels of assurance.
To understand these levels, let's start with a foundational reading.
What are Solana Commitment Levels?
This article from Helius provides an excellent and comprehensive explanation of Solana's commitment levels. We will use it as our primary guide for this topic. First, let's get a high-level overview of the three main levels and the trade-offs involved.
Read the introduction, up to the section titled 'Processed Commitment Level'. Focus on the definitions of Finalized, Confirmed, and Processed, and the core concept of balancing 'latency vs. certainty'.
As the article explains, the three key commitment levels represent a journey towards irreversibility:
processed: The transaction has been received and processed by the current leader validator. This is the fastest acknowledgment, but it offers the least certainty. The block it's in might be dropped if a different "fork" of the chain wins out.confirmed: A supermajority (at least 66%) of validators have voted on the block containing the transaction. This is often called "optimistic confirmation." The risk of this transaction being reversed is extremely low, and for most dApps, this is the ideal balance.finalized: The block has not only been confirmed by a supermajority but has also had at least 31 more confirmed blocks built on top of it. This makes the transaction effectively irreversible. This offers the maximum security but comes with the highest latency.
Let's dig a bit deeper into the specifics of each level.
What are Solana Commitment Levels?
Now, let's explore the technical details and characteristics of each commitment level using the same Helius article.
Read the sections 'Processed Commitment Level', 'Confirmed Commitment Level', and 'Finalized Commitment Level'. Also, review the comparison table in the 'Differences Between Commitment Levels' section. This will solidify your understanding of what each level guarantees.
To summarize the key differences:
| Property | processed | confirmed | finalized |
|---|---|---|---|
| Network Acknowledgment | A single leader has processed it. | A supermajority of the network has voted for it. | The network has locked it in as irreversible. |
| Latency | Fastest (~0.4s) | Fast (~0.6s) | Slowest (~13s) |
| Certainty | Lowest (can be dropped) | High (very unlikely to be dropped) | Highest (effectively permanent) |
| Typical Use Case | Optimistic UI updates (e.g., "pending...") | Most standard dApp operations | High-value, critical actions (e.g., exchange deposits) |
2. Choosing the Right Commitment Level for Your dApp
Now that you understand the theory, the practical question is: when should you use each level? Your experience as a front-end developer will be valuable here, as choosing a commitment level is often about managing user experience.
What are Solana Commitment Levels?
The Helius article provides excellent guidance on when to use each commitment level in your application.
Read the sections 'Developer Use Cases for Each Commitment Level' and 'Impacts on Transaction Reliability, Performance, and Security'. Pay close attention to the trade-offs described.
Here's a practical guide based on that reading:
-
Use
processedfor quick, optimistic UI feedback. For example, when a user submits a transaction, you could immediately disable the "submit" button and show a "Processing..." message uponprocessedconfirmation. You wouldn't credit their account yet, but you improve the perceived responsiveness of your app. -
Use
confirmedfor the vast majority of operations. This is the recommended default. When a transaction isconfirmed, you can confidently update the UI to show that the action was successful (e.g., "Your NFT has been minted!" or "Payment successful"). The odds of a reversal are negligible for most use cases. -
Use
finalizedonly when absolute, irreversible certainty is required. Think of a crypto exchange crediting a large deposit, a cross-chain bridge completing a transfer, or an audit process that needs to read the permanent state of the ledger. Waiting forfinalizedadds significant latency (~13 seconds or more), which can be detrimental to the user experience if used unnecessarily.
Test your understanding!
You are building a DeFi swapping interface. A user wants to swap 1 SOL for some USDC. You need to decide which commitment level to wait for before telling the user "Swap successful" and updating their displayed token balances. Which level would you choose and why? What about for an on-chain game where a player makes a quick move—which level might you use to update the game state visually?
Show answer
For the DeFi swap, confirmed is the best choice. It provides a very high degree of certainty with low latency. The user gets fast feedback that their swap is complete, and the risk of the transaction being dropped is extremely low. Waiting for finalized would make the UI feel sluggish for no significant practical benefit.
For the on-chain game, you might use processed for an initial visual update. As soon as the player's move is processed by a leader, you could show the move on the screen optimistically. This makes the game feel instantaneous. You would then wait for confirmed in the background to lock in the game state officially. This hybrid approach gives the best of both worlds: a responsive UI and eventual consistency.
3. Implementing Transaction Confirmation in Code
Let's translate this theory into TypeScript code. When you send a transaction, you can specify the commitment level you want to wait for. The modern @solana/web3.js library has a specific pattern for this.
The sendAndConfirmTransaction Factory
The latest version of @solana/web3.js encourages a factory pattern to create a function that can both send a transaction and listen for its confirmation. This is more efficient than the old method of sending and then repeatedly polling the network.
This factory requires two connections:
- An RPC connection (HTTP) to send the transaction.
- A WebSocket connection to subscribe to confirmation status updates.
How to Start Building with the Solana Web3.js 2.0 SDK
This article from Helius on the Web3.js 2.0 SDK demonstrates the modern pattern for sending and confirming transactions. Let's look at how to set up the factory and then use it.
Read the sections 'Configure RPC Connections' and 'Send and Confirm Transaction'. Notice how sendAndConfirmTransactionFactory is created with both RPC and WebSocket subscription clients. Then, see how the commitment level is passed as an option to the resulting function.
Here's a condensed example of the code pattern:
import {
createSolanaRpc,
createSolanaRpcSubscriptions,
sendAndConfirmTransactionFactory,
} from "@solana/web3.js";
// 1. Create RPC and WebSocket clients
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const rpcSubscriptions = createSolanaRpcSubscriptions("wss://api.devnet.solana.com");
// 2. Create the sender function using the factory
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
// Assume `signedTransaction` is a fully signed Transaction object
async function sendMyTx(signedTransaction) {
console.log("Sending and waiting for 'confirmed' commitment...");
// 3. Send the transaction and wait for confirmation
await sendAndConfirmTransaction(signedTransaction, {
commitment: "confirmed", // <-- Here's where you set the level!
skipPreflight: true, // Optional: skips simulation for speed
maxRetries: 0n,
});
console.log("Transaction confirmed!");
}
The Solandy video you've seen before also walks through this process, contrasting the old and new methods, which can be helpful for context. He explicitly sets the commitment to confirmed when sending the final transaction.
Solana kit (forermly web3.js v2.0.0) officially released [Solana Tutorial] - Nov 12th '24
To see this in a live-coding context, this clip from Solandy shows him sending and confirming a transaction with a specific commitment level using the new libraries.
Watch from 00:40:32 to 00:41:29. Notice his use of send it with commitment confirmed in the code. This is the same principle we just read about.
Commitment Levels with the Anchor Client
In the previous lesson, you used Anchor's .rpc() method. How do you specify the commitment level there?
You do it when you create the AnchorProvider. The provider's constructor accepts an optional opts object of type ConfirmOptions. This object is then used for all subsequent .rpc() calls made with that provider instance.
import { AnchorProvider, Program } from "@project-serum/anchor";
import type { ConfirmOptions } from "@solana/web3.js";
// Get connection and wallet from your React hooks
// const connection = useConnection();
// const wallet = useWallet();
// 1. Define your confirmation options
const opts: ConfirmOptions = {
preflightCommitment: "processed",
commitment: "confirmed", // Wait for 'confirmed' status
};
// 2. Create the provider with these options
const provider = new AnchorProvider(connection, wallet, opts);
// 3. Create your program instance
const program = new Program(idl, programId, provider);
// 4. Any .rpc() call now uses the 'confirmed' commitment level by default
await program.methods.myInstruction().rpc();
You'll notice two properties:
preflightCommitment: This is the commitment level used for the initial transaction simulation (preflight) that web3.js runs to catch errors early. Usingprocessedhere is common for a quick check.commitment: This is the commitment level that.rpc()will wait for after sending the transaction to the network before it returns. This is the crucial one for final confirmation.
By setting commitment: "confirmed" in your provider, you ensure that your dApp's logic only proceeds after the transaction has been optimistically confirmed by the network, providing a great balance of safety and speed.
Conclusion
You've now mastered a fundamental concept of Solana development: transaction confirmation. Knowing how to use commitment levels effectively is key to building dApps that feel both fast and reliable.
Key Takeaways:
- Solana offers three main commitment levels:
processed,confirmed, andfinalized, each offering a different trade-off between speed and certainty. processedis for fast but risky UI updates.confirmedis the standard for most dApp operations, offering high security with low latency.finalizedis for high-value, critical operations where absolute irreversibility is required, at the cost of higher latency.- In
@solana/web3.js, you specify the commitment level in the options of thesendAndConfirmTransactionfunction. - In Anchor, you set the default commitment level for all
.rpc()calls by passingConfirmOptionswhen creating yourAnchorProvider.
In our next lesson, we'll build on this by exploring another powerful feature for creating responsive dApps: subscribing to account changes using WebSockets. This will allow us to get real-time updates from the blockchain whenever data changes, without needing to poll or refresh the page.
Can't find a good explanation? Sign up and we'll make it for you
Sign up