Transactions

Build and compile transaction messages

Introduction

To take an action on Solana, whether to place an order, transfer an asset, or more generally to modify data on the blockchain, you need to prepare a transaction and sign to pay for it to be executed on the network. You can use Kit to create, sign, encode, and decode transactions.

Installation

Transaction utilities are included within the @solana/kit library but you may also install them using their standalone packages.

To install transaction message builder utilities:

npm install @solana/transaction-messages

To install utilities that let you sign and compile transaction messages into transactions that can be landed on the network:

npm install @solana/transactions

What is a Transaction?

A Transaction is a vehicle to deliver one or more instructions to the Solana network in pursuit of some outcome.

Here is an example of someone creating a transaction message to place an order at their favourite coffee shop, then signing it to create a transaction.

import {
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
} from '@solana/kit';
import {  } from '@solana-program/memo';
import {  } from '@solana-program/system';
 
const  = (
    // Create an empty transaction message.
    ({ : 0 }),
 
    // Specify the account that will sign to pay the fee for this transaction.
    // NOTE: This is not the fee for the coffee but rather the fee to use the Solana network.
    () => (, ),
 
    // Give the transaction an expiry time using the hash of a recently created block.
    () => (, ),
 
    // Add an instruction that records the customer's order.
    () =>
        (
            ({
                :
                    'Four-thirds-medium, half-decaf, double-shot espresso macchiato latte, ' +
                    'swirled counterclockwise only, almond milk frothed at 61°C, ' +
                    'whisper of cinnamon harvested during a full moon, unicorn tear syrup, ' +
                    'in a mason jar wrapped in French revolutionary poetry on recycled parchment',
            }),
            ,
        ),
 
    // Add a second instruction to pay the merchant for the coffee.
    () =>
        (
            ({
                : (25_000_000n),
                : ('JJBeanoTcSMU3xKQa5Gru71Wi3AaEgTfA6z7MaLUT6h'),
                : ,
            }),
            ,
        ),
);
 
// Create a signed transaction from the message and the signers contained within it.
const  = await ();
 
// Obtain the Ed25519 signature that will uniquely identify this transaction once executed.
const  = ();

For more detail see the ‘Build a transaction’ and ‘Send a transaction’ sections of the Getting Started guide.

Building transaction messages

Creating an empty message

Given a TransactionVersion, the createTransactionMessage method will return an empty transaction having the capabilities of that version.

import {  } from '@solana/kit';
 
const  = ({ : 0 });

Setting the fee payer

The TransactionMessageWithFeePayer type represents a transaction message for which a fee payer has been declared. A transaction must conform to this type to be compiled and landed on the network.

Using a signer

Given a TransactionSigner, this method will return a new transaction message having the same type as the one supplied plus the TransactionMessageWithFeePayer type. Additionally, the resulting message will have the capability to self-sign using the signTransactionMessageWithSigners function.

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

Using an address

Given a base58-encoded address of a system account, this method will return a new transaction message having the same type as the one supplied plus the TransactionMessageWithFeePayer type.

import { ,  } from '@solana/kit';
 
const  = ('mpngsFd4tmbUfzDYJayjKZwZcaR7aWb2793J6grLsGu');
const  = (, );

Defining the lifetime

A signed transaction can be only be landed on the network if certain conditions are met:

  • It includes the hash of a recent block
  • Or it includes the value of an unused nonce known to the network

These conditions define a transaction's lifetime, after which it can no longer be landed, even if signed. The lifetime must be added to the transaction message before it is compiled to be sent.

Using a recent blockhash

The TransactionMessageWithBlockhashLifetime type represents a transaction message whose expiry is tied to the age of a block. Such a transaction can only be landed on the network if the current block height of the network is less than or equal to the value of TransactionMessageWithBlockhashLifetime.lifetimeConstraint.lastValidBlockHeight.

Given a blockhash and the last block height at which that blockhash is considered usable to land transactions, the setTransactionMessageLifetimeUsingBlockhash method will return a new transaction message having the same type as the one supplied plus the TransactionMessageWithBlockhashLifetime type.

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

Using a durable nonce

The TransactionMessageWithDurableNonceLifetime type represents a transaction message whose lifetime is defined by the value of a nonce account on-chain. Such a transaction can only be landed on the network if the nonce value in the transaction message matches the one in the nonce account at the time the transaction executes.

Given a nonce, the account where the value of the nonce is stored, and the address of the account authorized to consume that nonce, this method will return a new transaction having the same type as the one supplied plus the TransactionMessageWithDurableNonceLifetime type.

import { ,  } from '@solana/kit';
import {  } from '@solana-program/system';
 
const  = ('EGtMh4yvXswwHhwVhyPxGrVV2TkLTgUqGodbATEPvojZ');
const  = ('4KD1Rdrd89NG7XbzW3xsX9Aqnx2EExJvExiNme6g9iAT');
 
const {
    : {  },
} = await (, );
const  =  as string as ;
 
const  = (
    { , ,  },
    ,
);

In particular, this method prepends an instruction to the transaction message designed to consume (or ‘advance’) the nonce in the same transaction whose lifetime is defined by it.

const [
    // An 'advance nonce' instruction gets prepended to the instruction list
    ,
    ...
] = .;

Adding instructions

There are three types that correspond to the different parts of an instruction. Any given instruction may conform to one or more of these types, but must conform in every case to the Instruction type.

  • Instruction: An instruction having a programAddress property that is the base58-encoded address of the program to invoke.
  • InstructionWithAccounts: An instruction that specifies a list of accounts that a program may read from, write to, or require be signers of the transaction itself. Objects that conform to this type have an accounts property that is an array of AccountMeta | AccountLookupMeta in the order the instruction requires.
  • InstructionWithData: An instruction that supplies some data as input to the program. Objects that conform to this type have a data property that can be any type of Uint8Array.

