Ping Thing with the @solana/kit (formerly @solana/web3.js 2) SDK Walkthrough

This blog will give you a tour of the Ping Thing service to show how to perform various operations using the @solana/kit (formerly @solana/web3.js 2) SDK. The goal is to provide you with some code snippets and examples of how to do things using the new shiny SDK.

If you haven’t read the intro to the kit SDK blog, read it here.

The code for the Ping Thing scripts is available on GitHub

What is the Ping Thing?

The Ping Thing service is a collection of independent scripts that continuously send transaction to the chain in order to measure and report the transaction landing time and slot latency. Ping-thing sends three types of transactions:

  1. SOL transfer
  2. Token Transfer (USDC)
  3. Jupiter Swaps (SOL to USDC)

At a high level, this seemingly simple script executes some of the most common codepaths performed by anyone interacting with Solana. In the following sections, I will line by line walk through the new kit SDK ping-thing script.

First things first: Imports

The kit SDK has independent packages. This is majorly done to support modular design and tree shakeability (more info here). Here is how the import statements can look:

import {
  createTransactionMessage,
  pipe,
  setTransactionMessageFeePayer,
  setTransactionMessageLifetimeUsingBlockhash,
  createKeyPairFromBytes,
  getAddressFromPublicKey,
  createSignerFromKeyPair,
  signTransaction,
  appendTransactionMessageInstructions,
  sendTransactionWithoutConfirmingFactory,
  createSolanaRpcSubscriptions_UNSTABLE,
  getSignatureFromTransaction,
  compileTransaction,
  createSolanaRpc,
  SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND,
  isSolanaError,
  type Signature,
  type Commitment,
} from "@solana/kit";
import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from "@solana-program/compute-budget";
import { getTransferSolInstruction } from "@solana-program/system";
import { createRecentSignatureConfirmationPromiseFactory } from "@solana/transaction-confirmation";

Creating RPC Connections (GitHub)

Before anything can happen we need to establish a connection to the RPC server. We create two connections - one regular RPC and one for web socket subscriptions.

// RPC connection for HTTP API calls, equivalent to `const c = new Connection(RPC_ENDPOINT)`
const rpcConnection = createSolanaRpc(RPC_ENDPOINT!);

// RPC connection for websocket connection
const rpcSubscriptions = createSolanaRpcSubscriptions_UNSTABLE(WS_ENDPOINT!);

Reading Keypair from Environment (GitHub)

We load a key pair which the pinger will use to sign transactions using an environment variable.

const USER_KEYPAIR = await createKeyPairFromBytes(
    bs58.decode(process.env.WALLET_PRIVATE_KEYPAIR!)
);

Getting Fee-payer Address & Signer (GitHub)

The key pair allows us to derive the addresses for fee-payer and signer for the transactions we need to create.

const feePayer = await getAddressFromPublicKey(USER_KEYPAIR.publicKey);
const signer = await createSignerFromKeyPair(USER_KEYPAIR);

Creating Transaction (GitHub)

Here we will create a SOL transfer transaction and set version, fee payer, blockhash and the transfer instruction. The ping-thing always aim to implement best practices for transaction sending including managing block hashes and setting compute limits. You can read more about the blockhash management of the ping-thing in the source code here.

const transaction = pipe(
  createTransactionMessage({ version: 0 }),
  (tx) => setTransactionMessageFeePayer(feePayer, tx),
  (tx) =>
    setTransactionMessageLifetimeUsingBlockhash(
      {
        blockhash: gBlockhash.value!,
        lastValidBlockHeight: BigInt(gBlockhash.lastValidBlockHeight!),
      },
      tx
    ),
  (tx) =>
    appendTransactionMessageInstructions(
      [
        getSetComputeUnitLimitInstruction({
          units: 500,
        }),
        getSetComputeUnitPriceInstruction({ microLamports: BigInt(PRIORITY_FEE_MICRO_LAMPORTS) }),

        // SOL transfer instruction
        getTransferSolInstruction({
          source: signer,
          destination: feePayer,
          amount: 5000,
        }),
      ],
      tx
    )
);

Signing the Transaction (GitHub)

Once we have constructed the transaction, we sign it using the key pair previously loaded.

// Sign the tx
const transactionSignedWithFeePayer = await signTransaction(
  [USER_KEYPAIR],
  compileTransaction(transaction)
);

// Get the tx signature
signature = getSignatureFromTransaction(transactionSignedWithFeePayer);

Sending the Transaction (GitHub)

We send the transaction. For sending, we first create a sending factory and then send the transaction. These factories allow us to create custom strategies. More on this in a later blog.

// Create a sender factory that sends a transaction and doesn't wait for confirmation
const mSendTransaction = sendTransactionWithoutConfirmingFactory({
  rpc: rpcConnection,
});

// Send the tx
await mSendTransaction(transactionSignedWithFeePayer, {
  commitment: "confirmed",
  maxRetries: 0n,
  skipPreflight: true,
});

Confirming the Transaction (GitHub)

We create a transaction confirmation factory and use it to confirm our transactions.

// Create a promise factory that has the logic for a the tx to be confirmed
const getRecentSignatureConfirmationPromise =
  createRecentSignatureConfirmationPromiseFactory({
    rpc: rpcConnection,
    rpcSubscriptions,
  });

await getRecentSignatureConfirmationPromise({
  signature,
  commitment: "confirmed",
  abortSignal: abortController.signal,
})

Getting Blockhash (GitHub)

Getting blockhash and other RPC calls for fetching data are done with the following syntax. Make sure you do the send() call after calling the method.

const latestBlockhash = await connection.getLatestBlockhash().send()

Getting Slot Updates (GitHub)

Subscriptions are now based on AsyncIterator. Here we create a slotsUpdateSubscribe subscription and update a global slot value

// Subscribing to the `slotsUpdatesSubscribe` and update slot number
// https://solana.com/docs/rpc/websocket/slotsupdatessubscribe
const slotNotifications = await rpcSubscription
  .slotsUpdatesNotifications()
  .subscribe({ abortSignal: abortController.signal });

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator
for await (const notification of slotNotifications) {
  if (
    notification.type === "firstShredReceived" ||
    notification.type === "completed"
  ) {
    gSlotSent.value = notification.slot;
    gSlotSent.updated_at = Date.now();
    attempts = 0;
    continue;
  }
}

Why is everything behind factories?

You might have noticed that functions like sending and confirming transactions are obtained from factory functions like sendTransactionWithoutConfirmingFactory and createRecentSignatureConfirmationPromiseFactory. Here’s an explanation for why this is a good design choice. TL;DR is:

  • Binding together multiple RPC interfaces so you don’t have to pass them separately
  • Assembling the strategy (do x, y, z in a particular order)
  • Create a closure that can retain state that spans multiple invocations across your app.

Conclusion

Follow along this blog series to learn about live projects using the 2.x SDK and much more. Reach out to Wilfred if you have any doubts or need any help with the kit SDK.

If you need a break from migrating to kit, checkout other cool suff Triton has.