Receiving real-time updates on Solana with Websockets
Introduction
In this Solana tutorial, we will explore an exciting topic that I've been eager to cover for a while now: websockets and how they can be used to receive real-time updates for Solana state changes. At some point in your Solana development journey, you will likely encounter a situation where you need to get notifications when certain events occur on the blockchain. Today, we will dive into the Solana websocket API and learn how to leverage it effectively.
What is a Websocket?
Before we delve into the specifics of Solana websockets, let's take a moment to understand what websockets are and how they differ from traditional HTTP requests.
In a typical web scenario, a user requests a web page by sending an HTTP request to a server. The server responds by sending the requested web page, and the connection is closed. This is a one-time interaction where the connection is established, data is sent, and then the connection is terminated.
Websockets, on the other hand, provide a persistent connection between the client and the server. Once the connection is established, it remains open, allowing for continuous communication. The server can push notifications to the client without the client having to constantly poll for new data. The term "socket" might be familiar from the TCP/IP world, where it refers to the combination of an IP address and a port, identifying a specific server and application. In the context of websockets, we are dealing with web-based protocols built on top of HTTP.
To summarize, with a websocket, the client connects to an RPC server or any other server and maintains an open connection. The server can then push notifications to the client whenever new data is available, eliminating the need for the client to repeatedly ask for updates.
Websockets with TypeScript
When working with Solana, you may have come across the websocket URL in the configuration, such as `wss://api.devnet.solana.com`. This URL represents the websocket URI for connecting to the Solana network. It's similar to the HTTP URL but uses the secure websocket protocol (`wss`) instead of `http` or `https`. The port component is optional: the default for “ws" is port 80, while the default for “wss" is port 443.
To use websockets with TypeScript, you can leverage the built-in `WebSocket` interface. This interface provides an API for creating and managing websocket connections to a server.
Here's an example of how to create a websocket connection in TypeScript:
Once you have a websocket instance, you can interact with it using various methods and events:
- `onopen`: Triggered when the websocket connection is established.
- `onclose`: Triggered when the websocket connection is closed.
- `onmessage`: Triggered when a message is received from the server.
- `onerror`: Triggered when an error occurs.
- `send`: Used to send messages to the server.
Here's an example of how you might set up these event listeners in JavaScript:
In this example, we're creating a new WebSocket connection to Solana's RPC endpoint using the Alchemy API. We then add event listeners for the `open`, `message`, `error`, and `close` events.
- When the connection is established (the `open` event), we log a message to the console.
- When data is received from the server (the `message` event), we parse the JSON data and log it to the console.
- If an error occurs (the `error` event), we log the error to the console.
- When the connection is closed (the `close` event), we log a message to the console.
By setting up these event listeners, we can handle the different states of the WebSocket connection and respond accordingly, such as processing incoming data or logging errors. It's important to note that you need to install the appropriate TypeScript types for the `ws` module to get proper type definitions. You can do this by running:
With the websocket connection established, you can now start sending messages and receiving responses from the Solana network.
Interacting with Solana's RPC Methods via WebSockets
Now that we have a basic understanding of how WebSockets work, let's see how we can use them to interact with Solana's RPC API and receive real-time updates. Unlike traditional HTTP requests, where you would make a one-time request to an RPC method (e.g., `getBalance`), WebSockets allow you to "subscribe" to certain RPC methods and receive updates as they occur.
Some common Solana RPC methods that you might want to subscribe to include:
- `accountSubscribe`: Subscribe to changes to a specific account.
- `blockSubscribe`: Subscribe to new confirmed blocks.
- `programSubscribe`: Subscribe to account changes for a specific program.
- `signatureSubscribe`: Subscribe to a specific transaction signature.
Subscribing to Account Updates
To subscribe to changes for a specific account, you'll need to send a JSON-RPC request over the WebSocket connection with the `accountSubscribe` method and the public key of the account you want to monitor. Here's an example:
In this example, the `subscribeToAccount` function takes a WebSocket connection (`ws`) and the public key of the account you want to subscribe to (`accountPublicKey`). It then constructs the JSON-RPC request with the following properties:
- `jsonrpc`: The JSON-RPC protocol version, which is `'2.0'` in this case.
- `id`: A unique identifier for the request, which will be mirrored in the response.
- `method`: The RPC method to call, which is `'accountSubscribe'` in this case.
- `params`: An array containing the account public key and an optional configuration object:
`encoding`: The encoding format for the account data, which can be `'base58'`, `'base64'`, `'base64+zstd'`, `'jsonParsed'`, or `'json'`. The `'jsonParsed'` option is recommended, as it attempts to use program-specific state parsers to return more human-readable data.
`commitment`: The commitment level for the subscription, which can be `'processed'`, `'confirmed'`, `'finalized'`, or `'recent'`. In this example, we're using `'confirmed'`, which means we'll receive updates when the account changes and the change is confirmed.
After constructing the request, the function sends it over the WebSocket connection using the `send` method.
When the server receives this request, it will respond with a message that includes a `result` property containing the subscription ID. This subscription ID is important, as you'll need it later if you want to unsubscribe from the account changes.
In the message event listener, we check if the incoming message is an `accountNotification`, which indicates that the subscribed account has been updated. We then log the updated account data to the console. We also check if the message contains a `result` property, which means the message is a response to our subscription request. In this case, we log the subscription ID, which we can use later to unsubscribe from the account changes.
Receiving Real-Time Account Updates
Imagine a scenario where you just received a massive airdrop of 5 SOL to your Solana account. You'd want to know about this update as soon as possible, right? That's where the `accountSubscribe` method shines.
Let's say we've already subscribed to our account using the `subscribeToAccount` function from the previous example. As soon as the 5 SOL airdrop hits our account, we'll receive an `accountNotification` message over the WebSocket connection.
In this updated event listener, we're looking for the `accountNotification` message. When we receive one, we log the updated account data to the console.
The account data is provided in the `data.params.result.value` object, which includes the following properties:
- `lamports`: The number of lamports (smallest unit of SOL) in the account.
- `owner`: The public key of the account's owner.
- `data`: The account data, encoded in base64.
- `executable`: A boolean indicating whether the account is executable.
In our example, we see that the account has received 5 SOL (5,000,000,000 lamports), the owner is the System Program, the account data is empty, and the account is not executable. This real-time update is incredibly valuable, as it allows your application to react to changes in the Solana blockchain immediately, without having to constantly poll the RPC API for updates.
Tracking Account Balance Changes in Real-Time
Now that we have the basics of account subscription down, let's take it a step further and see how we can track changes to our account balance in real-time. Imagine you want to keep an eye on your Solana account balance, so you can react quickly to any changes, such as incoming transactions or outgoing transfers.
In this updated event listener, we're specifically looking for the `lamports` property in the `data.params.result.value` object. We then calculate the new balance in SOL by dividing the `lamports` value by the number of lamports per SOL (`LAMPORTS_PER_SOL`).
Now, whenever there's a change to the account balance, whether it's an incoming transaction or an outgoing transfer, we'll immediately see the updated balance logged to the console. Let's say you accidentally send 1 SOL to the wrong address. You'll see the balance update almost instantly:
This real-time tracking of account balance changes is incredibly powerful. Instead of having to constantly poll the RPC API to check the balance, you can sit back and wait for the updates to come to you via the WebSocket connection.
Additionally, you're not limited to just subscribing to a single account. You can subscribe to multiple accounts simultaneously, allowing you to receive updates for multiple accounts in real-time without the need to constantly poll the server. Of course, this is just one example of using the `accountSubscribe` method. You can apply the same principles to subscribe to other RPC methods, such as `blockSubscribe` to receive updates on new confirmed blocks, or `programSubscribe` to track changes to specific Solana programs.
Encoding and Commitment
When subscribing to accounts, you can specify the encoding format for the account data and the commitment level for the notifications. The `encoding` parameter determines how the account data is encoded in the notifications. The available options are:
- `base58`: Account data is returned as a base58-encoded string (slower).
- `base64`: Account data is returned as a base64-encoded string (preferred).
- `jsonParsed`: Attempts to use program-specific state parsers to return more human-readable and explicit account data.
If you choose `jsonParsed` encoding and subscribe to a token account, you will receive parsed token account data in the notifications. This includes information such as the token mint, owner, token amount, and more. The parsed data provides a convenient format that you can directly use in your application.
The `commitment` parameter specifies the commitment level required for the account updates. The available options are:
- `processed`: Returns updates as soon as the transaction is processed by the node.
- `confirmed`: Returns updates after the transaction has been confirmed by the network (default).
- `finalized`: Returns updates after the transaction has been finalized and is irreversible.
By default, the commitment level is set to `finalized`. If you require faster updates and can tolerate potentially unconfirmed or rollbackable transactions, you can use `confirmed` or `processed` commitment levels. However, keep in mind that with `processed` commitment, there is a possibility that the transaction may not be fully confirmed by the network.
It's important to choose the appropriate commitment level based on your application's requirements. If you need high certainty and can tolerate slower updates, stick with `finalized`. If you prioritize speed and can handle potential inconsistencies, you can opt for `confirmed` or `processed`.
Unsubscribing from Account Notifications
In addition to subscribing to account updates, you can also unsubscribe from them when you no longer need to receive notifications for a particular account. To unsubscribe, you need to use the `accountUnsubscribe` method and provide the subscription ID that was returned when you initially subscribed.
Here's an example of how to unsubscribe from account notifications:
In the above code, we define a function `unsubscribeFromAccount` that takes the websocket instance and the subscription ID as parameters. It constructs a JSON-RPC request object with the `accountUnsubscribe` method and the subscription ID as the parameter.
To demonstrate unsubscribing, let's modify the code to unsubscribe from the account immediately after receiving the initial subscription confirmation:
In this code, we check if the received message has an ID of 1, which corresponds to the ID we used for the `accountSubscribe` request. If it matches, we extract the subscription ID from the `result` field and log a message indicating the successful subscription. Immediately after that, we call the `unsubscribeFromAccount` function, passing the websocket instance and the subscription ID.
If the unsubscribe request is successful, we will receive a response with `result` set to `true`, and we log a message indicating the successful unsubscription. After unsubscribing, even if you make changes to the account, you will no longer receive any notifications for that account.
We can also unsubscribe from the account subscription immediately after receiving the first notification. Here's how that would look:
In this updated code, we're checking if the incoming message is an accountNotification. If so, we log the new account balance and then immediately unsubscribe from the account subscription. The key difference is that we're using the data.params.subscription value as the parameter for the unsubscribeFromAccount function, instead of the subscriptionId variable we stored earlier.
Block Subscription
In addition to subscribing to individual accounts or programs, Solana also provides the ability to subscribe to blocks using the `blockSubscribe` method. This allows you to receive notifications whenever a new block is confirmed on the Solana network.
The `blockSubscribe` method is considered an unstable method, which means it may not be available on all validators. Validators need to be started with a specific RPC flag to enable these unstable methods.
When subscribing to blocks, you have the option to filter the notifications based on certain criteria:
- `all`: Include all transactions in the block.
- `mentions`: Include only transactions that mention a provided public key (account or program).
You can also specify the level of transaction detail to return:
- `full`: Return full transaction signatures.
- `signatures`: Return only transaction signatures.
- `none`: Return no transaction details.
The notification format for block subscriptions includes the following fields:
- `slot`: The slot in which the block was produced.
- `error`: Any error that occurred during block processing.
- `block`: The block information, similar to the response of the `getBlock` RPC method.
`previousBlockhash`: The hash of the previous block.
`blockhash`: The hash of the current block.
`parentSlot`: The slot of the parent block.
`transactions`: The list of transactions included in the block.
`blockTime`: The timestamp of the block.
`blockHeight`: The height of the block.
Here's an example of subscribing to blocks and handling the notifications:
In this updated function, we're adding the mentions parameter to the blockSubscribe request. This tells the Solana RPC server to only send us notifications for blocks that mention the specified account public key.
We're also including the following additional parameters:
commitment: This sets the commitment level to 'confirmed', meaning we'll only receive notifications for blocks that have been fully confirmed.
encoding: This sets the encoding for the block data to 'base64', which makes it easier to work with.
showRewards: This tells the server to include reward information in the block notifications.
transactionDetails: This sets the level of transaction detail to 'full', meaning we'll receive the full transaction data, including signatures, for each transaction in the block.
Now, when we call the subscribeToBlocks function and pass in an account public key, we'll only receive notifications for blocks that mention that specific account:
In the WebSocket event listener, we can then process the block notifications as before, but with the added context of knowing that the block contains transactions related to the specified account:
By using the mentions parameter in the blockSubscribe request, you can create highly targeted subscriptions that only deliver the information you need, reducing the amount of data you need to process and making your application more efficient.
Slot Subscription
Similar to block subscriptions, you can also subscribe to slots using the `slotSubscribe` method. This method notifies you whenever a slot is processed by the validator.
The notification format for slot subscriptions includes the following fields:
- `parent`: The parent slot.
- `root`: The root slot.
- `slot`: The current slot.
Here's an example of subscribing to slots:
In this code, the `subscribeToSlots` function is used to subscribe to slots. When a slot notification is received, the `message` event handler checks if the `method` is `slotNotification`. If it is, it extracts the `slot` from the `params.result` field and logs it to the console.
Slot subscriptions can be useful if you want to keep track of the current slot or maintain a live view of the slot progression. However, for most application-level use cases, block subscriptions or account/program subscriptions are more relevant.
Program Subscription
Subscribing to a program allows you to receive notifications whenever an account owned by that program changes. This is similar to account subscriptions but applies to all accounts associated with a specific program.
To subscribe to a program, you need to provide the following parameters:
- `programId`: The public key of the program.
- `commitment`: The commitment level for the notifications (`processed`, `confirmed`, or `finalized`).
- `filters` (optional): Additional filters to apply to the account changes.
The notification format for program subscriptions is similar to the response of the `getProgramAccounts` RPC method. Each notification represents a single account change and includes the following fields:
- `pubkey`: The public key of the account.
- `account`: The account information, including the data, lamports, owner, and other metadata.
Here's an example of subscribing to a program:
In this code, the `subscribeToProgramAccounts` function is used to subscribe to a program by providing the program ID. The `encoding` is set to `jsonParsed` to receive the account data in a parsed format, and the `commitment` is set to `confirmed`.
When an account owned by the Token program is modified, we'll receive a programNotification message with the updated account data:
In the event listener, we're extracting the updated account's public key and account data from the programNotification message.
Program subscriptions can be useful when you want to monitor changes across all accounts associated with a specific program. For example, if you have a custom program and want to react to any modifications made to its accounts, you can subscribe to that program and handle the notifications accordingly.
Conclusion
Solana provides various subscription methods through its websocket API, allowing you to receive real-time updates on different aspects of the blockchain state. Whether you want to subscribe to individual accounts, blocks, slots, or programs, you can leverage these subscriptions to build responsive and event-driven applications.
By using websockets and subscriptions, you can efficiently receive updates without the need for constant polling. This enables you to react to changes in real-time and provide a seamless user experience. Remember to handle websocket connections and subscriptions carefully, considering factors such as connection limits, data volume, and performance implications. Choose the appropriate subscription methods based on your application's requirements and optimize your code to process the received notifications effectively. With the power of Solana websockets and subscriptions, you can unlock a wide range of possibilities for building interactive and real-time applications on the Solana blockchain.