Instruction Plans

Compose instructions into multi-step operations

Introduction

Instruction plans describe operations that go beyond a single instruction and may even span multiple transactions.

They define a set of instructions that must be executed following a specific order. For instance, imagine we wanted to create an instruction plan for a simple escrow transfer between Alice and Bob. First, both would need to deposit their assets into a vault. This could happen in any order. Then and only then, the vault can be activated to switch the assets. Alice and Bob can now both withdraw each other's assets (again, in any order). Here's how we could describe an instruction plan for such an operation.

const  = ([
    ([, ]),
    ,
    ([, ]),
]);

As you can see, instruction plans don't concern themselves with:

  • Adding structural instructions — e.g. compute budget limits and prices.
  • Building transaction messages from these instructions. That is, planning how many can fit into a single instruction, adding a fee payer, a lifetime, etc.
  • Compiling, signing and sending transactions to the network.

Instead, they solely focus on describing operations and delegate all that to two components introduced in this package:

  • Transaction planner: builds transaction messages from an instruction plan and returns an appropriate transaction plan.
  • Transaction plan executor: compiles, signs and sends transaction plans and returns a detailed result of this operation.
// Plan instructions into transactions.
const  = await ();
 
// Execute transactions.
const  = await ();

This separation of concerns not only improves the developer experience but also allows program maintainers to offer helper functions that go beyond a single instruction, while leaving their consumers to decide how they want these operations to materialise.

Installation

Instruction plans are included within the @solana/kit library but you may also install them using their standalone package.

npm install @solana/instruction-plans

Creating instruction plans

This package offers several helpers to help you compose your own instruction plans. Let's have a look at them.

Single instructions

The most trivial way to create an instruction plan is to use the singleInstructionPlan helper to create a plan with only one instruction.

const  = ();

Sequential plans

The sequentialInstructionPlan helper allows you to create plans from other plans that must be executed sequentially. Therefore, in the example below, we guarantee that Bob will receive assets from Alice before sending them to Carla.

const  = ([
    (),
    (),
]);

Note that the sequentialInstructionPlan helper also accept Instruction objects directly and automatically wraps them in singleInstructionPlans. Therefore the following is equivalent to the previous example.

const  = ([, ]);

A nonDivisibleSequentialInstructionPlan helper is also available to define sequential plans whose inner instructions should all be executed atomically. That is, either in a single transaction or in a transaction bundle when not possible.

const  = ([, ]);

In this example, we know that both instruction will either succeed or fail together.

Parallel plans

The parallelInstructionPlan function can be used to create plans from other plans that can be executed in parallel. This means direct children of this plan can be executed in separate parallel transactions without consequence. For instance, in the example below, Alice can transfer assets to both Bob and Carla in any order without affecting the final outcome.

const  = ([
    (),
    (),
]);

The parallelInstructionPlan also accepts Instruction object directly and therefore the previous example can be simplified as:

const  = ([, ]);

Message packer plans

Message packer plans are a bit special. They can dynamically pack instructions into transaction messages. This is particularly useful when packing instructions whose size will vary based on the available space left on the transaction message being packed.

For instance, imagine a write instruction on a program that gradually write data from instructions into a buffer account. The instruction data used in this case will ideally be as long as the transaction message can fit. Using the getLinearMessagePackerInstructionPlan helper, we can create an instruction plan that does just that.

const  = ({
    : .,
    : (, ) =>
        ({
            ,
            : .(,  + ),
        }),
});

As you can see, the getLinearMessagePackerInstructionPlan helper accepts a totalLength attribute representing the total amount of bytes we eventually want to write to the buffer. The purpose of the getInstruction function is then to generate these write instructions at the provided positions.

There also exists a getReallocMessagePackerInstructionPlan helper that works similarly but whose purpose is to pack multiple realloc instructions to help resize an account.

const  = ({
    : ,
    : () => ({ :  }),
});

Whilst these helpers are fairly situational, you can create any custom message packer as long as you implement the following interfaces.

