Kit

Build a transaction

Construct a new transaction message and sign it

An immutable affair

Before we start building our transaction, let's take a moment to discuss immutability in TypeScript. This is relevant for understanding how we can build transaction messages in a functional way.

Consider the following mutable variable:

let  = { : "Alice" };

As you can see by hovering on top of the person variable, TypeScript assigns the type { name: string } to it. Now imagine we wanted to gradually grow this type by adding more properties to it. Say we wanted to add an age and a wallet property to it by using the following functions:

import {  } from "@solana/kit";
 
const  = < extends object>(: number, : ) => ({ ...,  });
const  = < extends object>(: , : ) => ({ ...,  });

One way to tackle this is to mutate the variable at each step like so.

import { ,  } from "@solana/kit";
 
let  = { : "Alice" };
 = (30, );
 = (("1234..5678"), );
 
 satisfies { : string; : number; :  };
Type '{ name: string; }' does not satisfy the expected type '{ name: string; age: number; wallet: Address; }'. Type '{ name: string; }' is missing the following properties from type '{ name: string; age: number; wallet: Address; }': age, wallet

But as you can see, even after updating our variable, TypeScript still believes the person variable is of type { name: string }. This is because TypeScript doesn't re-assign types to variables when they are mutated.

Fortunately, the Kit library offers a functional API which means its types are designed to be immutable. So, instead of trying to grow the person variable by mutating it, it creates new variables with new types at each step.

import { ,  } from "@solana/kit";
 
const  = { : "Alice" };
const  = (30, );
const  = (("1234..5678"), );
 
 satisfies { : string; : number; :  };

The downside is we now need to create a new variable at each step. This is where the pipe function comes in. It takes a starting value and runs it through a series of functions, one after the other, returning the final result. This lets us build up the value and its type without needing extra variables.

import { , ,  } from "@solana/kit";
 
const  = (
  { : "Alice" },
  () => (30, ),
  () => (("1234..5678"), ),
);
 
 satisfies { : string; : number; :  };

This is exactly how we'll build our transaction in the following sections. Similarly to this person example, transaction messages can grow in a variety of ways and the ability to use composable functions to gradually construct them is a powerful tool.

With that in mind, let's start building our transaction message, starting with the initial value of our pipe.

Create the base transaction message

The first step in building a transaction is to create a new empty transaction message. This message will eventually contain all the necessary information to send a transaction to the Solana network before being compiled and signed.

To create one, we can use the createTransactionMessage function. This function requires a version parameter which is used to determine the format of the transaction. At the time of writing, 0 is the latest version available so we'll use that.

import { ,  } from "@solana/kit";
 
const  = (
  ({ : 0 }),
  // Customize the transaction message using helper functions here...
);

Set the fee payer

Next, we need to set a fee payer for our transaction. This is the wallet that will pay for the transaction to be sent to the network. For this, we can use the setTransactionMessageFeePayerSigner function which requires a TransactionSigner object. Since we already have a signer in our Client object, let's use it.

import { , ,  } from "@solana/kit";
 
const  = (
  ({ : 0 }),
  () => (., ), 
);

Set the transaction lifetime

We also need to specify a lifetime for our transaction. This lifetime defines a period of time during which the transaction is valid.

There are currently two lifetime strategies available:

  • The blockhash strategy: Given a block height — i.e. number of blocks since the genesis block — the transaction will be valid until the next block at the given height is produced. This is the most common strategy.
  • The durable nonce strategy: This strategy requires setting up a durable nonce account and advancing its value in the first instruction of the transaction. This is useful for transactions that need to be valid for long periods of time.

For this tutorial, we'll use the blockhash strategy. To do this, we can use the setTransactionMessageLifetimeUsingBlockhash function which requires an object with a recent blockhash and a block height. Fortunately, this is exactly what the getLatestBlockhash RPC method provides.

import {
  ,
  ,
  ,
  ,
} from "@solana/kit";
 
const { :  } = await ..().(); 
 
const  = (
  ({ : 0 }),
  () => (., ),
  () => (, ), 
);

Add instructions

Last but not least, we need to add our instructions to the transaction message. Since we've already built our instructions in the previous article, we can add them to our transaction message using the appendTransactionMessageInstructions function like so.

