Building with Solana Web3.js 2.0 SDK: A Developer's Guide

·

The Solana Web3.js SDK is a powerful TypeScript and JavaScript library designed for building Solana applications across Node.js, web, and React Native platforms. In November 2024, Anza released the highly anticipated 2.0 SDK update, introducing a range of modern JavaScript features and significant improvements. This guide explores the key updates, migration steps, and practical examples to help you start building with the new SDK.

What’s New in Web3.js 2.0?

The latest version of the Solana Web3.js SDK brings several enhancements that improve performance, efficiency, and flexibility for developers.

Performance Improvements

Cryptographic operations are now up to 10 times faster. Key pair generation, transaction signing, and message verification leverage native cryptographic APIs in modern JavaScript environments like Node.js and current browsers.

Smaller Application Bundles

Web3.js 2.0 fully supports tree-shaking, allowing you to include only the parts of the library you use. This significantly reduces bundle size. Additionally, the new SDK has no external dependencies, ensuring lightweight and secure builds.

Enhanced Flexibility

Developers can now create custom solutions by:

The new TypeScript clients are hosted under the @solana-program GitHub organization. These clients are auto-generated using Codama, enabling rapid client generation for custom programs.

Should You Migrate to Web3.js v2 Now?

As of early 2025:

Migrating from Web3.js Version 1

If you've used Web3.js v1, here's a quick summary of the key differences:

Key Pairs

Wherever you used Keypair, now use KeyPairSigner. The Keypair.generate() method is now generateKeyPairSigner(). Key pairs are now spelled as keyPair throughout, following standard JS/TS camelCase conventions.

Private keys are now called privateKey and accessible via keyPairSigner.privateKey. Generally, in Web3.js v2, you use KeyPairSigner anywhere you previously used secretKey.

Addresses and Public Keys

Where you used PublicKey in Web3.js v1, simply use address in Web3.js v2. For example, KeyPairSigner has a keypairSigner.address property representing its public key. You can convert string public keys to addresses using the address function.

SOL and Token Amounts

Amounts now use the native JS BigInt type. You need to add n to the end of numbers, turning 1 into 1n.

Factory Methods

Many features are now configurable through factory methods. Instead of preset implementations (like doThing()), you have factories (called doThingFactory()) that create custom functions. For example:

How to Send Transactions Using Web3.js 2.0

Helius recently released Kite, a TypeScript framework for Web3.js v2, which includes one-off functions for most common Solana tasks.

We'll build a client program using Web3.js 2.0 to transfer lamports to another wallet, demonstrating techniques to improve transaction success rates and confirmation times.

We'll follow these best practices for sending transactions:

  1. Get the latest block hash using the confirmed commitment level
  2. Set priority fees recommended by Helius's Priority Fee API
  3. Optimize compute units
  4. Set transaction sending with maxRetries as 0 and skipPreflight as true

This approach ensures optimal performance and reliability even during network congestion.

Prerequisites

Installation

First, create 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 add an index.ts file inside it for your main code:

mkdir src
touch src/index.ts

Next, install the required dependencies for working with Solana's Web3.js 2.0 SDK using npm:

npm install @solana/web3.js@2 @solana-program/system @solana-program/compute-budget esrun

Here's what each package provides:

Define Transfer Addresses

In index.ts, define the source and destination addresses for transferring lamports. We'll use the address() function to generate the destination public key from a provided string.

For the source address, we'll derive a KeyPair from 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 Connection

Next, set up the relevant RPC connections. The createSolanaRpc function establishes communication with the RPC server using the default HTTP transport, suitable for most use cases.

Similarly, we use createSolanaRpcSubscriptions to establish a WebSocket connection. You can find both rpc_url and wss_url in the Helius Dashboard—simply register or log in and navigate to the "Endpoints" section.

The sendAndConfirmTransactionFactory function builds a reusable transaction sender. This sender requires both 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 Transfer Instruction

Including a recent block hash prevents transaction duplication and provides an expiration time for transactions—every transaction must include a valid block hash to be accepted for execution. For this transaction, we'll use the confirmed commitment level to get the latest block hash.

Then, we'll use getTransferSolInstruction() to create a predefined transfer instruction provided by the System Program. This requires specifying the amount, source address, and destination address. The source address must always be a Signer, while the destination should be a public key:

import {
  lamports,
} from "@solana/web3.js";
import { getTransferSolInstruction } from "@solana-program/system";

/**
 * Step 1: Create transfer transaction
 */
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const instruction = getTransferSolInstruction({
  amount: lamports(1n),
  destination: destinationAddress,
  source: sourceKeypair,
});

Create Transaction Message