type  = {
    : () => ;
    : 'messagePacker';
};
 
type  = {
    : () => boolean;
    : (
        :  & ,
    ) =>  & ;
};

Most of your custom logic will live in the packMessageToCapacity function whose purpose is to pack the provided message with as many instruction datas as possible or throw when it isn't achievable. The done function lets us know if there is any instruction data left to pack. See MessagePacker for more information.

Combining plans

It is worth noting that more complex operations can be created by combining plans together. For instance, the following plan will:

  • Create two accounts in parallel, one Buffer account and one Metadata account.
    • The Buffer account will be written to via multiple parallel write instructions.
    • The Metadata account will be created and initialized atomically.
  • Once both of these accounts are created, the data in the Buffer account will be used to set the data in the Metadata account before being closed.
const  = ([
    ([
        ([
            ,
            ,
            ([]),
        ]),
        ([, ]),
    ]),
    ,
    ,
]);

Planning instructions

Once we have an instruction plan, the first step is to build transaction messages from it in the most optimal way whilst satisfying all the constraints defined in the instruction plan. This is the role of transaction planners.

Transaction planners

Transaction planners are defined as simple abortable functions that transform a given instruction plan into a set of built transaction messages called a transaction plan.

const  = await (, {  });

Creating a transaction planner

To spin up a transaction planner, you may use the createTransactionPlanner helper. This helper requires a createTransactionMessage function that tells us how each new transaction message should be created before being packed with instructions.

For instance, in the example below we create a new planner such that each planned transaction message will be using version 0 and using the payer signer as a fee payer.

const  = ({
    : () =>
        (
            ({ : 0 }),
            () => (, ),
            // ...
        ),
});

Additionally, the onTransactionMessageUpdated function may be provided to update transaction messages during the planning process. This function will be called whenever a transaction message is updated — e.g. when new instructions are added. It accepts the updated transaction message and must return a transaction message back, even if no changes were made.

In the example below, we check if the packed transaction contains an instruction that transfers SOL and, if so, add a guard instruction that ensures no more than 1 SOL is transfered.

const  = ({
    ,
    : () => {
        if (()) {
            return (
                ,
                ,
            ) as unknown as typeof ;
        }
 
        return ;
    },
});

Check out the Recipes section at this end of this guide for ideas of what can be achieved with this API.

Transaction plans

Since transaction planners output transaction plans, it may be useful to see what these look like.

They work similarly to instruction plans but they wrap transaction messages instead of instructions and do not contain message packers.

const  = ([
    ([(), ()]),
    (),
]);

Advanced transaction planners

Whilst the createTransactionPlanner helper is designed to suit most projects, it may not suit yours. If so, you may create your own by offering a function that satisfy the following signature.

type  = (
    : ,
    ?: { ?: AbortSignal },
) => <>;

Executing transactions

Now that we have obtained a transaction plan from our instruction plan, the next and final step is to send these transactions using a transaction plan executor.

Transaction plan executors

Transaction plan executors are defined as abortable functions that transform a given transaction plan into a mirrored data structure that contains the execution status of each transaction. That data structure is called a transaction plan result.

const  = await (, {  });

Creating a transaction plan executor

To spin up a transaction plan executor, you may use the createTransactionPlanExecutor helper. This helper requires an executeTransactionMessage function that tells us how each transaction message should be executed when encountered during the execution process.

This function accepts a transaction message and must return an object containing the successfully executed transaction, along with an optional context object that can be used to pass additional information about the execution.

For instance, in the example below we create a new executor such that each transaction message will be assigned the latest blockhash lifetime before being signed and sent to the network using the provided RPC client.

const  = ({ ,  });
 
const  = ({
    : async (
        :  & ,
    ) => {
        const { :  } = await .().();
        const  = (
            ,
            ,
        );
        const  = await ();
        ();
        await (, { : 'confirmed' });
        return {  };
    },
});

Check out the Recipes section at this end of this guide for ideas of what can be achieved with this API.

Transaction plan results

