How to Start Building with the Solana Web3.js 2.0 SDK
/Development

How to Start Building with the Solana Web3.js 2.0 SDK

10 min read

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 custom sendAndConfirmTransaction() function back. You can then use your sendAndConfirmTransaction() whenever you need to send and confirm a transaction. 
  • To get an airdrop on devnet or localnet, you run airdropFactory() once, and get a custom airdrop() function you can use whenever you want an airdrop.

How to Send Transactions with Web3.js 2.0

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: 

  1. Fetch the latest blockhash with a confirmed commitment level  
  2. Set priority fees as recommended by Helius’ Priority Fee API
  3. Optimize compute units  
  4. 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:

Shell commands
npm init -y

Create a src directory, and within it, add an index.ts file where the main code will reside:

Shell commands
mkdir src  
touch src/index.ts

Next, use npm to install the necessary dependencies for working with Solana's Web3.js 2.0 SDK:

Shell commands
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 transactions
  • esrun 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.

send-transaction.ts
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.

send-transaction.ts
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.

send-transaction.ts
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.

send-transaction.ts
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.

send-transaction.ts
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.

send-transaction.ts
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.

send-transaction.ts
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: 

  1. One instruction to set the priority fees and;
  2. Another instruction to set the compute units 

Finally, we will sign this updated transaction to prepare it for submission:

send-transaction.ts
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.

send-transaction.ts
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