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:
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:
One way to tackle this is to mutate the variable at each step like so.
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.
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.
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.
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.
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.
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.
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.
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.
Next, we'll implement this function in the createClient
method by adding the following code:
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
.
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.
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.