When you execute a transaction plan, you get back a TransactionPlanResult that tells you what happened during execution. This result object has the same tree structure as your original transaction plan, but includes execution information for each transaction message.

Each transaction message in your plan can have one of three execution outcomes:

  • Successful - The transaction was sent and confirmed. You get the original transaction message, the executed Transaction object and any context data.
  • Failed - The transaction encountered an error. You get the original transaction message and the error that caused the failure.
  • Canceled - The transaction was skipped because an earlier transaction failed or the operation was aborted. You only get the original transaction message.

The result structure mirrors your transaction plan structure:

  • Single transaction messages become SingleTransactionPlanResult with the original message plus execution status
  • Sequential plans become SequentialTransactionPlanResult containing child results
  • Parallel plans become ParallelTransactionPlanResult containing child results
// If your transaction plan looked like this:
const  = ([
    (),
    (),
]);
 
// Your result may look like this:
const  = ([
    (, ),
    (, ),
]);

Failed transaction executions

When a transaction plan executor — created via the createTransactionPlanExecutor helper — encounters a failed transaction, it will cancel all remaining transactions in the plan. The executor will then throw a SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN error code.

This error contains a transactionPlanResult property (accessible as a non-enumerable property) that provides detailed information about which transactions succeeded, failed, or were canceled.

try {
    const  = await ();
} catch () {
    if ((, )) {
        // Access the failed `TransactionPlanResult` to understand what happened.
        const  = .. as ;
    }
}

Advanced transaction plan executors

Whilst the createTransactionPlanExecutor helper is designed to suit most projects, it may not suit yours. If so, you may create your own by offering a function that satisfies the following signature.

type  = (
    : ,
    ?: { ?: AbortSignal },
) => <>;

This allows you to implement custom execution strategies, such as using transaction bundles for atomic execution or adding custom transaction prioritization.

Recipes

Here are some common patterns and recipes for using instruction plans effectively.

Setting priority fees

You can set priority fees by using the setTransactionMessageComputeUnitPrice helper from @solana-program/compute-budget in your createTransactionMessage function.

import {  } from '@solana-program/compute-budget'; 
 
const  = ({
    : () =>
        (
            ({ : 0 }),
            () => (, ),
            // Set priority fees to 0.01 lamports per compute unit.
            () => (10_000n, ),
        ),
});

Estimating compute units

You can estimate and set compute unit limits dynamically by using a two-step process:

  • First, add a provisional compute unit limit instruction (if missing) in your transaction planner.
  • Then, estimate and update it right before sending the transaction.

You may use the fillProvisorySetComputeUnitLimitInstruction helper in your transaction planner so the instruction is accounted for when planning transaction messages.

import {  } from '@solana-program/compute-budget'; 
 
const  = ({
    : () =>
        (
            ({ : 0 }),
            () => (, ),
            () => (), 
        ),
});

Then, use the estimateAndUpdateProvisoryComputeUnitLimitFactory helper in your transaction plan executor to estimate the compute units right before sending the transaction.

import {
    ,
    ,
} from '@solana-program/compute-budget';
 
const  = ({ ,  });
const  = ({  });
const  = ();
 
const  = ({
    : async (
        :  & ,
    ) => {
        const { :  } = await .().();
        const  = (
            ,
            ,
        );
        const  = await (); 
        const  = await ();
        ();
        await (, { : 'confirmed' });
        return {  };
    },
});

Durable nonce executor

You may create transaction plans that use durable nonces for offline transaction signing by using the setTransactionMessageLifetimeUsingDurableNonce helper in your transaction planner.

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

Then, make sure to use the sendAndConfirmDurableNonceTransactionFactory helper in your transaction plan executor in order to use the appropriate confirmation strategy for your transactions.

import {
    ,
    ,
} from '@solana/kit';
 
const  = ({
    ,
    ,
});
 
const  = ({
    : async (
        :  & ,
    ) => {
        (); 
        const  = await ();
        ();
        await (, { : 'confirmed' }); 
        return {  };
    },
});