
How to Start Building with the Solana Web3.js 2.0 SDK
Before we dive in, we want to thank Evan and Nick for reviewing this article. Their valuable feedback and insights were greatly appreciated.
Introduction
The Solana Web3.js SDK is a powerful TypeScript and JavaScript library for building Solana applications across Node.js, web, and React Native platforms. On November 7, 2024, Anza introduced the highly anticipated 2.0 SDK update, introducing a host of modern JavaScript features and improvements. Key highlights include standard JS types for bigints and crypto, and reduced bundle sizes, making it a significant upgrade for developers.
If you’ve been using @solana/web3.js
, you’ll need to either port your software to the new v2.0 package or explicitly specify the version to lock it to v1.x.
In this article, we’ll explore the latest updates in the Web3.js 2.0 SDK, guide you through the migration process, and provide an example to help you get started.
This article assumes a solid understanding of foundational Solana concepts, such as sending transactions, the Solana account model, blockhashes, and priority fees, along with experience in TypeScript or JavaScript. While familiarity with the previous version of the Web3.js SDK is recommended, it is not mandatory. Let’s get started!
What is new in Web3.js 2.0?
Let’s quickly explore what the new Web3.js 2.0 SDK has to offer:
1. Performance Improvements
Faster cryptographic operations: keypair generation, transaction signing, and message verification are up to 10x faster, leveraging native cryptography APIs in modern JavaScript environments like Node.js and current browsers.
2. Smaller and Efficient Applications
Web3.js 2.0 is fully tree-shakable, allowing you to include only the parts of the library you use, minimizing your bundle size. Additionally, the new SDK has zero external dependencies, ensuring a lightweight and secure build.
3. Enhanced Flexibility
Developers can now create custom solutions by:
- Defining RPC instances with custom methods
- Using specialized network transports or transaction signers
- Composing custom primitives for networking, transaction confirmations, and codecs
The new TypeScript clients for on-chain programs are now hosted in the @solana-program GitHub organization. These clients are autogenerated using Codama, enabling developers to generate clients for custom programs quickly.
Should You Move to Web3.js v2 Yet?
As of February 2025:
- If you’re making a new Solana app in JS/TS, and using existing programs like the system program, token program, associated token program, and other common programs, you can use web3.js v2 now.
- If you’re creating custom on-chain apps using Anchor, you may wish to wait—Anchor doesn’t support web3.js v2 out of the box just yet. You may want to wait for a future Anchor update. Alternatively, use Codama to create a TypeScript client for your on-chain apps, though this is a little more work.
Migrating from web3.js version 1
If you’ve used web3.js v1, here’s a quick summary of the important differences:
Keypairs
Everywhere you'd use Keypair
you now use a KeyPairSigner
. Keypair.generate()
is now generateKeyPairSigner()
. Also, keypair is now spelled keyPair everywhere, like normal JS/TS camelCase.
Secret keys are now called privateKey
and are accessible on keyPairSigner.privateKey
. You generally use KeyPairSigner
in web3.js v2 anywhere a secretKey was used in web3.js v1.
Addresses / Public keys
Places that used a PublicKey in web3.js v1 just use an address in web3.js v2. For example, KeyPairSigner
s have a keypairSigner.address
property, which is their public key. You can make a string Public Key into an address using the address
function.
SOL and Token Amounts
Amounts use the native JS BigInt
type. So, you’d add n
at the end of numbers, making one 1n
instead of 1
.
Factories
Many features are configurable, so rather than having a pre-set implementation (e.g., doThing()
), there’s a factory (called doThingFactory()
) that you can use to create your own doThing()
function. For example:
- To send and confirm transactions, you run
sendAndConfirmTransactionFactory()
once with your preferred options, and get a customsendAndConfirmTransaction()
function back. You can then use yoursendAndConfirmTransaction()
whenever you need to send and confirm a transaction. - To get an airdrop on devnet or localnet, you run
airdropFactory()
once, and get a customairdrop()
function you can use whenever you want an airdrop.
How to Send Transactions with Web3.js 2.0
Kite Framework
Helius has recently published Kite, a TypeScript framework for web3.js v2 that includes one-shot functions for most common Solana tasks.
We will build a client-side program using Web3.js 2.0 to transfer lamports to another wallet. This program will showcase techniques to enhance transaction success rates and faster confirmation times.
We will adhere to these best practices for sending transactions:
- Fetch the latest blockhash with a confirmed commitment level
- Set priority fees as recommended by Helius’ Priority Fee API
- Optimize compute units
- Send the transaction with maxRetries set to 0 and skipPreflight set to true
This approach ensures optimal performance and reliability, even during network congestion.
Prerequisites
- Install Node.js
- A compatible IDE (e.g., VS Code or Cursor)
Installation
Start by creating a basic Node.js project to structure your application.
Run the following command to create a package.json
file that manages your dependencies and project metadata:
npm init -y
Create a src
directory, and within it, add an index.ts
file where the main code will reside:
mkdir src
touch src/index.ts
Next, use npm
to install the necessary dependencies for working with Solana's Web3.js 2.0 SDK:
npm install @solana/web3.js@2 @solana-program/system @solana-program/compute-budget esrun
Here's a description of each package:
@solana/web3.js
: the Solana Web3.js 2.0 SDK is essential for building and managing Solana transactions@solana-program/system
: provides access to the Solana System Program, enabling operations like lamport transfers@solana-program/compute-budget
: used for setting priority fees and optimizing compute units for transactionsesrun
is a simple way to run TypeScript apps from the command line without needing config or wrapper functions.
Define Transfer Addresses
In index.ts
, let's define the source and destination addresses for transferring lamports. We will use the address()
function to generate the destination public key from the provided string.
For the source, we will derive the KeyPair
using its secretKey
.
import { address, createKeyPairSignerFromBytes, getBase58Encoder } from "@solana/web3.js";
const destinationAddress = address("public-key-to-send-lamports-to");
const secretKey = "add-your-private-key";
const sourceKeypair = await createKeyPairSignerFromBytes(getBase58Encoder().encode(secretKey));
Configure RPC Connections
Next, we can set up the relevant RPC connections. The createSolanaRpc
function establishes communication with the RPC server using a default HTTP transport, which is sufficient for most use cases.
Similarly, we use createSolanaRpcSubscriptions
to establish a WebSocket connection. The rpc_url
and wss_url
are in the Helius Dashboard—simply sign up or log in and navigate to the “Endpoints” section.
The sendAndConfirmTransactionFactory
function builds a reusable transaction sender. This sender requires an RPC connection to send transactions and an RPC subscription to monitor transaction status.
import {
// ...
createSolanaRpcSubscriptions,
createSolanaRpc,
sendAndConfirmTransactionFactory,
} from "@solana/web3.js";
const rpc_url = "https://mainnet.helius-rpc.com/?api-key=<your-key>";
const wss_url = "wss://mainnet.helius-rpc.com/?api-key=<your-key>";
const rpc = createSolanaRpc(rpc_url);
const rpcSubscriptions = createSolanaRpcSubscriptions(wss_url);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
Create a Transfer Instruction
Including a recent blockhash prevents duplication and provides transactions with lifetimes—every transaction has to include a valid blockhash to be accepted for execution. For this transaction, we’ll fetch the latest blockhash using the confirmed commitment level.
Next, we'll use getTransferSolInstruction()
to create a predefined transfer instruction provided by the System Program. This requires specifying the amount, source
, and destination. The source must always be a Signer, while the destination should be a public address.
import {
// ...
lamports,
} from "@solana/web3.js";
import { getTransferSolInstruction } from "@solana-program/system";
/**
* STEP 1: CREATE THE TRANSFER TRANSACTION
*/
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const instruction = getTransferSolInstruction({
amount: lamports(1n),
destination: destinationAddress,
source: sourceKeypair,
});
Create Transaction Message
We will then create the transaction message. All transaction messages are now version-aware, eliminating the need to handle different types (e.g., Transaction
vs. VersionedTransaction
).
We'll set the source as the fee payer, include the blockhash, and add the instruction for transferring lamports.
import {
// ...
pipe,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
} from "@solana/web3.js";
// ...
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(message) => setTransactionMessageFeePayer(sourceKeypair.address, message),
(message) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message),
(message) => appendTransactionMessageInstruction(instruction, message),
);
console.log("Transaction message created");
The pipe function, commonly used in functional programming, creates a sequence of functions where the output of one becomes the input of the next. Here, it builds a transaction message step by step, applying transformations like setting the fee payer and lifetime and adding instructions.
Initialize Transaction Message:
createTransactionMessage({ version: 0 })
starts with a basic transaction message.
Set Fee Payer:
message
=> setTransactionMessageFeePayer(fromKeypair.address, message)
adds the fee payer's address.
Set Lifetime Using Blockhash
message => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message)
ensures the transaction is valid within a timeframe using the latest blockhash.
Add Transfer Instruction
message => appendTransactionMessageInstruction(instruction, message)
appends the action (e.g., transferring lamports) to the message.
Each arrow function message => (...)
modifies and passes the updated message to the next step, producing an entirely constructed new transaction message.
Sign the Transaction
We will sign the transaction using the specified signer, the source Keypair
.
import {
// ...
signTransactionMessageWithSigners,
} from "@solana/web3.js";
// ...
/**
* STEP 2: SIGN THE TRANSACTION
*/
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
console.log("Transaction signed");
Estimate Priority Fees
At this step, we can move forward with sending and confirming the transaction. However, we should optimize the transaction by setting priority fees and adjusting compute units. These optimizations help improve transaction success rates and reduce confirmation times, especially during network congestion.
To set priority fees, we will use Helius' Priority Fee API. This requires the serialized transaction in Base64 format. While the API also supports Base58 encoding, the current SDK directly provides the transaction in Base64 format, simplifying the process.
import {
// ...
getBase64EncodedWireTransaction,
} from "@solana/web3.js";
/**
* STEP 3: GET PRIORITY FEE FROM SIGNED TRANSACTION
*/
const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction);
const response = await fetch(rpc_url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: "helius-example",
method: "getPriorityFeeEstimate",
params: [
{
transaction: base64EncodedWireTransaction,
options: {
transactionEncoding: "base64",
priorityLevel: "High",
},
},
],
}),
});
const { result } = await response.json();
const priorityFee = result.priorityFeeEstimate;
console.log("Setting priority fee to ", priorityFee);
Setting the priorityLevel
to High
is usually sufficient. However, implementing advanced priority fee strategies can significantly improve transaction success rates during network congestion.
Optimize Compute Units
Next, we will estimate the actual compute units the transaction message consumes.
Then, we add a 10% buffer by multiplying this value by 1.1. This buffer accounts for the compute units used by priority fees and additional compute unit instructions, which we will incorporate later.
Some instructions, such as transferring lamports, may have a lower compute unit estimate. To ensure sufficient resources, we’ve added a safeguard to set the compute units to a minimum of 1000 if the estimate falls below this threshold.
import {
// ...
getComputeUnitEstimateForTransactionMessageFactory,
} from "@solana/web3.js";
/**
* STEP 4: OPTIMIZE COMPUTE UNITS
*/
const getComputeUnitEstimateForTransactionMessage = getComputeUnitEstimateForTransactionMessageFactory({
rpc,
});
// Request an estimate of the actual compute units this message will consume.
let computeUnitsEstimate = await getComputeUnitEstimateForTransactionMessage(transactionMessage);
computeUnitsEstimate = computeUnitsEstimate < 1000 ? 1000 : Math.ceil(computeUnitsEstimate * 1.1);
console.log("Setting compute units to ", computeUnitsEstimate);
Rebuild and Sign the Transaction
We now have the priority fees and compute units required for this transaction. Since the transaction has already been signed, we cannot directly add new instructions. Instead, we will rebuild the entire transaction message with a new blockhash.
Blockhashes are only valid for about 1-2 minutes, and fetching the priority fees and compute units takes some time. To avoid the risk of the blockhash expiring while sending the transaction, it’s safer to get a new one while rebuilding the transaction.
In this rebuilt transaction, we will include two additional instructions:
- One instruction to set the priority fees and;
- Another instruction to set the compute units
Finally, we will sign this updated transaction to prepare it for submission:
import {
// ...
appendTransactionMessageInstructions,
} from "@solana/web3.js";
import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from "@solana-program/compute-budget";
/**
* STEP 5: REBUILD AND SIGN FINAL TRANSACTION
*/
const { value: finalLatestBlockhash } = await rpc.getLatestBlockhash().send();
const finalTransactionMessage = appendTransactionMessageInstructions(
[
getSetComputeUnitPriceInstruction({ microLamports: priorityFee }),
getSetComputeUnitLimitInstruction({ units: computeUnitsEstimate }),
],
transactionMessage,
);
setTransactionMessageLifetimeUsingBlockhash(finalLatestBlockhash, finalTransactionMessage);
const finalSignedTransaction = await signTransactionMessageWithSigners(finalTransactionMessage);
console.log("Rebuilt the transaction and signed it");
Send and Confirm Transaction
Next, the signed transaction is sent and confirmed using the sendAndConfirmTransaction
function.
The commitment level is set to confirmed, consistent with the blockhash fetched earlier, while maxRetries
is set to 0
. The skipPreflight
option is set to true
, bypassing preflight checks for faster execution; however, this should only be used when you are confident that your transaction signature is verified and there are no other errors.
The sendAndConfirmTransaction
was created earlier by providing both the RPC and RPC subscription URLs. Using the RPC subscription URL checks the transaction status, eliminating the need for manual polling.
In the error handling section, the code checks for errors that occurred during the preflight checks. Since we set skipPreflight
to true
, this check is redundant. However, it will be helpful if you do not set it to true.
import {
getSignatureFromTransaction,
isSolanaError,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
} from "@solana/web3.js";
import { getSystemErrorMessage, isSystemError } from "@solana-program/system";
/**
* STEP 6: SEND AND CONFIRM THE FINAL TRANSACTION
*/
console.log("Sending and confirming transaction");
await sendAndConfirmTransaction(finalSignedTransaction, {
commitment: "confirmed",
maxRetries: 0n,
skipPreflight: true,
});
console.log("Transfer confirmed: ", getSignatureFromTransaction(finalSignedTransaction));
Run the Code
Finally, we can run the code:npx esrun send-transaction.ts
Conclusion
The release of Solana's Web3.js 2.0 SDK is a transformative update that empowers developers to create faster, more efficient, and scalable applications on Solana. By embracing modern JavaScript standards and introducing features like native cryptographic APIs, tree-shakability, and autogenerated TypeScript clients, the SDK significantly enhances the developer experience and application performance.
The complete code for the programming example is on GitHub.
If you’ve read this far, thank you, anon! Be sure to enter your email address below so you’ll never miss an update about what’s new on Solana. Ready to dive deeper? Explore the latest articles on the Helius blog and continue your Solana journey today.
Resources
Related Articles
Subscribe to Helius
Stay up-to-date with the latest in Solana development and receive updates when we post