Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-chairs-throw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/instruction-plans': minor
---

Add a new function `appendTransactionMessageInstructionPlan` that can be used to add the instructions from an instruction plan to a transaction message
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Address } from '@solana/addresses';
import { isSolanaError, SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN } from '@solana/errors';
import { pipe } from '@solana/functional';
import { Instruction } from '@solana/instructions';
import {
appendTransactionMessageInstruction,
createTransactionMessage,
setTransactionMessageFeePayer,
} from '@solana/transaction-messages';

import { appendTransactionMessageInstructionPlan } from '../append-instruction-plan';
import {
getMessagePackerInstructionPlanFromInstructions,
parallelInstructionPlan,
sequentialInstructionPlan,
singleInstructionPlan,
} from '../instruction-plan';

function createInstruction<TId extends string>(id: TId): Instruction & { id: TId } {
return { id, programAddress: '1'.repeat(32) as Address };
}

const feePayer = '2'.repeat(44) as Address;

describe('appendTransactionMessageInstructionPlan', () => {
it('appends a single instruction plan to a transaction message', () => {
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const instructionA = createInstruction('A');
const plan = singleInstructionPlan(instructionA);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([instructionA]);
});

it('appends instructions from a sequential instruction plan in order', () => {
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const instructionA = createInstruction('A');
const instructionB = createInstruction('B');
const instructionC = createInstruction('C');
const plan = sequentialInstructionPlan([instructionA, instructionB, instructionC]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([instructionA, instructionB, instructionC]);
});

it('appends instructions from a parallel instruction plan', () => {
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const instructionA = createInstruction('A');
const instructionB = createInstruction('B');
const plan = parallelInstructionPlan([instructionA, instructionB]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([instructionA, instructionB]);
});

it('appends instructions from nested plans', () => {
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const instructionA = createInstruction('A');
const instructionB = createInstruction('B');
const instructionC = createInstruction('C');
const instructionD = createInstruction('D');
const plan = sequentialInstructionPlan([
parallelInstructionPlan([instructionA, instructionB]),
parallelInstructionPlan([instructionC, instructionD]),
]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([instructionA, instructionB, instructionC, instructionD]);
});

it('returns the original message for an empty sequential plan', () => {
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const plan = sequentialInstructionPlan([]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([]);
});

it('returns the original message for an empty parallel plan', () => {
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const plan = parallelInstructionPlan([]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([]);
});

it('preserves existing instructions when appending', () => {
const existingInstruction = createInstruction('existing');
const message = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageFeePayer(feePayer, m),
m => appendTransactionMessageInstruction(existingInstruction, m),
);
const instructionA = createInstruction('A');
const instructionB = createInstruction('B');
const plan = sequentialInstructionPlan([instructionA, instructionB]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([existingInstruction, instructionA, instructionB]);
});

it('appends instructions from a message packer instruction plan', () => {
Comment thread
lorisleiva marked this conversation as resolved.
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const instructionA = createInstruction('A');
const instructionB = createInstruction('B');
const plan = getMessagePackerInstructionPlanFromInstructions([instructionA, instructionB]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([instructionA, instructionB]);
});

it('appends instructions from a plan containing both single and message packer plans', () => {
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const instructionA = createInstruction('A');
const instructionB = createInstruction('B');
const instructionC = createInstruction('C');
const plan = sequentialInstructionPlan([
instructionA,
getMessagePackerInstructionPlanFromInstructions([instructionB, instructionC]),
]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([instructionA, instructionB, instructionC]);
});

it('preserves existing instructions when appending a message packer plan', () => {
const existingInstruction = createInstruction('existing');
const message = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageFeePayer(feePayer, m),
m => appendTransactionMessageInstruction(existingInstruction, m),
);
const instructionA = createInstruction('A');
const instructionB = createInstruction('B');
const plan = getMessagePackerInstructionPlanFromInstructions([instructionA, instructionB]);

const result = appendTransactionMessageInstructionPlan(plan, message);

expect(result.instructions).toStrictEqual([existingInstruction, instructionA, instructionB]);
});

it('throws if the message packer plan cannot fit all instructions', () => {
expect.assertions(1);
const message = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayer(feePayer, m));
const instructionA = createInstruction('A');
const instructionB = createInstruction('B');
const largeInstruction = { ...createInstruction('C'), data: new Uint8Array(50_000) }; // Simulate a large instruction
const plan = getMessagePackerInstructionPlanFromInstructions([instructionA, instructionB, largeInstruction]);

try {
appendTransactionMessageInstructionPlan(plan, message);
} catch (error) {
// eslint-disable-next-line jest/no-conditional-expect
expect(isSolanaError(error, SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN)).toBe(true);
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Instruction } from '@solana/instructions';
import {
TransactionMessage,
TransactionMessageWithFeePayer,
TransactionMessageWithinSizeLimit,
} from '@solana/transaction-messages';

import { appendTransactionMessageInstructionPlan } from '../append-instruction-plan';
import { InstructionPlan } from '../instruction-plan';

// [DESCRIBE] appendTransactionMessageInstructionPlan
{
// It returns the same TransactionMessage type
{
const message = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & { some: 1 };
const newMessage = appendTransactionMessageInstructionPlan(null as unknown as InstructionPlan, message);
newMessage satisfies TransactionMessage & TransactionMessageWithFeePayer & { some: 1 };
}

// It maintains the existing instruction types
{
type InstructionA = Instruction & { identifier: 'A' };

const message = null as unknown as {
feePayer: TransactionMessageWithFeePayer['feePayer'];
instructions: [InstructionA];
version: 0;
};
const newMessage = appendTransactionMessageInstructionPlan(null as unknown as InstructionPlan, message);
newMessage.instructions[0] satisfies InstructionA;
}

// It removes the size limit type safety.
{
const message = null as unknown as TransactionMessage &
TransactionMessageWithFeePayer &
TransactionMessageWithinSizeLimit;
const newMessage = appendTransactionMessageInstructionPlan(null as unknown as InstructionPlan, message);
// @ts-expect-error Potentially no longer within size limit.
newMessage satisfies TransactionMessageWithinSizeLimit;
}
}
90 changes: 90 additions & 0 deletions packages/instruction-plans/src/append-instruction-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND, SolanaError } from '@solana/errors';
import { type Instruction } from '@solana/instructions';
import {
appendTransactionMessageInstruction,
appendTransactionMessageInstructions,
TransactionMessage,
TransactionMessageWithFeePayer,
} from '@solana/transaction-messages';

import { flattenInstructionPlan, InstructionPlan } from './instruction-plan';

/**
* A helper type to append instructions to a transaction message
* without losing type information about the current instructions.
*/

type AppendTransactionMessageInstructions<TTransactionMessage extends TransactionMessage> = ReturnType<
typeof appendTransactionMessageInstructions<TTransactionMessage, Instruction[]>
>;

/**
* Appends all instructions from an instruction plan to a transaction message.
*
* This function flattens the instruction plan into its leaf plans and sequentially
* appends each instruction to the provided transaction message. It handles both
* single instructions and message packer plans.
*
* Note that any {@link MessagePackerInstructionPlan} is assumed to only append
* instructions. If it modifies other properties of the transaction message, the
* type of the returned transaction message may not accurately reflect those changes.
*
* @typeParam TTransactionMessage - The type of transaction message being modified.
*
* @param transactionMessage - The transaction message to append instructions to.
* @param instructionPlan - The instruction plan containing the instructions to append.
* @returns The transaction message with all instructions from the plan appended.
*
* @example
* Appending a simple instruction plan to a transaction message.
* ```ts
* import { appendTransactionMessageInstructionPlan } from '@solana/instruction-plans';
* import { createTransactionMessage, setTransactionMessageFeePayer } from '@solana/transaction-messages';
*
* const message = setTransactionMessageFeePayer(feePayer, createTransactionMessage({ version: 0 }));
* const plan = singleInstructionPlan(myInstruction);
*
* const messageWithInstructions = appendTransactionMessageInstructionPlan(message, plan);
* ```
*
* @example
* Appending a sequential instruction plan.
* ```ts
* const plan = sequentialInstructionPlan([instructionA, instructionB, instructionC]);
* const messageWithInstructions = appendTransactionMessageInstructionPlan(message, plan);
* ```
*
* @see {@link InstructionPlan}
* @see {@link flattenInstructionPlan}
*/
export function appendTransactionMessageInstructionPlan<
TTransactionMessage extends TransactionMessage & TransactionMessageWithFeePayer,
>(
instructionPlan: InstructionPlan,
transactionMessage: TTransactionMessage,
): AppendTransactionMessageInstructions<TTransactionMessage> {
type Out = AppendTransactionMessageInstructions<TTransactionMessage>;

const leafInstructionPlans = flattenInstructionPlan(instructionPlan);

return leafInstructionPlans.reduce(
(messageSoFar, plan) => {
const kind = plan.kind;
if (kind === 'single') {
return appendTransactionMessageInstruction(plan.instruction, messageSoFar) as unknown as Out;
}
if (kind === 'messagePacker') {
const messagerPacker = plan.getMessagePacker();
let nextMessage: Out = messageSoFar;
while (!messagerPacker.done()) {
nextMessage = messagerPacker.packMessageToCapacity(nextMessage) as Out;
}
return nextMessage;
}
throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND, {
kind,
});
},
transactionMessage as unknown as Out,
);
}
Loading