-
Notifications
You must be signed in to change notification settings - Fork 183
Add appendTransactionMessageInstructionPlan function #1247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
166 changes: 166 additions & 0 deletions
166
packages/instruction-plans/src/__tests__/append-instruction-plan-test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', () => { | ||
| 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); | ||
| } | ||
| }); | ||
| }); | ||
42 changes: 42 additions & 0 deletions
42
packages/instruction-plans/src/__typetests__/append-instruction-plan-typetest.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.