Skip to content

Commit 235c747

Browse files
authored
Update instruction plans documentation guide (#1310)
1 parent 608663c commit 235c747

File tree

7 files changed

+1322
-1364
lines changed

7 files changed

+1322
-1364
lines changed

docs/content/docs/concepts/instruction-plans.mdx

Lines changed: 142 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ const instructionPlan = getReallocMessagePackerInstructionPlan({
177177
Whilst these helpers are fairly situational, you can create any custom message packer as long as you implement the following interfaces.
178178

179179
```ts twoslash
180-
import { BaseTransactionMessage, TransactionMessageWithFeePayer } from '@solana/kit';
180+
import { TransactionMessage, TransactionMessageWithFeePayer } from '@solana/kit';
181181
// ---cut-before---
182182
type MessagePackerInstructionPlan = {
183183
getMessagePacker: () => MessagePacker;
@@ -187,8 +187,8 @@ type MessagePackerInstructionPlan = {
187187
type MessagePacker = {
188188
done: () => boolean;
189189
packMessageToCapacity: (
190-
transactionMessage: BaseTransactionMessage & TransactionMessageWithFeePayer,
191-
) => BaseTransactionMessage & TransactionMessageWithFeePayer;
190+
transactionMessage: TransactionMessage & TransactionMessageWithFeePayer,
191+
) => TransactionMessage & TransactionMessageWithFeePayer;
192192
};
193193
```
194194

@@ -326,12 +326,12 @@ import {
326326
singleTransactionPlan,
327327
sequentialTransactionPlan,
328328
parallelTransactionPlan,
329-
BaseTransactionMessage,
329+
TransactionMessage,
330330
TransactionMessageWithFeePayer,
331331
} from '@solana/kit';
332-
const messageA = {} as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer;
333-
const messageB = {} as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer;
334-
const messageC = {} as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer;
332+
const messageA = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer;
333+
const messageB = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer;
334+
const messageC = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer;
335335
// ---cut-before---
336336
const transactionPlan = parallelTransactionPlan([
337337
sequentialTransactionPlan([singleTransactionPlan(messageA), singleTransactionPlan(messageB)]),
@@ -373,7 +373,13 @@ const transactionPlanResult = await transactionPlanExecutor(transactionPlan, { a
373373

374374
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.
375375

376-
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.
376+
The `executeTransactionMessage` callback receives the following arguments:
377+
378+
- **`context`**: A mutable object for storing data during execution — see the [next section](#the-execution-context) for details.
379+
- **`message`**: The transaction message to execute.
380+
- **`config`**: An optional configuration object that may include an `abortSignal`.
381+
382+
The callback must return either a `Signature` or a full `Transaction` object directly.
377383

378384
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.
379385

@@ -389,7 +395,7 @@ import {
389395
signTransactionMessageWithSigners,
390396
assertIsSendableTransaction,
391397
assertIsTransactionWithBlockhashLifetime,
392-
BaseTransactionMessage,
398+
TransactionMessage,
393399
TransactionMessageWithFeePayer,
394400
} from '@solana/kit';
395401
const rpc = {} as unknown as Rpc<SolanaRpcApi>;
@@ -398,23 +404,109 @@ const rpcSubscriptions = {} as unknown as RpcSubscriptions<SolanaRpcSubscription
398404
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
399405

400406
const transactionPlanExecutor = createTransactionPlanExecutor({
401-
executeTransactionMessage: async (
402-
message: BaseTransactionMessage & TransactionMessageWithFeePayer,
403-
) => {
407+
executeTransactionMessage: async (context, message) => {
408+
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
409+
const messageWithBlockhash = setTransactionMessageLifetimeUsingBlockhash(
410+
latestBlockhash,
411+
message,
412+
);
413+
context.message = messageWithBlockhash;
414+
const transaction = await signTransactionMessageWithSigners(messageWithBlockhash);
415+
context.transaction = transaction;
416+
assertIsSendableTransaction(transaction);
417+
assertIsTransactionWithBlockhashLifetime(transaction);
418+
await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' });
419+
return transaction;
420+
},
421+
});
422+
```
423+
424+
### The execution context
425+
426+
The `context` object passed to the `executeTransactionMessage` callback is a mutable object that you can populate incrementally as execution progresses. This context is preserved in the resulting `SingleTransactionPlanResult` regardless of the outcome — successful, failed, or canceled.
427+
428+
This is particularly useful for debugging failures or building recovery plans. If an error is thrown at any point in the callback, any attributes you've already saved to the context will still be available in the `FailedSingleTransactionPlanResult`.
429+
430+
The context object supports three optional base properties that have semantic meaning:
431+
432+
- **`message`**: The transaction message after any modifications (e.g., after setting a lifetime).
433+
- **`transaction`**: The signed transaction ready to be sent.
434+
- **`signature`**: The transaction signature. Note that this is automatically populated when you set the `transaction` property on the context and/or return a `Transaction`.
435+
436+
You can also add any custom properties you need:
437+
438+
```ts twoslash
439+
import {
440+
createTransactionPlanExecutor,
441+
setTransactionMessageLifetimeUsingBlockhash,
442+
signTransactionMessageWithSigners,
443+
sendAndConfirmTransactionFactory,
444+
assertIsSendableTransaction,
445+
assertIsTransactionWithBlockhashLifetime,
446+
Rpc,
447+
SolanaRpcApi,
448+
RpcSubscriptions,
449+
SolanaRpcSubscriptionsApi,
450+
} from '@solana/kit';
451+
const rpc = {} as unknown as Rpc<SolanaRpcApi>;
452+
const rpcSubscriptions = {} as unknown as RpcSubscriptions<SolanaRpcSubscriptionsApi>;
453+
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
454+
// ---cut-before---
455+
const transactionPlanExecutor = createTransactionPlanExecutor({
456+
executeTransactionMessage: async (context, message) => {
457+
// Store the start time (custom context).
458+
context.startedAt = Date.now();
459+
404460
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
405461
const messageWithBlockhash = setTransactionMessageLifetimeUsingBlockhash(
406462
latestBlockhash,
407463
message,
408464
);
465+
466+
// Store the message about to be signed.
467+
context.message = messageWithBlockhash;
468+
409469
const transaction = await signTransactionMessageWithSigners(messageWithBlockhash);
470+
471+
// Store the transaction about to be sent.
472+
context.transaction = transaction;
473+
410474
assertIsSendableTransaction(transaction);
411475
assertIsTransactionWithBlockhashLifetime(transaction);
412476
await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' });
413-
return { transaction };
477+
478+
// Store the confirmation time (custom context).
479+
context.confirmedAt = Date.now();
480+
481+
return transaction;
414482
},
415483
});
416484
```
417485

486+
When accessing the context from a result, you can retrieve both the base properties and your custom properties:
487+
488+
```ts twoslash
489+
import {
490+
SingleTransactionPlanResult,
491+
isSuccessfulSingleTransactionPlanResult,
492+
isFailedSingleTransactionPlanResult,
493+
} from '@solana/kit';
494+
const result = {} as unknown as SingleTransactionPlanResult<{ startedAt: number }>;
495+
// ---cut-before---
496+
if (isSuccessfulSingleTransactionPlanResult(result)) {
497+
console.log(result.context.signature); // Always available for successful results.
498+
console.log(result.context.transaction); // Available if you stored it.
499+
console.log(result.context.startedAt); // Custom context property.
500+
}
501+
502+
if (isFailedSingleTransactionPlanResult(result)) {
503+
console.log(result.error); // The error that caused the failure.
504+
console.log(result.context.message); // Available if stored before the failure.
505+
console.log(result.context.transaction); // Available if stored before the failure.
506+
console.log(result.context.startedAt); // Custom context property.
507+
}
508+
```
509+
418510
Check out the [Recipes section](#recipes) at this end of this guide for ideas of what can be achieved with this API.
419511

420512
### Transaction plan results
@@ -423,30 +515,34 @@ When you execute a transaction plan, you get back a `TransactionPlanResult` that
423515

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

426-
- **Successful** - The transaction was sent and confirmed. You get the original transaction message, the executed `Transaction` object and any context data.
427-
- **Failed** - The transaction encountered an error. You get the original transaction message and the error that caused the failure.
428-
- **Canceled** - The transaction was skipped because an earlier transaction failed or the operation was aborted. You only get the original transaction message.
518+
- **Successful** - The transaction was sent and confirmed. You get the original planned message, a context object containing the signature (and optionally the transaction), plus any custom context data.
519+
- **Failed** - The transaction encountered an error. You get the original planned message, the error that caused the failure, and a context object with any data accumulated before the failure.
520+
- **Canceled** - The transaction was skipped because an earlier transaction failed or the operation was aborted. You get the original planned message with any context data accumulated before cancellation.
429521

430522
The result structure mirrors your transaction plan structure:
431523

432-
- Single transaction messages become `SingleTransactionPlanResult` with the original message plus execution status
524+
- Single transaction messages become `SingleTransactionPlanResult` with the original `plannedMessage` plus execution status
433525
- Sequential plans become `SequentialTransactionPlanResult` containing child results
434526
- Parallel plans become `ParallelTransactionPlanResult` containing child results
435527

528+
Each `SingleTransactionPlanResult` has a `status` property that is a string literal (`'successful'`, `'failed'`, or `'canceled'`), and properties like `context`, `error` live at the top level of each variant.
529+
436530
```ts twoslash
437531
import {
438532
parallelTransactionPlan,
439533
singleTransactionPlan,
440534
parallelTransactionPlanResult,
441-
successfulSingleTransactionPlanResult,
535+
successfulSingleTransactionPlanResultFromTransaction,
442536
failedSingleTransactionPlanResult,
537+
isSuccessfulSingleTransactionPlanResult,
538+
isFailedSingleTransactionPlanResult,
443539
SolanaError,
444-
BaseTransactionMessage,
540+
TransactionMessage,
445541
TransactionMessageWithFeePayer,
446542
Transaction,
447543
} from '@solana/kit';
448-
const messageA = {} as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer;
449-
const messageB = {} as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer;
544+
const messageA = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer;
545+
const messageB = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer;
450546
const transactionA = {} as unknown as Transaction;
451547
const error = {} as unknown as SolanaError;
452548
// ---cut-before---
@@ -458,9 +554,22 @@ const plan = parallelTransactionPlan([
458554

459555
// Your result may look like this:
460556
const result = parallelTransactionPlanResult([
461-
successfulSingleTransactionPlanResult(messageA, transactionA),
557+
successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA),
462558
failedSingleTransactionPlanResult(messageB, error),
463559
]);
560+
561+
// Access the signature from a successful result:
562+
if (isSuccessfulSingleTransactionPlanResult(result.plans[0])) {
563+
console.log(result.plans[0].context.signature);
564+
console.log(result.plans[0].context.transaction); // If available
565+
}
566+
567+
// Access the error from a failed result:
568+
if (isFailedSingleTransactionPlanResult(result.plans[1])) {
569+
console.log(result.plans[1].error);
570+
console.log(result.plans[1].context.signature); // If available
571+
console.log(result.plans[1].context.transaction); // If available
572+
}
464573
```
465574

466575
### Failed transaction executions
@@ -582,7 +691,7 @@ import {
582691
signTransactionMessageWithSigners,
583692
assertIsSendableTransaction,
584693
assertIsTransactionWithBlockhashLifetime,
585-
BaseTransactionMessage,
694+
TransactionMessage,
586695
TransactionMessageWithFeePayer,
587696
} from '@solana/kit';
588697
const rpc = {} as unknown as Rpc<SolanaRpcApi>;
@@ -600,20 +709,21 @@ const estimateCULimit = estimateComputeUnitLimitFactory({ rpc });
600709
const estimateAndSetCULimit = estimateAndUpdateProvisoryComputeUnitLimitFactory(estimateCULimit);
601710

602711
const transactionPlanExecutor = createTransactionPlanExecutor({
603-
executeTransactionMessage: async (
604-
message: BaseTransactionMessage & TransactionMessageWithFeePayer,
605-
) => {
712+
executeTransactionMessage: async (context, message) => {
606713
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
607714
const messageWithBlockhash = setTransactionMessageLifetimeUsingBlockhash(
608715
latestBlockhash,
609716
message,
610717
);
718+
context.message = messageWithBlockhash;
611719
const estimatedMessage = await estimateAndSetCULimit(messageWithBlockhash); // [!code ++]
720+
context.message = estimatedMessage; // [!code ++]
612721
const transaction = await signTransactionMessageWithSigners(estimatedMessage);
722+
context.transaction = transaction;
613723
assertIsSendableTransaction(transaction);
614724
assertIsTransactionWithBlockhashLifetime(transaction);
615725
await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' });
616-
return { transaction };
726+
return transaction;
617727
},
618728
});
619729
```
@@ -667,7 +777,7 @@ import {
667777
SolanaRpcSubscriptionsApi,
668778
signTransactionMessageWithSigners,
669779
assertIsSendableTransaction,
670-
BaseTransactionMessage,
780+
TransactionMessage,
671781
TransactionMessageWithFeePayer,
672782
assertIsTransactionWithDurableNonceLifetime,
673783
} from '@solana/kit';
@@ -687,15 +797,15 @@ const sendAndConfirmDurableNonceTransaction = sendAndConfirmDurableNonceTransact
687797
});
688798

689799
const transactionPlanExecutor = createTransactionPlanExecutor({
690-
executeTransactionMessage: async (
691-
message: BaseTransactionMessage & TransactionMessageWithFeePayer,
692-
) => {
800+
executeTransactionMessage: async (context, message) => {
693801
assertIsTransactionMessageWithDurableNonceLifetime(message); // [!code ++]
802+
context.message = message;
694803
const transaction = await signTransactionMessageWithSigners(message);
804+
context.transaction = transaction;
695805
assertIsSendableTransaction(transaction);
696806
assertIsTransactionWithDurableNonceLifetime(transaction);
697807
await sendAndConfirmDurableNonceTransaction(transaction, { commitment: 'confirmed' }); // [!code ++]
698-
return { transaction };
808+
return transaction;
699809
},
700810
});
701811
```

0 commit comments

Comments
 (0)