Hello! Welcome back to our course on Solana development.
In our last lesson, we focused on confirming transactions and understanding the different commitment levels (processed, confirmed, and finalized). This allows our dApp to answer the question, "Did my transaction succeed?" after we send it.
Today, we'll explore a different, more proactive way to interact with the blockchain. Instead of asking for the status of a specific action, we want the blockchain to tell us when something changes, in real time. This is essential for building dynamic and responsive dApps, such as live activity feeds, real-time balance updates, or auction sites.
Your extensive front-end development experience will be a great asset here, as the core technology, WebSockets, is likely familiar to you from building real-time web applications. We'll see how Solana leverages this same technology to push on-chain data directly to our client.
By the end of this lesson, you will be able to subscribe to account changes using WebSockets to get real-time updates.
1. From Pull to Push: Polling vs. WebSockets
Imagine you want to display a user's SOL balance and have it update instantly if they receive funds. How would you implement this?
One approach is polling. You could use setInterval to call connection.getBalance() every few seconds. This "pull" model works, but it has significant downsides:
- Latency: There's always a delay between the on-chain change and your next poll.
- Inefficiency: Most of your requests will be wasted, returning the same data and consuming RPC credits and bandwidth.
- Rate-limiting: Aggressive polling can get your dApp's IP address temporarily blocked by the RPC provider.
A much better solution is the "push" model, enabled by WebSockets. A WebSocket establishes a persistent, two-way communication channel between your dApp (the client) and a Solana RPC node (the server). Once connected, the server can push updates to your client as soon as they happen.
Let's begin by solidifying our understanding of WebSockets in the context of Solana.
Receiving real-time updates on Solana with Websockets
This article from Andrew's Substack gives a clear introduction to what WebSockets are and how they differ from the standard HTTP requests you typically use for fetching data.
Read the sections 'Introduction' and 'What is a Websocket?'. This will provide a great conceptual foundation before we dive into the code.
As the article highlights, the key difference is moving from a request-response cycle to a persistent connection where the server can initiate communication. For Solana, this means we can connect to a node's WebSocket endpoint (e.g., wss://api.devnet.solana.com) and subscribe to events.
2. Subscribing to Account Changes with onAccountChange
The @solana/web3.js library provides a high-level, convenient method for subscribing to changes in a single account: connection.onAccountChange(). This is the simplest and most common way to get real-time updates for a specific piece of data.
Solana On-Chain Event Monitoring Guide - Panda Academy
The 'Solana On-Chain Event Monitoring Guide' from Panda Academy provides a concise code example for using onAccountChange.
In the section '3. Tutorial: 3 Monitoring Methods', focus on the code block under 'Method 1: WebSocket Account Monitoring'. We will break this code down together.
Let's analyze the structure of this method:
import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
// 1. Establish a WebSocket connection
// Note the 'wss://' protocol for WebSockets
const connection = new Connection("wss://api.devnet.solana.com");
// The public key of the account we want to monitor
const accountToWatch = new PublicKey("your_account_public_key_here");
console.log(`Watching account ${accountToWatch.toBase58()} for changes...`);
// 2. Subscribe to account changes
const subscriptionId = connection.onAccountChange(
accountToWatch,
(accountInfo, context) => {
console.log("Account has changed!");
console.log("New balance:", accountInfo.lamports / LAMPORTS_PER_SOL, "SOL");
console.log("Change occurred in slot:", context.slot);
},
"confirmed" // Optional: Specify the commitment level
);
// 3. Later, to stop listening...
// connection.removeAccountChangeListener(subscriptionId);
// console.log(`Stopped watching account ${accountToWatch.toBase58()}`);
Let's break down the key parts:
Connection: To use WebSockets, yourConnectionobject must point to awss://URL.onAccountChange(publicKey, callback, commitment):publicKey: The public key of the account you want to listen to.callback: A function that will be executed every time a change is detected on the account. It receives two arguments:accountInfo: An object containing the updated account data, such aslamports,owner,data, etc.context: An object containing metadata about the change, most importantly theslotin which it occurred.
commitment: This is the same concept from our last lesson. It specifies how certain you need to be before the callback is triggered. Using'confirmed'is a safe default, balancing speed and reliability.
subscriptionId: The method returns a number, which is the ID for this specific subscription. You must store this ID so you can unsubscribe later.removeAccountChangeListener(subscriptionId): This function is crucial for resource management. In a real-world application like a React component, you would call this in a cleanup function (e.g., insideuseEffect's return function) to prevent memory leaks and unnecessary network traffic when the component unmounts.
Test your understanding!
You are building a feature to display a user's SOL balance in real-time. Write a TypeScript snippet using onAccountChange that connects to the devnet and logs the new balance in SOL to the console whenever it changes. How would you ensure you stop listening for changes (e.g., when the user navigates away from the page)?
Show answer
import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
// Assume we have the user's public key
const userPublicKey = new PublicKey("9B5Xsz8Ecf2cWC3K3eU1i8Z26h1n1Lq3u1x6jZ8z3Y7p"); // Example public key
const connection = new Connection("wss://api.devnet.solana.com");
console.log(`Watching wallet ${userPublicKey.toBase58()} for balance changes...`);
const subscriptionId = connection.onAccountChange(
userPublicKey,
(accountInfo) => {
const newBalance = accountInfo.lamports / LAMPORTS_PER_SOL;
console.log(`New balance is: ${newBalance} SOL`);
},
"confirmed"
);
// To stop listening, you would call this function, passing the stored ID.
// For example, in a React component's cleanup effect:
//
// useEffect(() => {
// ... subscription logic here ...
// return () => {
// connection.removeAccountChangeListener(subscriptionId);
// console.log("Unsubscribed from account changes.");
// };
// }, [connection, userPublicKey]);
// For this standalone script, we can just log how it would be done.
console.log(`To unsubscribe, call connection.removeAccountChangeListener(${subscriptionId})`);
This demonstrates setting up the listener and the importance of using removeAccountChangeListener with the returned subscriptionId to clean up the subscription.
3. A Deeper Dive: The Raw WebSocket JSON-RPC API
The onAccountChange method is a convenient wrapper. For a deeper understanding and more control, it's valuable to see what's happening underneath. We can interact with the RPC node's WebSocket server directly by sending formatted JSON-RPC messages.
This low-level approach involves four steps:
- Establish a raw WebSocket connection.
- Send a subscription request message (e.g.,
accountSubscribe). - Listen for and parse incoming notification messages.
- Send an unsubscribe request message when done.
Receiving real-time updates on Solana with Websockets
Andrew's Substack article provides an excellent, detailed walkthrough of this lower-level process. We'll follow its structure to see how subscriptions are manually managed.
Read the following sections: 'Websockets with TypeScript' to see how to set up the connection and listeners; 'Interacting with Solana's RPC Methods via WebSockets' for the concept; 'Subscribing to Account Updates' for the core accountSubscribe example; and finally, 'Unsubscribing from Account Notifications' to see how to clean up.
Let's summarize the key points from the reading.
1. Creating the Connection:
Instead of using @solana/web3.js's Connection object, you can use a standard WebSocket client (available natively in browsers or via a library like ws in Node.js). You then attach listeners for events like open, message, error, and close.
2. Sending a Subscription Request:
Once the connection is open, you send a JSON string with a specific structure. To subscribe to an account, the method is accountSubscribe.
{
"jsonrpc": "2.0",
"id": 1,
"method": "accountSubscribe",
"params": [
"YOUR_ACCOUNT_PUBLIC_KEY",
{
"encoding": "jsonParsed",
"commitment": "confirmed"
}
]
}
id: A unique ID you create to track this request. The server's first response will include this ID.method:accountSubscribeis the key RPC method here.params: An array containing the public key string and a configuration object. Theencodingparameter is very powerful;jsonParsedasks the RPC node to try and parse the account's binary data into a human-readable JSON object, which is incredibly useful for token accounts and custom program data.
3. Handling Messages:
Your onmessage handler will receive two kinds of messages:
- Subscription Confirmation: The first message will contain your original
idand aresultfield with thesubscriptionId. - Notifications: Subsequent messages will have a
methodofaccountNotificationandparamscontaining thesubscriptionIdand the updated account data inresult.
4. Unsubscribing:
To unsubscribe, you send another message using the accountUnsubscribe method, passing the subscriptionId you received earlier.
While you'll likely use the simpler connection.onAccountChange for most of your work, understanding this underlying JSON-RPC protocol is invaluable for debugging and for advanced use cases where the high-level abstractions aren't flexible enough.
4. Beyond Single Accounts: Subscribing to Program Logs
What if you want to monitor all activity related to a specific program, not just a single data account? For example, you might want to build a live feed of all mints from a candy machine program.
For this, you can subscribe to a program's logs using connection.onLogs().
Solana On-Chain Event Monitoring Guide - Panda Academy
The Panda Academy article also includes a clear example for onLogs. This is another powerful tool in your real-time toolkit.
Review the code block under 'Method 2: Smart Contract Event Logs'. Notice the similarity in structure to onAccountChange.
The pattern is very similar:
const programId = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); // SPL Token Program
const logsSubscriptionId = connection.onLogs(
programId,
(logs, context) => {
if (logs.err) {
console.error("Transaction failed:", logs.err);
return;
}
console.log("Logs for transaction:", logs.signature);
console.log(logs.logs); // An array of log strings
},
"confirmed"
);
// To unsubscribe:
// connection.removeLogsListener(logsSubscriptionId);
This subscription triggers whenever a transaction that invokes the specified program is confirmed. The logs object gives you the transaction signature and an array of log messages emitted by the program during execution. This is extremely useful for debugging and for listening to program-defined events (which are often just specially formatted log messages).
Conclusion
Congratulations! You've just added a powerful tool for building real-time, responsive Solana dApps to your skillset. By moving from a "pull" model to a "push" model with WebSockets, you can create a much more fluid and engaging user experience.
Key Takeaways:
- WebSockets provide a persistent connection to a Solana node, enabling real-time "push" updates that are far more efficient than polling.
- The easiest way to watch an account is with
connection.onAccountChange(), which takes a public key, a callback, and an optional commitment level. - It's critical to manage the subscription lifecycle by storing the
subscriptionIdand calling the correspondingremove...Listener()function to prevent memory leaks. - For advanced use cases, you can use the low-level JSON-RPC API (
accountSubscribe) over a raw WebSocket connection. - To monitor all activity for a program, you can use
connection.onLogs(), which is great for debugging or building event feeds.
In our next module, we will shift our focus to building the dApp frontend in earnest. We'll start by integrating the @solana/wallet-adapter libraries into a React application, allowing users to connect their wallets—the first and most crucial step in any dApp interaction.
Can't find a good explanation? Sign up and we'll make it for you
Sign up