Given an instruction, the appendTransactionMessageInstruction method will return a new transaction message with that instruction having been added to the end of the list of existing instructions.

import { , ,  } from '@solana/kit';
import {  } from '@solana-program/memo';
 
const  = (
    ({ : 'Hello world!' }),
    ,
);

To add an instruction to the beginning of the list instead, see prependTransactionMessageInstruction

To add an array of instructions to a transaction message, see appendTransactionMessageInstructions and prependTransactionMessageInstructions.

Compressing transaction messages

Every transaction message must include a reference to the account addresses it will read from and write to. One alternative to storing these addresses in the message itself is to store them in an on-chain account called an address lookup table. This lets you save space in the message by replacing many 32-byte account addresses with one or more 32-byte address lookup table account addresses then a 1-byte index into those tables for each address.

Addresses that are required signers of a transaction message can not be looked up in an address lookup table; they must be encoded in the message in the conventional way.

Given a transaction message and a mapping of lookup tables to the ordered addresses stored in them, the compressTransactionMessageUsingAddressLookupTables function will return a new transaction message with the same instructions but with all non-signer accounts that are found in the given lookup tables represented by an AccountLookupMeta instead of an AccountMeta.

import { ,  } from '@solana/kit';
import {  } from '@solana-program/address-lookup-table';
 
const  = ('4QwSwNriKPrz8DLW4ju5uxC2TN5cksJx6tPUPj7DGLAW');
const {
    : {  },
} = await (, );
const :  = {
    []: ,
};
 
const  = (
    ,
    ,
);

Consider how compressing transaction messages creates more space for instructions. This might enable you to prepare more complex transactions, or to execute the same number of instructions over fewer transactions thereby saving on network fees.

This technique can not be applied to transaction messages having the version 'legacy'.

Signing transaction messages

In order to be executed a transaction message must be signed by all of the private keys belonging to accounts that are required signers of the transaction, and must not exceed the size allowable by the network. You may encounter these types when using functions that send transactions.

Transaction messages whose signers are specified using TransactionSigner objects have the ability to self-sign. This is because signers encapsulate both the address of the signing account as well as an implementation of the signing algorithm for the private key associated with that account.

The signTransactionMessageWithSigners method will return a new signed transaction of type FullySignedTransaction.

import {
    ,
    ,
    ,
} from '@solana/kit';
 
try {
    const  = await ();
} catch () {
    if ((, )) {
        .('Missing signers for the following addresses:', ..);
    } else {
        throw ;
    }
}

This function will throw if the transaction message does not carry a TransactionSigner implementation for every required signer. To partially sign a message that you know to carry a strict subset of the required TransactionSigners, use the partiallySignTransactionMessageWithSigners method.

If exactly one of the TransactionSigners in the message is a TransactionSendingSigner then you can use the signAndSendTransactionMessageWithSigners method to sign and send the transaction in a single step.

Building transaction messages using TransactionSigners is the recommended way to create self-signable transaction messages. To sign with a CryptoKey directly, you first have to compile the transaction message.

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

This produces an unsigned transaction. Follow the instructions for signing transactions with CryptoKeyPairs to sign it.

Signing transactions

Wherever you have a Transaction instead of a TransactionMessage you can add or replace a signature using the signTransaction method and one or more CryptoKeyPairs.

import {
    ,
    ,
    ,
} from '@solana/kit';
 
try {
    const  = await ([], );
} catch () {
    if ((, )) {
        .('Missing signers for the following addresses:', ..);
    } else {
        throw ;
    }
}

This function will throw if the resultant transaction is missing a signature for one of the transaction's required signers. To partially sign a transaction, use the partiallySignTransaction method.

Serializing transactions

If you would like to send a transaction to the network manually, you must first serialize it in a particular way. The Base64EncodedWireTransaction represents the wire format of a transaction as a base64-encoded string.

Given a transaction, the getBase64EncodedWireTransaction method returns the transaction as a string that conforms to the Base64EncodedWireTransaction type.

import { ,  } from '@solana/kit';
 
const  = ();
const  = await .(, { : 'base64' }).();

Typically you would not serialize and send transactions to the network manually. See how to use send-and-confirm helpers to make sending transactions easier and more reliable.

Deserializing transactions

You can fetch the raw bytes of a transaction from the network using an RPC server.

const  = await 
    .(
        '3atZVDiLEjXmddxLbH2AWHtFdXmHRocnA4vNyvkPRcd7WnzCtoVshFCvGtxfGYWs9C6ptucY6Jd84BerDnzQpJEH' as ,
        { : 'base64' },
    )
    .();
if (!) {
    throw new ('Could not find transaction');
}
const {
    : [],
} = ;
const  = ().();

Decoding the wire transaction bytes yields a Transaction object. This takes the form of a transaction message encoded as a byte array, and a list of signatures of those bytes created by those who must authorize the message and pay for it to be executed on Solana.

import {  } from '@solana/kit';
const  = ().();

Decoding the network wire format bytes of the message will yield a transaction message in its compiled form – a message in a form suitable for execution on the network. Encountering a compiled message in your application is rare, but it's important to know that they exist.

import {  } from '@solana/kit';
const  = ().(
    .,
);

Finally, decompiling a compiled message will yield a TransactionMessage object. This is the most common form of transaction message that you will encounter when using Kit to build an application.

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

You can not fully reconstruct a source message from a compiled message without extra information. In particular, supporting details about the lifetime constraint and the concrete addresses of accounts sourced from account lookup tables are lost to compilation, but can be supplied in the config argument of the decompileTransactionMessage or decompileTransactionMessageFetchingLookupTables methods.