DEV Community

Mohammed Alsabaawi
Mohammed Alsabaawi

Posted on

Atom transactions using Jito bundles

Summary

In this blog, you learn:

  • The Solana transaction size limitation
  • Jito bundles, what they are, and why we need them
  • A practical example with TS on how to build and submit a Jito bundler to the network (Github repo)

Introduction

Solana is a fast, cheap, and performant network; it has a very high TPS, the best of all other chains. But we are not living in a perfect world; everything comes with a price. When you make something that good and cheap, tradeoffs are inevitable. One of these tradeoffs is the transaction size limitation.

Solana transaction size limitation

Solana network enforces a 1232-byte transaction size limit to ensure each transaction fits into a single network packet. This design choice is deliberate and efficient. Solana network adheres to a maximum transmission unit (MTU) size of 1280 bytes, consistent with the IPv6 MTU, and after accounting for the necessary headers (40 bytes for IPv6 and 8 bytes for the fragment header), 1232 bytes remain available for packet data. You can read more about this here

While this is sufficient for most use cases, there are times when you might need more. And if, for some reason, you want to have two transactions, and you want one of them to get submitted before the other, and you want to make sure that the second transaction never reaches the network if the first one has some issues,

You can't sign and send both transactions simultaneously because there's no guarantee the first transaction will be submitted first. Instead, you must sign and submit the first transaction, wait for confirmation, then sign and submit the second. This process creates a poor user experience, as users must sign twice for a single action. So, what's the solution? Here comes Jito Bundles

Jito

Jito is one of the biggest projects on the Solana network; they do a lot of cool things (check their blog), but we are now interested in the Jito Bundles feature. Jito Bundles consist of up to five transactions that execute sequentially and atomically, ensuring an all-or-nothing outcome.

How do Bundles work?

  • Traders submit bundles to block engines
  • Block engines simulate bundles to determine the most profitable combinations
  • Winning bundles are sent to validators to include in blocks
  • Validators execute bundles atomically and collect tips

Practical Example

In this example, you will build and submit a bundle to Jito. You can find the example code in this GitHub repo

Similar to Solana's priority fees, Jito employs a mechanism to prioritize bundles called 'Jito tips.' These user-defined amounts incentivize validators to process bundles ahead of other transactions, ensuring faster execution.

Tips are simply a SOL transfer instruction to one of the known Jito tip accounts, and this instruction should be present in your bundle either as a part of another transaction or as a stand-alone transaction, we will look at both of them here.

If you go ahead and clone the repo, open the readme file and follow the instructions to install the dependencies (I use a bun, but feel free to use something else) and set up the env variables, then you can find two files inside the src folder, embedded-tip-ix.ts and separated-tip-tx.ts, they are pretty similar, the only change is that we are embedding the tip instruction in one of the bundle's transactions (the last one) in one of them, while we are separating it into a standalone transaction in the other one, you can read more about the best practices of tipping here and here

TL;DR: you should try to have the tipping instruction as the last one in the bundle, and it is better to have it inside a transaction and not a stand-alone transaction, in this case, you will make sure that your tip will not go for wast by a malicious validator or an uncle block

Now let's take a look at the file src/embedded-tip-ix.ts and skip the import part (because it is boring), the first function that you will see there is getRandomeTipAccountAddress

const getRandomeTipAccountAddress = async (
  searcherClient: searcher.SearcherClient,
) => {
  const account = await searcherClient.getTipAccounts();
  return new PublicKey(account[Math.floor(Math.random() * account.length)]);
};
Enter fullscreen mode Exit fullscreen mode

This function fetches the tip accounts and selects a random one. Using a random account instead of always selecting the first one increases the chance of landing our bundle, if too many transactions try to tip the same account, that might push some of them to fail.

The very next thing is the buildMemoTransaction:

const MEMO_PROGRAM_ID = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";

const buildMemoTransaction = (
  keypair: Keypair,
  message: string,
  recentBlockhash: string,
  tipIx?: TransactionInstruction,
): VersionedTransaction => {
  const ix = new TransactionInstruction({
    keys: [
      {
        pubkey: keypair.publicKey,
        isSigner: true,
        isWritable: true,
      },
    ],
    programId: new PublicKey(MEMO_PROGRAM_ID),
    data: Buffer.from(message),
  });

  const instructions = [ix];

  if (tipIx) instructions.push(tipIx);

  const messageV0 = new TransactionMessage({
    payerKey: keypair.publicKey,
    recentBlockhash: recentBlockhash,
    instructions,
  }).compileToV0Message();

  const tx = new VersionedTransaction(messageV0);

  tx.sign([keypair]);

  console.log("txn signature is: ", bs58.encode(tx.signatures[0]));
  return tx;
};
Enter fullscreen mode Exit fullscreen mode

Just a simple method that will build a memo transaction, which has no use, but it is so cheap and simple, so it is perfect for our example since we are going to do this on the main net, it also takes an optional tip instruction and pushes it in the transaction if provided.