Next, we'll create the transaction message. All transaction messages are now version-aware, eliminating the need to handle different types (like Transaction vs. VersionedTransaction).

We'll set the source address as the fee payer, include the block hash, and add the instruction to transfer 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, common in functional programming, creates a sequence of functions where the output of one becomes the input to the next. Here, it progressively builds the transaction message by applying transformations to set the fee payer, validity period, and add 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 address.

Set validity using block hash:

message => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message) ensures the transaction is valid for a period using the latest block hash.

Add transfer instruction:

message => appendTransactionMessageInstruction(instruction, message) attaches the action (transferring lamports) to the message.

Each arrow function message => (...) modifies and passes the updated message to the next step, resulting in a fully constructed transaction message.

Sign the Transaction

We'll sign the transaction using the specified signer, which is the source Keypair:

import {
  signTransactionMessageWithSigners,
} from "@solana/web3.js";

/**
 * Step 2: Sign the transaction
 */
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
console.log("Transaction signed");

Evaluate Priority Fees

At this point, we could proceed to send and confirm the transaction. However, we should optimize it 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'll use Helius's Priority Fee API. This requires serializing the transaction in Base64 format. While the API also supports Base58 encoding, the current SDK directly provides transactions 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 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'll evaluate how many compute units this transaction message actually consumes.

We then add a 10% buffer by multiplying this value by 1.1. This buffer accounts for priority fees and other compute unit instructions that might be added in later operations.

Some instructions, like transferring lamports, might have low compute unit estimates. To ensure sufficient resources, we add a safeguard that sets 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 compute units this message will actually 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 fee and compute units needed for this transaction. Since the transaction is already signed, we can't directly add new instructions. Therefore, we'll rebuild the entire transaction message with a new block hash.

Block hashes are valid for approximately 1-2 minutes, and obtaining priority fees and compute units takes some time. To avoid losing the block hash when sending the transaction, it's safer to get a new block hash when rebuilding the transaction.

In this rebuilt transaction, we'll include two additional instructions:

  1. One to set the priority fee
  2. Another to set the compute units

Finally, we'll 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 transaction signed");

Send and Confirm the Transaction

Next, use the sendAndConfirmTransaction function to send and confirm the signed transaction.

The commitment level for submission is set to confirmed, matching the block hash obtained earlier, while maxRetries is set to 0. The skipPreflight option is set to true, skipping preflight checks for faster execution—however, this should only be used when you're confident the transaction signature is verified and there are no other errors.

When creating the sendAndConfirmTransaction earlier, both RPC and RPC subscription URLs were provided. The RPC subscription URL checks transaction status, eliminating the need for manual polling.

The error handling section checks for errors that occur during preflight. Since we set skipPreflight to true, this check is redundant. However, it would be useful if you didn't 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 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, run the code with:

npx esrun send-transaction.ts

👉 Explore more development strategies

Frequently Asked Questions

What are the main benefits of upgrading to Web3.js 2.0?

The upgrade offers significantly faster cryptographic operations (up to 10x improvement), reduced bundle sizes through tree-shaking, and enhanced flexibility for custom implementations. The elimination of external dependencies also improves security and reduces potential vulnerability points.

Is Web3.js 2.0 compatible with existing Solana programs?

Yes, Web3.js 2.0 works with existing Solana programs like the System Program, Token Program, and Associated Token Program. However, if you use Anchor for custom on-chain applications, you may need to wait for Anchor to add support or use Codama to generate TypeScript clients.

How does priority fee estimation improve transaction success?

Priority fees help ensure your transactions are processed during network congestion by incentivizing validators to prioritize them. Using Helius's Priority Fee API provides data-driven recommendations for appropriate fee levels based on current network conditions.

What's the purpose of compute unit optimization?

Compute unit optimization ensures your transactions have sufficient resources to execute without exceeding limits. By accurately estimating compute requirements and adding a buffer, you reduce the likelihood of transaction failures due to insufficient compute units.

Can I use Web3.js 2.0 with React applications?

Yes, Web3.js 2.0 works with web applications including those built with React. The library supports tree-shaking, which is particularly beneficial for frontend applications where bundle size impacts performance.

How does the new factory pattern improve code organization?

The factory pattern allows you to create customized functions with predefined configurations. This approach promotes code reusability, reduces duplication, and makes it easier to maintain consistent behavior across different parts of your application.

Conclusion

The release of Solana's Web3.js 2.0 SDK represents a transformative update that enables developers to create faster, more efficient, and scalable applications on Solana. By adopting modern JavaScript standards and introducing features like native cryptographic APIs, tree-shaking, and auto-generated TypeScript clients, the SDK significantly enhances both developer experience and application performance.

The complete code for the programming example is available on GitHub.

👉 View real-time development tools

Resources