From 6269a07740b9eb7aaa3b01936d33f3ea0a086493 Mon Sep 17 00:00:00 2001 From: Callum Date: Tue, 27 Jan 2026 15:36:55 +0000 Subject: [PATCH 1/2] Support multiple iterations in message packer in fitEntirePlanInsideMessage --- .../src/__tests__/__setup__.ts | 28 ++++++++++++++ .../src/__tests__/transaction-planner-test.ts | 37 +++++++++++++++++++ .../src/transaction-planner.ts | 2 +- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/instruction-plans/src/__tests__/__setup__.ts b/packages/instruction-plans/src/__tests__/__setup__.ts index e717e397d..a9ca77a9e 100644 --- a/packages/instruction-plans/src/__tests__/__setup__.ts +++ b/packages/instruction-plans/src/__tests__/__setup__.ts @@ -17,6 +17,34 @@ import { MessagePackerInstructionPlan } from '../instruction-plan'; const MINIMUM_INSTRUCTION_SIZE = 35; +/** + * Creates a message packer that packs one instruction at a time, + * even when there's space to pack more in a single iteration. + * This is useful for testing that the message packer loop correctly accumulates + * results across iterations. + */ +export function createSingleInstructionAtATimeMessagePackerInstructionPlan( + instructions: Instruction[], +): MessagePackerInstructionPlan { + return Object.freeze({ + getMessagePacker: () => { + let index = 0; + return { + done: () => index >= instructions.length, + packMessageToCapacity: message => { + if (index >= instructions.length) { + return message; + } + const instruction = instructions[index]; + index++; + return appendTransactionMessageInstruction(instruction, message); + }, + }; + }, + kind: 'messagePacker', + }); +} + export const FOREVER_PROMISE = new Promise(() => { /* never resolve */ }); diff --git a/packages/instruction-plans/src/__tests__/transaction-planner-test.ts b/packages/instruction-plans/src/__tests__/transaction-planner-test.ts index 3ac2e10b9..18a7ff27c 100644 --- a/packages/instruction-plans/src/__tests__/transaction-planner-test.ts +++ b/packages/instruction-plans/src/__tests__/transaction-planner-test.ts @@ -30,6 +30,7 @@ import { createTransactionPlanner, TransactionPlanner } from '../transaction-pla import { createMessage, createMessagePackerInstructionPlan, + createSingleInstructionAtATimeMessagePackerInstructionPlan, FOREVER_PROMISE, instructionFactory, transactionPercentFactory, @@ -1437,6 +1438,42 @@ describe('createTransactionPlanner', () => { singleTransactionPlan([instructionA, messagePackerB.get(0, txPercent(50)), instructionC]), ); }); + + /** + * [Par] ─────────────────────────▶ [Tx: A + B + C] + * │ + * ├── [A: 25%] + * └── [NonDivSeq] + * └── [MessagePackerWithMultipleIterations(B: 25%, C: 25%)] + * + * This tests a case where the entire message packer must be packed into a single transaction. + * It uses a message packer that has instructions that can fit, but require multiple iterations + * to be fully added. + */ + it('accumulates all instructions when a message packer requires multiple iterations in fitEntirePlanInsideMessage', async () => { + expect.assertions(1); + const createTransactionMessage = createMockTransactionMessage; + const { instruction, singleTransactionPlan, txPercent } = getHelpers(createTransactionMessage); + const planner = createTransactionPlanner({ createTransactionMessage }); + + const instructionA = instruction('A', txPercent(25)); + const instructionB = instruction('B', txPercent(25)); + const instructionC = instruction('C', txPercent(25)); + + const multiIterPacker = createSingleInstructionAtATimeMessagePackerInstructionPlan([ + instructionB, + instructionC, + ]); + + await expect( + planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + nonDivisibleSequentialInstructionPlan([multiIterPacker]), + ]), + ), + ).resolves.toEqual(singleTransactionPlan([instructionA, instructionB, instructionC])); + }); }); describe('complex scenarios', () => { diff --git a/packages/instruction-plans/src/transaction-planner.ts b/packages/instruction-plans/src/transaction-planner.ts index 47dcbd019..0fec3dcbe 100644 --- a/packages/instruction-plans/src/transaction-planner.ts +++ b/packages/instruction-plans/src/transaction-planner.ts @@ -409,7 +409,7 @@ function fitEntirePlanInsideMessage( // eslint-disable-next-line no-case-declarations const messagePacker = instructionPlan.getMessagePacker(); while (!messagePacker.done()) { - newMessage = messagePacker.packMessageToCapacity(message); + newMessage = messagePacker.packMessageToCapacity(newMessage); } return newMessage; default: From 282b517f392c5c5558b8ba25a3d8c0ab503df24c Mon Sep 17 00:00:00 2001 From: Callum Date: Tue, 27 Jan 2026 15:38:00 +0000 Subject: [PATCH 2/2] Add changeset --- .changeset/moody-needles-laugh.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/moody-needles-laugh.md diff --git a/.changeset/moody-needles-laugh.md b/.changeset/moody-needles-laugh.md new file mode 100644 index 000000000..9cd446ccb --- /dev/null +++ b/.changeset/moody-needles-laugh.md @@ -0,0 +1,5 @@ +--- +'@solana/instruction-plans': patch +--- + +Fix a bug where a message packer that requires multiple iterations is not correctly added when forced to fit in a single transaction, even if it can fit