Sitting that aside, let's get to the meat of this, the first few lines of the main function is just to setup few variables.

const main = async () => {
  const blockEngineUrl = process.env.BLOCK_ENGINE_URL || "";
  console.log("BLOCK_ENGINE_URL:", blockEngineUrl);

  const authKeypairPath = process.env.AUTH_KEYPAIR_PATH || "";
  console.log("AUTH_KEYPAIR_PATH:", authKeypairPath);
  const decodedKey = new Uint8Array(
    JSON.parse(Fs.readFileSync(authKeypairPath).toString()) as number[],
  );
  const keypair = Keypair.fromSecretKey(decodedKey);

  const bundleTransactionLimit = parseInt(
    process.env.BUNDLE_TRANSACTION_LIMIT || "5",
  );
Enter fullscreen mode Exit fullscreen mode

After that, we need to create the searcher client, which is what we will use to communicate with the Jito server

  // Create the searcher client that will interact with Jito
  const searcherClient = searcher.searcherClient(blockEngineUrl);
Enter fullscreen mode Exit fullscreen mode

And we can subscribe to the onBundleRequest so we get updates about our bundle once we send it to the network

  // Subscribe to the bundle result
  searcherClient.onBundleResult(
    (result) => {
      console.log("received bundle result:", result);
    },
    (e) => {
      throw e;
    },
  );
Enter fullscreen mode Exit fullscreen mode

Then we will get the tip account and build the tip instruction

  // Get a random tip account address
  const tipAccount = await getRandomeTipAccountAddress(searcherClient);
  console.log("tip account:", tipAccount);

  const rpcUrl = process.env.RPC_URL || "";
  console.log("RPC_URL:", rpcUrl);

  // get the latest blockhash
  const connection = new Connection(rpcUrl, "confirmed");
  const blockHash = await connection.getLatestBlockhash();

  // Build a Transfer Instruction
  const tipIx = SystemProgram.transfer({
    fromPubkey: keypair.publicKey,
    toPubkey: tipAccount,
    lamports: 1000,
  });
Enter fullscreen mode Exit fullscreen mode

As mentioned earlier, the tip can be included in one of the bundle transactions. Alternatively, it can be a stand-alone transaction.

  const transactions = [
    buildMemoTransaction(keypair, "jito test 1", blockHash.blockhash),
    // Include the tip instruction in the second transactions
    buildMemoTransaction(keypair, "jito test 2", blockHash.blockhash, tipIx),
  ];
Enter fullscreen mode Exit fullscreen mode

but we can also have it as a stand-alone transaction like this

  const tipTx = new VersionedTransaction(
    new TransactionMessage({
      payerKey: keypair.publicKey,
      recentBlockhash: blockHash.blockhash,
      instructions: [tipIx],
    }).compileToV0Message(),
  );
  tipTx.sign([keypair]);

  const transactions = [
    buildMemoTransaction(keypair, "jito test 1", blockHash.blockhash),
    buildMemoTransaction(keypair, "jito test 2", blockHash.blockhash),
  ];
Enter fullscreen mode Exit fullscreen mode

With everything in place, we can now build the bundle using the transactions and submit it to Jito.

  const jitoBundle = new bundle.Bundle(
    [...transactions, tipTx],
    bundleTransactionLimit,
  );

  try {
    const resp = await searcherClient.sendBundle(jitoBundle);
    console.log("resp:", resp);
  } catch (e) {
    console.error("error sending bundle:", e);
  }
Enter fullscreen mode Exit fullscreen mode

here is the full file from the example src/embedded-tip-ix.ts link

Now if you run that file, you should see the transaction signatures and the bundle Id, you can check the transactions in any explorer and you can check the bundle id in Jito Explorer (it might take a minute before showing up there)

txn signature is:  5YQVsedCaaf1bCbTUJgd23vNfte2doB1K3CB6tKxCH7KYY7Y8rDwbmetCcgBufhz8nY1nWDeCRNqhUkWNydnsjeZ
txn signature is:  3mnhGK2X2FVnsada8YL46TYdZc7BfR14GdYTjJWk8mrG2WzBJGXdyN7aLVNP3ZkWGwUBpGypjW7JWFeYGKNRa2vR
resp: 09d2c693a232d48781f69d786276b8af04be9138b0777d313b18251271825b3c
Enter fullscreen mode Exit fullscreen mode

Congrats, you have submitted a bundle

Conclusion

Jito Bundles provide a powerful solution for handling Solana’s transaction size limitations, ensuring seamless and atomic execution of complex operations. By leveraging tips and smart transaction bundling, developers can optimize the user experience and make the most of Solana’s capabilities. With this guide and the provided example, you now have the tools to integrate Jito Bundles into your Solana projects effectively. Try it out and explore the possibilities!

Top comments (0)