import {
  ,
  ,
  ,
  ,
  ,
} from "@solana/kit";
 
const { :  } = await ..().();
 
const  = (
  ({ : 0 }),
  () => (., ),
  () => (, ),
  () => ([, ], ), 
);

Set the compute limit dynamically

As mentioned in the previous article, setting a compute limit on a transaction as close as possible to the actual cost of running it will have many benefits, such as: increasing the likelihood of the transaction being scheduled, increasing the number of transactions that can be produced in a block and reducing the price of your priority fees, if any.

A good way to estimate how much compute unit a transaction will need is to simulate it and measure its cost. This can be done using the getComputeUnitEstimateForTransactionMessageFactory function from @solana/kit to get the estimated compute limit before setting it on the transaction message using the getSetComputeUnitLimitInstruction function from @solana-program/compute-budget. Note that this must be done after the transaction message is fully configured to ensure the estimate is as accurate as possible.

import { ,  } from "@solana/kit";
import {  } from "@solana-program/compute-budget";
 
// Build the function to estimate compute units.
const  = ({ : . });
 
// Estimate compute units.
const  = await ();
 
// Prepend the compute unit limit instruction to the transaction message.
const  = (
  ({ :  }),
  ,
);

Since this is a common operation, let's add a helper function to our Client type so we can reuse it any time we need to set a compute unit limit. First, let's update the Client type so it offers an estimateAndSetComputeUnitLimit function like so.

src/client.ts
import {
  , 
  // ...
} from "@solana/kit";
 
export type  = {
  : < extends >(: ) => <>; 
  : <>;
  : <>;
  :  & ;
};

Next, we'll implement this function in the createClient method by adding the following code:

src/client.ts
import {
  , 
  , 
  // ...
} from "@solana/kit";
import {  } from "@solana-program/compute-budget"; 
 
let :  | undefined;
export async function (): <> {
  if (!) {
    // ...
 
    // Create a function to estimate and set the compute unit limit.
    const  = ({  }); 
    const  = async < extends >(: ) => {
      const  = await (); 
      return (
        ({ :  }), 
        , 
      ); 
    }; 
 
    // Store the client.
     = { , , ,  };
  }
  return ;
}

Finally, we can now use this new estimateAndSetComputeUnitLimit helper at the end of our pipe call to set the compute unit limit on our transaction message. Note that, since estimateAndSetComputeUnitLimit is asynchronous, we now need to await on the return value of our pipe.

import {
  ,
  ,
  ,
  ,
  ,
} from "@solana/kit";
 
const { :  } = await ..().();
 
const  = await (
  ({ : 0 }),
  () => (., ),
  () => (, ),
  () => ([, ], ),
  () => .(), 
);

Sign the transaction message

Our transaction message is now fully configured and ready to be signed. Since we have been providing signer objects every step of the way, our transaction message already knows how to sign itself. All that is left to do is call signTransactionMessageWithSigners. This helper function will extract and deduplicate all the signers from the transaction message and use them to sign the message. As the message is signed, it is compiled into a new Transaction type that contains the compiled message and all of its signatures.

import {  } from "@solana/kit";
 
const  = await ();

The createMint helper

And we finally have our fully signed transaction! In the next article, we'll learn how to send it and wait for its confirmation but before we do, here's our updated createMint function including everything we've learned so far.

src/create-mint.ts
import {
  ,
  ,
  ,
  ,
  ,
  ,
  ,
} from "@solana/kit";
import {  } from "@solana-program/system";
import { , ,  } from "@solana-program/token";
 
import type {  } from "./client";
 
export async function (: , : { ?: number } = {}) {
  // Prepare inputs.
  const  = ();
  const [, , { :  }] = await .([
    (),
    ..(()).(),
    ..().(),
  ]);
 
  // Build instructions.
  const  = ({
    : .,
    : ,
    : ,
    : ,
    : ,
  });
  const  = ({
    : .,
    : . ?? 0,
    : ..,
    : ..,
  });
 
  // Build the transaction message.
  const  = await (
    ({ : 0 }),
    () => (., ),
    () => (, ),
    () => ([, ], ),
    () => .(),
  );
 
  // Compile the transaction message and sign it.
  const  = await ();
 
  // Send the transaction and wait for confirmation.
  // TODO: Next article!
}

On this page