From 4aed4e2e0310ebd6a481c1ab0191fd05e7f78b3e Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 23 Jun 2025 14:35:09 +0100 Subject: [PATCH 01/10] Add internal mutable types and refactor emit function --- .../transactionPlannerBase.ts | 144 ++++++++++-------- .../transactionPlanner.test.ts | 37 +++++ 2 files changed, 120 insertions(+), 61 deletions(-) diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index 3f65e93..fae9d15 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -18,6 +18,9 @@ import { } from './transactionHelpers'; import { getAllSingleTransactionPlans, + nonDivisibleSequentialTransactionPlan, + parallelTransactionPlan, + sequentialTransactionPlan, singleTransactionPlan, SingleTransactionPlan, TransactionPlan, @@ -44,36 +47,23 @@ export function createBaseTransactionPlanner( { abortSignal } = {} ): Promise => { const createSingleTransactionPlan: CreateSingleTransactionPlanFunction = - async (instructions = []) => { + async () => { abortSignal?.throwIfAborted(); - const emptyMessage = await Promise.resolve( - config.createTransactionMessage({ abortSignal }) - ); - if (instructions.length <= 0) { - return { kind: 'single', message: emptyMessage }; - } - const plan: SingleTransactionPlan = { + return { kind: 'single', - message: appendTransactionMessageInstructions( - instructions, - emptyMessage + message: await Promise.resolve( + config.createTransactionMessage({ abortSignal }) ), }; - return await onSingleTransactionPlanUpdated(plan); }; - const onSingleTransactionPlanUpdated: OnSingleTransactionPlanUpdatedFunction = + const emitSingleTransactionPlanUpdated: EmitSingleTransactionPlanUpdatedFunction = async (plan) => { abortSignal?.throwIfAborted(); - if (!config?.onTransactionMessageUpdated) { - return plan; - } - return { - kind: 'single', - message: await Promise.resolve( - config.onTransactionMessageUpdated(plan.message, { abortSignal }) - ), - }; + if (!config?.onTransactionMessageUpdated) return; + plan.message = await Promise.resolve( + config.onTransactionMessageUpdated(plan.message, { abortSignal }) + ); }; const plan = await traverse(originalInstructionPlan, { @@ -81,7 +71,7 @@ export function createBaseTransactionPlanner( parent: null, parentCandidates: [], createSingleTransactionPlan, - onSingleTransactionPlanUpdated, + emitSingleTransactionPlanUpdated, }); if (!plan) { @@ -97,30 +87,32 @@ export function createBaseTransactionPlanner( throw error; } - return plan; + return freezeTransactionPlan(plan); }; } -type CreateSingleTransactionPlanFunction = ( - instructions?: IInstruction[] -) => Promise; +type MutableTransactionPlan = Mutable; +type MutableSingleTransactionPlan = Mutable; -type OnSingleTransactionPlanUpdatedFunction = ( - plan: SingleTransactionPlan -) => Promise; +type CreateSingleTransactionPlanFunction = + () => Promise; + +type EmitSingleTransactionPlanUpdatedFunction = ( + plan: MutableSingleTransactionPlan +) => Promise; type TraverseContext = { abortSignal?: AbortSignal; parent: InstructionPlan | null; - parentCandidates: SingleTransactionPlan[]; + parentCandidates: MutableSingleTransactionPlan[]; createSingleTransactionPlan: CreateSingleTransactionPlanFunction; - onSingleTransactionPlanUpdated: OnSingleTransactionPlanUpdatedFunction; + emitSingleTransactionPlanUpdated: EmitSingleTransactionPlanUpdatedFunction; }; async function traverse( instructionPlan: InstructionPlan, context: TraverseContext -): Promise { +): Promise { context.abortSignal?.throwIfAborted(); switch (instructionPlan.kind) { case 'sequential': @@ -142,8 +134,8 @@ async function traverse( async function traverseSequential( instructionPlan: SequentialInstructionPlan, context: TraverseContext -): Promise { - let candidate: SingleTransactionPlan | null = null; +): Promise { + let candidate: MutableSingleTransactionPlan | null = null; const mustEntirelyFitInParentCandidate = context.parent && (context.parent.kind === 'parallel' || !instructionPlan.divisible); @@ -154,9 +146,8 @@ async function traverseSequential( parentCandidate ); if (transactionPlan) { - (parentCandidate as Mutable).message = - transactionPlan.message; - await context.onSingleTransactionPlanUpdated(parentCandidate); + parentCandidate.message = transactionPlan.message; + await context.emitSingleTransactionPlanUpdated(parentCandidate); return null; } } @@ -198,8 +189,10 @@ async function traverseSequential( async function traverseParallel( instructionPlan: ParallelInstructionPlan, context: TraverseContext -): Promise { - const candidates: SingleTransactionPlan[] = [...context.parentCandidates]; +): Promise { + const candidates: MutableSingleTransactionPlan[] = [ + ...context.parentCandidates, + ]; const transactionPlans: TransactionPlan[] = []; // Reorder children so iterable plans are last. @@ -235,22 +228,27 @@ async function traverseParallel( async function traverseSingle( instructionPlan: SingleInstructionPlan, context: TraverseContext -): Promise { +): Promise { const ix = instructionPlan.instruction; const candidate = selectCandidate(context.parentCandidates, [ix]); if (candidate) { - (candidate.message as Mutable) = - appendTransactionMessageInstructions([ix], candidate.message); - await context.onSingleTransactionPlanUpdated(candidate); + candidate.message = appendTransactionMessageInstructions( + [ix], + candidate.message + ); + await context.emitSingleTransactionPlanUpdated(candidate); return null; } - return await context.createSingleTransactionPlan([ix]); + const plan = await context.createSingleTransactionPlan(); + plan.message = appendTransactionMessageInstructions([ix], plan.message); + await context.emitSingleTransactionPlanUpdated(plan); + return plan; } async function traverseIterable( instructionPlan: IterableInstructionPlan, context: TraverseContext -): Promise { +): Promise { const iterator = instructionPlan.getIterator(); const transactionPlans: SingleTransactionPlan[] = []; const candidates = [...context.parentCandidates]; @@ -259,20 +257,24 @@ async function traverseIterable( const candidateResult = selectCandidateForIterator(candidates, iterator); if (candidateResult) { const [candidate, ix] = candidateResult; - (candidate.message as Mutable) = - appendTransactionMessageInstructions([ix], candidate.message); - await context.onSingleTransactionPlanUpdated(candidate); + candidate.message = appendTransactionMessageInstructions( + [ix], + candidate.message + ); + await context.emitSingleTransactionPlanUpdated(candidate); } else { - const newPlan = await context.createSingleTransactionPlan([]); + const newPlan = await context.createSingleTransactionPlan(); const ix = iterator.next(newPlan.message); if (!ix) { throw new Error( 'Could not fit `InterableInstructionPlan` into a transaction' ); } - (newPlan.message as Mutable) = - appendTransactionMessageInstructions([ix], newPlan.message); - await context.onSingleTransactionPlanUpdated(newPlan); + newPlan.message = appendTransactionMessageInstructions( + [ix], + newPlan.message + ); + await context.emitSingleTransactionPlanUpdated(newPlan); transactionPlans.push(newPlan); // Adding the new plan to the candidates is important for cases @@ -299,8 +301,8 @@ async function traverseIterable( } function getSequentialCandidate( - latestPlan: TransactionPlan -): SingleTransactionPlan | null { + latestPlan: MutableTransactionPlan +): MutableSingleTransactionPlan | null { if (latestPlan.kind === 'single') { return latestPlan; } @@ -314,14 +316,14 @@ function getSequentialCandidate( function getParallelCandidates( latestPlan: TransactionPlan -): SingleTransactionPlan[] { +): MutableSingleTransactionPlan[] { return getAllSingleTransactionPlans(latestPlan); } function selectCandidateForIterator( - candidates: SingleTransactionPlan[], + candidates: MutableSingleTransactionPlan[], iterator: InstructionIterator -): [SingleTransactionPlan, IInstruction] | null { +): [MutableSingleTransactionPlan, IInstruction] | null { for (const candidate of candidates) { const ix = iterator.next(candidate.message); if (ix) { @@ -332,9 +334,9 @@ function selectCandidateForIterator( } function selectCandidate( - candidates: SingleTransactionPlan[], + candidates: MutableSingleTransactionPlan[], instructions: IInstruction[] -): SingleTransactionPlan | null { +): MutableSingleTransactionPlan | null { const firstValidCandidate = candidates.find((candidate) => isValidCandidate(candidate, instructions) ); @@ -342,7 +344,7 @@ function selectCandidate( } function isValidCandidate( - candidate: SingleTransactionPlan, + candidate: MutableSingleTransactionPlan, instructions: IInstruction[] ): boolean { const message = appendTransactionMessageInstructions( @@ -358,6 +360,26 @@ export function getRemainingTransactionSize( return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); } +function freezeTransactionPlan(plan: MutableTransactionPlan): TransactionPlan { + switch (plan.kind) { + case 'single': + return singleTransactionPlan(plan.message); + case 'sequential': + return plan.divisible + ? sequentialTransactionPlan(plan.plans.map(freezeTransactionPlan)) + : nonDivisibleSequentialTransactionPlan( + plan.plans.map(freezeTransactionPlan) + ); + case 'parallel': + return parallelTransactionPlan(plan.plans.map(freezeTransactionPlan)); + default: + plan satisfies never; + throw new Error( + `Unknown transaction plan kind: ${(plan as { kind: string }).kind}` + ); + } +} + function isValidTransactionPlan(transactionPlan: TransactionPlan): boolean { if (transactionPlan.kind === 'single') { const transactionSize = getTransactionSize(transactionPlan.message); diff --git a/clients/js/test/instructionPlans/transactionPlanner.test.ts b/clients/js/test/instructionPlans/transactionPlanner.test.ts index 8d8d773..d79e793 100644 --- a/clients/js/test/instructionPlans/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlans/transactionPlanner.test.ts @@ -223,6 +223,43 @@ test('it simplifies nested sequential plans', async (t) => { ); }); +/** + * [Seq] ──────────────────────▶ [Seq] + * │ │ + * ├── [A: 50%] ├── [Tx: A + B] + * ├── [Seq] └── [Tx: C + D] + * │ ├── [B: 50%] + * │ └── [C: 50%] + * └── [D: 50%] + */ +test('it simplifies sequential plans nested in the middle', async (t) => { + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(50)); + const instructionD = instruction(txPercent(50)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + sequentialInstructionPlan([ + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]), + singleInstructionPlan(instructionD), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + singleTransactionPlan([instructionC, instructionD]), + ]) + ); +}); + /** * [Par] ───────────────────▶ [Tx: A + B] * │ From 7416673b921b4bb14a131bd2e448bc45eedcf70f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 23 Jun 2025 15:48:21 +0100 Subject: [PATCH 02/10] Use provided config functions instead of wrapping them --- .../transactionPlannerBase.ts | 185 +++++++++++------- 1 file changed, 109 insertions(+), 76 deletions(-) diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index fae9d15..9975e33 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -27,16 +27,20 @@ import { } from './transactionPlan'; import { TransactionPlanner } from './transactionPlanner'; +type CreateTransactionMessage = (config?: { + abortSignal?: AbortSignal; +}) => Promise | CompilableTransactionMessage; + +type OnTransactionMessageUpdated = < + TTransactionMessage extends CompilableTransactionMessage, +>( + transactionMessage: TTransactionMessage, + config?: { abortSignal?: AbortSignal } +) => Promise | TTransactionMessage; + export type TransactionPlannerConfig = { - createTransactionMessage: (config?: { - abortSignal?: AbortSignal; - }) => Promise | CompilableTransactionMessage; - onTransactionMessageUpdated?: < - TTransactionMessage extends CompilableTransactionMessage, - >( - transactionMessage: TTransactionMessage, - config?: { abortSignal?: AbortSignal } - ) => Promise | TTransactionMessage; + createTransactionMessage: CreateTransactionMessage; + onTransactionMessageUpdated?: OnTransactionMessageUpdated; }; export function createBaseTransactionPlanner( @@ -46,32 +50,13 @@ export function createBaseTransactionPlanner( originalInstructionPlan, { abortSignal } = {} ): Promise => { - const createSingleTransactionPlan: CreateSingleTransactionPlanFunction = - async () => { - abortSignal?.throwIfAborted(); - return { - kind: 'single', - message: await Promise.resolve( - config.createTransactionMessage({ abortSignal }) - ), - }; - }; - - const emitSingleTransactionPlanUpdated: EmitSingleTransactionPlanUpdatedFunction = - async (plan) => { - abortSignal?.throwIfAborted(); - if (!config?.onTransactionMessageUpdated) return; - plan.message = await Promise.resolve( - config.onTransactionMessageUpdated(plan.message, { abortSignal }) - ); - }; - const plan = await traverse(originalInstructionPlan, { abortSignal, parent: null, parentCandidates: [], - createSingleTransactionPlan, - emitSingleTransactionPlanUpdated, + createTransactionMessage: config.createTransactionMessage, + onTransactionMessageUpdated: + config.onTransactionMessageUpdated ?? ((msg) => msg), }); if (!plan) { @@ -94,19 +79,12 @@ export function createBaseTransactionPlanner( type MutableTransactionPlan = Mutable; type MutableSingleTransactionPlan = Mutable; -type CreateSingleTransactionPlanFunction = - () => Promise; - -type EmitSingleTransactionPlanUpdatedFunction = ( - plan: MutableSingleTransactionPlan -) => Promise; - type TraverseContext = { abortSignal?: AbortSignal; parent: InstructionPlan | null; parentCandidates: MutableSingleTransactionPlan[]; - createSingleTransactionPlan: CreateSingleTransactionPlanFunction; - emitSingleTransactionPlanUpdated: EmitSingleTransactionPlanUpdatedFunction; + createTransactionMessage: CreateTransactionMessage; + onTransactionMessageUpdated: OnTransactionMessageUpdated; }; async function traverse( @@ -146,8 +124,12 @@ async function traverseSequential( parentCandidate ); if (transactionPlan) { - parentCandidate.message = transactionPlan.message; - await context.emitSingleTransactionPlanUpdated(parentCandidate); + const message = await Promise.resolve( + context.onTransactionMessageUpdated(transactionPlan.message, { + abortSignal: context.abortSignal, + }) + ); + parentCandidate.message = message; return null; } } @@ -230,19 +212,34 @@ async function traverseSingle( context: TraverseContext ): Promise { const ix = instructionPlan.instruction; - const candidate = selectCandidate(context.parentCandidates, [ix]); + + // TODO: Get candidate and new message ??? + const candidate = selectCandidate(context.parentCandidates, (message) => + appendTransactionMessageInstructions([ix], message) + ); + if (candidate) { - candidate.message = appendTransactionMessageInstructions( - [ix], - candidate.message + candidate.message = await Promise.resolve( + context.onTransactionMessageUpdated( + appendTransactionMessageInstructions([ix], candidate.message), + { abortSignal: context.abortSignal } + ) ); - await context.emitSingleTransactionPlanUpdated(candidate); return null; } - const plan = await context.createSingleTransactionPlan(); - plan.message = appendTransactionMessageInstructions([ix], plan.message); - await context.emitSingleTransactionPlanUpdated(plan); - return plan; + + const message = await Promise.resolve( + context.createTransactionMessage({ abortSignal: context.abortSignal }) + ); + return { + kind: 'single', + message: await Promise.resolve( + context.onTransactionMessageUpdated( + appendTransactionMessageInstructions([ix], message), + { abortSignal: context.abortSignal } + ) + ), + }; } async function traverseIterable( @@ -257,24 +254,31 @@ async function traverseIterable( const candidateResult = selectCandidateForIterator(candidates, iterator); if (candidateResult) { const [candidate, ix] = candidateResult; - candidate.message = appendTransactionMessageInstructions( - [ix], - candidate.message + candidate.message = await Promise.resolve( + context.onTransactionMessageUpdated( + appendTransactionMessageInstructions([ix], candidate.message), + { abortSignal: context.abortSignal } + ) ); - await context.emitSingleTransactionPlanUpdated(candidate); } else { - const newPlan = await context.createSingleTransactionPlan(); - const ix = iterator.next(newPlan.message); + const message = await Promise.resolve( + context.createTransactionMessage({ abortSignal: context.abortSignal }) + ); + const ix = iterator.next(message); if (!ix) { throw new Error( 'Could not fit `InterableInstructionPlan` into a transaction' ); } - newPlan.message = appendTransactionMessageInstructions( - [ix], - newPlan.message - ); - await context.emitSingleTransactionPlanUpdated(newPlan); + const newPlan: MutableSingleTransactionPlan = { + kind: 'single', + message: await Promise.resolve( + context.onTransactionMessageUpdated( + appendTransactionMessageInstructions([ix], message), + { abortSignal: context.abortSignal } + ) + ), + }; transactionPlans.push(newPlan); // Adding the new plan to the candidates is important for cases @@ -333,31 +337,56 @@ function selectCandidateForIterator( return null; } +// async function selectCandidateWithContext( +// context: Pick, +// candidates: MutableSingleTransactionPlan[], +// predicate: ( +// message: CompilableTransactionMessage +// ) => CompilableTransactionMessage +// ): Promise { +// for (const candidate of candidates) { +// if (isValidCandidate(candidate, predicate)) { +// // 1. Check immutable message. +// // 2. Apply message to candidate. +// candidate.message = predicate(candidate.message); +// await context.emitSingleTransactionPlanUpdated(candidate); +// if (isValidTransactionPlan(candidate)) { +// return candidate; +// } +// } +// } +// return null; +// } + function selectCandidate( candidates: MutableSingleTransactionPlan[], - instructions: IInstruction[] + predicate: ( + message: CompilableTransactionMessage + ) => CompilableTransactionMessage ): MutableSingleTransactionPlan | null { const firstValidCandidate = candidates.find((candidate) => - isValidCandidate(candidate, instructions) + isValidCandidate(candidate, predicate) ); return firstValidCandidate ?? null; } function isValidCandidate( candidate: MutableSingleTransactionPlan, - instructions: IInstruction[] + predicate: ( + message: CompilableTransactionMessage + ) => CompilableTransactionMessage ): boolean { - const message = appendTransactionMessageInstructions( - instructions, - candidate.message - ); - return getRemainingTransactionSize(message) >= 0; + const message = predicate(candidate.message); + return getTransactionSize(message) <= TRANSACTION_SIZE_LIMIT; } -export function getRemainingTransactionSize( - message: CompilableTransactionMessage -) { - return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); +function isValidCandidateWithInstructions( + candidate: MutableSingleTransactionPlan, + instructions: IInstruction[] +): boolean { + return isValidCandidate(candidate, (message) => + appendTransactionMessageInstructions(instructions, message) + ); } function freezeTransactionPlan(plan: MutableTransactionPlan): TransactionPlan { @@ -406,7 +435,11 @@ function fitEntirePlanInsideCandidate( } return newCandidate; case 'single': - if (!isValidCandidate(candidate, [instructionPlan.instruction])) { + if ( + !isValidCandidateWithInstructions(candidate, [ + instructionPlan.instruction, + ]) + ) { return null; } return singleTransactionPlan( @@ -420,7 +453,7 @@ function fitEntirePlanInsideCandidate( const iterator = instructionPlan.getIterator(); while (iterator.hasNext()) { const ix = iterator.next(candidate.message); - if (!ix || !isValidCandidate(candidate, [ix])) { + if (!ix || !isValidCandidateWithInstructions(candidate, [ix])) { return null; } newCandidate = singleTransactionPlan( From 78ca8a177ecc9b23fc06f4a97d2be32d76e35e48 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 23 Jun 2025 17:09:58 +0100 Subject: [PATCH 03/10] Throw error when iterator cannot assemble --- .../src/instructionPlans/instructionPlan.ts | 19 +- .../transactionPlannerBase.ts | 163 +++++++++--------- .../_instructionPlanHelpers.ts | 3 +- 3 files changed, 92 insertions(+), 93 deletions(-) diff --git a/clients/js/src/instructionPlans/instructionPlan.ts b/clients/js/src/instructionPlans/instructionPlan.ts index 10f5130..2aae23b 100644 --- a/clients/js/src/instructionPlans/instructionPlan.ts +++ b/clients/js/src/instructionPlans/instructionPlan.ts @@ -46,11 +46,18 @@ export type InstructionIterator< /** Checks whether there are more instructions to retrieve. */ hasNext: () => boolean; /** Get the next instruction for the given transaction message or return `null` if not possible. */ - next: ( - transactionMessage: CompilableTransactionMessage - ) => TInstruction | null; + next: (transactionMessage: CompilableTransactionMessage) => TInstruction; }>; +// TODO: Make SolanaError instead. +export class CannotIterateUsingProvidedMessageError extends Error { + constructor() { + super(''); + this.name = + 'Cannot iterate the next instruction using the provided message'; + } +} + export function parallelInstructionPlan( plans: (InstructionPlan | IInstruction)[] ): ParallelInstructionPlan { @@ -114,7 +121,7 @@ export function getLinearIterableInstructionPlan({ 1; /* Leeway for shortU16 numbers in transaction headers. */ if (maxLength <= 0) { - return null; + throw new CannotIterateUsingProvidedMessageError(); } const length = Math.min(totalBytes - offset, maxLength); @@ -138,7 +145,7 @@ export function getIterableInstructionPlanFromInstructions< hasNext: () => instructionIndex < instructions.length, next: (tx: CompilableTransactionMessage) => { if (instructionIndex >= instructions.length) { - return null; + throw new CannotIterateUsingProvidedMessageError(); } const instruction = instructions[instructionIndex]; @@ -147,7 +154,7 @@ export function getIterableInstructionPlanFromInstructions< ); if (transactionSize > TRANSACTION_SIZE_LIMIT) { - return null; + throw new CannotIterateUsingProvidedMessageError(); } instructionIndex++; diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index 9975e33..10ef10e 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -4,7 +4,7 @@ import { IInstruction, } from '@solana/kit'; import { - InstructionIterator, + CannotIterateUsingProvidedMessageError, InstructionPlan, IterableInstructionPlan, ParallelInstructionPlan, @@ -211,35 +211,15 @@ async function traverseSingle( instructionPlan: SingleInstructionPlan, context: TraverseContext ): Promise { - const ix = instructionPlan.instruction; - - // TODO: Get candidate and new message ??? - const candidate = selectCandidate(context.parentCandidates, (message) => - appendTransactionMessageInstructions([ix], message) - ); - - if (candidate) { - candidate.message = await Promise.resolve( - context.onTransactionMessageUpdated( - appendTransactionMessageInstructions([ix], candidate.message), - { abortSignal: context.abortSignal } + return await selectCandidateOrCreateNewPlan( + context, + context.parentCandidates, + (message) => + appendTransactionMessageInstructions( + [instructionPlan.instruction], + message ) - ); - return null; - } - - const message = await Promise.resolve( - context.createTransactionMessage({ abortSignal: context.abortSignal }) ); - return { - kind: 'single', - message: await Promise.resolve( - context.onTransactionMessageUpdated( - appendTransactionMessageInstructions([ix], message), - { abortSignal: context.abortSignal } - ) - ), - }; } async function traverseIterable( @@ -251,34 +231,20 @@ async function traverseIterable( const candidates = [...context.parentCandidates]; while (iterator.hasNext()) { - const candidateResult = selectCandidateForIterator(candidates, iterator); + const candidateResult = await selectCandidateMessage( + context, + candidates, + (message) => + appendTransactionMessageInstructions([iterator.next(message)], message) + ); if (candidateResult) { - const [candidate, ix] = candidateResult; - candidate.message = await Promise.resolve( - context.onTransactionMessageUpdated( - appendTransactionMessageInstructions([ix], candidate.message), - { abortSignal: context.abortSignal } - ) - ); + const [candidate, candidateMessage] = candidateResult; + candidate.message = candidateMessage; } else { - const message = await Promise.resolve( - context.createTransactionMessage({ abortSignal: context.abortSignal }) + const message = await createNewMessage(context, (message) => + appendTransactionMessageInstructions([iterator.next(message)], message) ); - const ix = iterator.next(message); - if (!ix) { - throw new Error( - 'Could not fit `InterableInstructionPlan` into a transaction' - ); - } - const newPlan: MutableSingleTransactionPlan = { - kind: 'single', - message: await Promise.resolve( - context.onTransactionMessageUpdated( - appendTransactionMessageInstructions([ix], message), - { abortSignal: context.abortSignal } - ) - ), - }; + const newPlan: MutableSingleTransactionPlan = { kind: 'single', message }; transactionPlans.push(newPlan); // Adding the new plan to the candidates is important for cases @@ -324,50 +290,75 @@ function getParallelCandidates( return getAllSingleTransactionPlans(latestPlan); } -function selectCandidateForIterator( +async function selectCandidateOrCreateNewPlan( + context: Pick< + TraverseContext, + 'createTransactionMessage' | 'onTransactionMessageUpdated' | 'abortSignal' + >, + candidates: MutableSingleTransactionPlan[], + predicate: ( + message: CompilableTransactionMessage + ) => CompilableTransactionMessage +): Promise { + const candidateResult = await selectCandidateMessage( + context, + candidates, + predicate + ); + if (candidateResult) { + const [candidate, candidateMessage] = candidateResult; + candidate.message = candidateMessage; + return null; + } + const message = await createNewMessage(context, predicate); + return { kind: 'single', message }; +} + +async function selectCandidateMessage( + context: Pick, candidates: MutableSingleTransactionPlan[], - iterator: InstructionIterator -): [MutableSingleTransactionPlan, IInstruction] | null { + predicate: ( + message: CompilableTransactionMessage + ) => CompilableTransactionMessage +): Promise< + [MutableSingleTransactionPlan, CompilableTransactionMessage] | null +> { for (const candidate of candidates) { - const ix = iterator.next(candidate.message); - if (ix) { - return [candidate, ix]; + try { + const message = await Promise.resolve( + context.onTransactionMessageUpdated(predicate(candidate.message), { + abortSignal: context.abortSignal, + }) + ); + if (getTransactionSize(message) <= TRANSACTION_SIZE_LIMIT) { + return [candidate, message]; + } + } catch (error) { + if (!(error instanceof CannotIterateUsingProvidedMessageError)) { + throw error; + } } } return null; } -// async function selectCandidateWithContext( -// context: Pick, -// candidates: MutableSingleTransactionPlan[], -// predicate: ( -// message: CompilableTransactionMessage -// ) => CompilableTransactionMessage -// ): Promise { -// for (const candidate of candidates) { -// if (isValidCandidate(candidate, predicate)) { -// // 1. Check immutable message. -// // 2. Apply message to candidate. -// candidate.message = predicate(candidate.message); -// await context.emitSingleTransactionPlanUpdated(candidate); -// if (isValidTransactionPlan(candidate)) { -// return candidate; -// } -// } -// } -// return null; -// } - -function selectCandidate( - candidates: MutableSingleTransactionPlan[], +async function createNewMessage( + context: Pick< + TraverseContext, + 'createTransactionMessage' | 'onTransactionMessageUpdated' | 'abortSignal' + >, predicate: ( message: CompilableTransactionMessage ) => CompilableTransactionMessage -): MutableSingleTransactionPlan | null { - const firstValidCandidate = candidates.find((candidate) => - isValidCandidate(candidate, predicate) +): Promise { + const message = await Promise.resolve( + context.createTransactionMessage({ abortSignal: context.abortSignal }) + ); + return await Promise.resolve( + context.onTransactionMessageUpdated(predicate(message), { + abortSignal: context.abortSignal, + }) ); - return firstValidCandidate ?? null; } function isValidCandidate( diff --git a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts index c55f29d..59f41e9 100644 --- a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts @@ -8,6 +8,7 @@ import { IInstruction, } from '@solana/kit'; import { + CannotIterateUsingProvidedMessageError, getTransactionSize, IterableInstructionPlan, TRANSACTION_SIZE_LIMIT, @@ -47,7 +48,7 @@ export function instructionIteratorFactory() { 1; /* Leeway for shortU16 numbers in transaction headers. */ if (maxLength <= 0) { - return null; + throw new CannotIterateUsingProvidedMessageError(); } const length = Math.min( From e2a9024fe4aef661282f13272472282c263371a1 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 23 Jun 2025 17:21:44 +0100 Subject: [PATCH 04/10] Make iterator return update messages --- .../src/instructionPlans/instructionPlan.ts | 36 +++++------ .../transactionPlannerBase.ts | 62 ++++++------------- .../_instructionPlanHelpers.ts | 6 +- 3 files changed, 41 insertions(+), 63 deletions(-) diff --git a/clients/js/src/instructionPlans/instructionPlan.ts b/clients/js/src/instructionPlans/instructionPlan.ts index 2aae23b..5ab3112 100644 --- a/clients/js/src/instructionPlans/instructionPlan.ts +++ b/clients/js/src/instructionPlans/instructionPlan.ts @@ -32,29 +32,26 @@ export type SingleInstructionPlan< instruction: TInstruction; }>; -export type IterableInstructionPlan< - TInstruction extends IInstruction = IInstruction, -> = Readonly<{ +export type IterableInstructionPlan = Readonly<{ kind: 'iterable'; /** Get an iterator for the instructions. */ - getIterator: () => InstructionIterator; + getIterator: () => InstructionIterator; }>; -export type InstructionIterator< - TInstruction extends IInstruction = IInstruction, -> = Readonly<{ +export type InstructionIterator = Readonly<{ /** Checks whether there are more instructions to retrieve. */ hasNext: () => boolean; /** Get the next instruction for the given transaction message or return `null` if not possible. */ - next: (transactionMessage: CompilableTransactionMessage) => TInstruction; + next: ( + transactionMessage: CompilableTransactionMessage + ) => CompilableTransactionMessage; }>; // TODO: Make SolanaError instead. export class CannotIterateUsingProvidedMessageError extends Error { constructor() { - super(''); - this.name = - 'Cannot iterate the next instruction using the provided message'; + super('Cannot iterate the next instruction using the provided message'); + this.name = 'CannotIterateUsingProvidedMessageError'; } } @@ -111,9 +108,12 @@ export function getLinearIterableInstructionPlan({ let offset = 0; return { hasNext: () => offset < totalBytes, - next: (tx: CompilableTransactionMessage) => { + next: (message: CompilableTransactionMessage) => { const baseTransactionSize = getTransactionSize( - appendTransactionMessageInstruction(getInstruction(offset, 0), tx) + appendTransactionMessageInstruction( + getInstruction(offset, 0), + message + ) ); const maxLength = TRANSACTION_SIZE_LIMIT - @@ -127,7 +127,7 @@ export function getLinearIterableInstructionPlan({ const length = Math.min(totalBytes - offset, maxLength); const instruction = getInstruction(offset, length); offset += length; - return instruction; + return appendTransactionMessageInstruction(instruction, message); }, }; }, @@ -136,21 +136,21 @@ export function getLinearIterableInstructionPlan({ export function getIterableInstructionPlanFromInstructions< TInstruction extends IInstruction = IInstruction, ->(instructions: TInstruction[]): IterableInstructionPlan { +>(instructions: TInstruction[]): IterableInstructionPlan { return { kind: 'iterable', getIterator: () => { let instructionIndex = 0; return { hasNext: () => instructionIndex < instructions.length, - next: (tx: CompilableTransactionMessage) => { + next: (message: CompilableTransactionMessage) => { if (instructionIndex >= instructions.length) { throw new CannotIterateUsingProvidedMessageError(); } const instruction = instructions[instructionIndex]; const transactionSize = getTransactionSize( - appendTransactionMessageInstruction(instruction, tx) + appendTransactionMessageInstruction(instruction, message) ); if (transactionSize > TRANSACTION_SIZE_LIMIT) { @@ -158,7 +158,7 @@ export function getIterableInstructionPlanFromInstructions< } instructionIndex++; - return instruction; + return appendTransactionMessageInstruction(instruction, message); }, }; }, diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index 10ef10e..d26582a 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -1,7 +1,6 @@ import { appendTransactionMessageInstructions, CompilableTransactionMessage, - IInstruction, } from '@solana/kit'; import { CannotIterateUsingProvidedMessageError, @@ -234,16 +233,13 @@ async function traverseIterable( const candidateResult = await selectCandidateMessage( context, candidates, - (message) => - appendTransactionMessageInstructions([iterator.next(message)], message) + iterator.next ); if (candidateResult) { const [candidate, candidateMessage] = candidateResult; candidate.message = candidateMessage; } else { - const message = await createNewMessage(context, (message) => - appendTransactionMessageInstructions([iterator.next(message)], message) - ); + const message = await createNewMessage(context, iterator.next); const newPlan: MutableSingleTransactionPlan = { kind: 'single', message }; transactionPlans.push(newPlan); @@ -361,25 +357,6 @@ async function createNewMessage( ); } -function isValidCandidate( - candidate: MutableSingleTransactionPlan, - predicate: ( - message: CompilableTransactionMessage - ) => CompilableTransactionMessage -): boolean { - const message = predicate(candidate.message); - return getTransactionSize(message) <= TRANSACTION_SIZE_LIMIT; -} - -function isValidCandidateWithInstructions( - candidate: MutableSingleTransactionPlan, - instructions: IInstruction[] -): boolean { - return isValidCandidate(candidate, (message) => - appendTransactionMessageInstructions(instructions, message) - ); -} - function freezeTransactionPlan(plan: MutableTransactionPlan): TransactionPlan { switch (plan.kind) { case 'single': @@ -426,30 +403,31 @@ function fitEntirePlanInsideCandidate( } return newCandidate; case 'single': - if ( - !isValidCandidateWithInstructions(candidate, [ - instructionPlan.instruction, - ]) - ) { + // eslint-disable-next-line no-case-declarations + const message = appendTransactionMessageInstructions( + [instructionPlan.instruction], + candidate.message + ); + if (getTransactionSize(message) > TRANSACTION_SIZE_LIMIT) { return null; } - return singleTransactionPlan( - appendTransactionMessageInstructions( - [instructionPlan.instruction], - candidate.message - ) - ); + return singleTransactionPlan(message); case 'iterable': // eslint-disable-next-line no-case-declarations const iterator = instructionPlan.getIterator(); while (iterator.hasNext()) { - const ix = iterator.next(candidate.message); - if (!ix || !isValidCandidateWithInstructions(candidate, [ix])) { - return null; + try { + const message = iterator.next(candidate.message); + if (getTransactionSize(message) > TRANSACTION_SIZE_LIMIT) { + return null; + } + newCandidate = singleTransactionPlan(message); + } catch (error) { + if (error instanceof CannotIterateUsingProvidedMessageError) { + return null; + } + throw error; } - newCandidate = singleTransactionPlan( - appendTransactionMessageInstructions([ix], newCandidate.message) - ); } return newCandidate; default: diff --git a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts index 59f41e9..348ccb6 100644 --- a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts @@ -38,9 +38,9 @@ export function instructionIteratorFactory() { let offset = 0; return { hasNext: () => offset < totalBytes, - next: (tx) => { + next: (message) => { const baseTransactionSize = getTransactionSize( - appendTransactionMessageInstruction(baseInstruction, tx) + appendTransactionMessageInstruction(baseInstruction, message) ); const maxLength = TRANSACTION_SIZE_LIMIT - @@ -58,7 +58,7 @@ export function instructionIteratorFactory() { const instruction = getInstruction(length); offset += length; - return instruction; + return appendTransactionMessageInstruction(instruction, message); }, }; }, From 1715299b73fd6e98096a8b7283f6cdf19ea96f1a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 23 Jun 2025 17:28:02 +0100 Subject: [PATCH 05/10] Refactor candidate selection --- .../transactionPlannerBase.ts | 58 ++++++------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index d26582a..fdda1d5 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -210,15 +210,21 @@ async function traverseSingle( instructionPlan: SingleInstructionPlan, context: TraverseContext ): Promise { - return await selectCandidateOrCreateNewPlan( + const predicate = (message: CompilableTransactionMessage) => + appendTransactionMessageInstructions( + [instructionPlan.instruction], + message + ); + const candidate = await selectAndMutateCandidate( context, context.parentCandidates, - (message) => - appendTransactionMessageInstructions( - [instructionPlan.instruction], - message - ) + predicate ); + if (candidate) { + return null; + } + const message = await createNewMessage(context, predicate); + return { kind: 'single', message }; } async function traverseIterable( @@ -230,15 +236,12 @@ async function traverseIterable( const candidates = [...context.parentCandidates]; while (iterator.hasNext()) { - const candidateResult = await selectCandidateMessage( + const candidate = await selectAndMutateCandidate( context, candidates, iterator.next ); - if (candidateResult) { - const [candidate, candidateMessage] = candidateResult; - candidate.message = candidateMessage; - } else { + if (!candidate) { const message = await createNewMessage(context, iterator.next); const newPlan: MutableSingleTransactionPlan = { kind: 'single', message }; transactionPlans.push(newPlan); @@ -286,39 +289,13 @@ function getParallelCandidates( return getAllSingleTransactionPlans(latestPlan); } -async function selectCandidateOrCreateNewPlan( - context: Pick< - TraverseContext, - 'createTransactionMessage' | 'onTransactionMessageUpdated' | 'abortSignal' - >, - candidates: MutableSingleTransactionPlan[], - predicate: ( - message: CompilableTransactionMessage - ) => CompilableTransactionMessage -): Promise { - const candidateResult = await selectCandidateMessage( - context, - candidates, - predicate - ); - if (candidateResult) { - const [candidate, candidateMessage] = candidateResult; - candidate.message = candidateMessage; - return null; - } - const message = await createNewMessage(context, predicate); - return { kind: 'single', message }; -} - -async function selectCandidateMessage( +async function selectAndMutateCandidate( context: Pick, candidates: MutableSingleTransactionPlan[], predicate: ( message: CompilableTransactionMessage ) => CompilableTransactionMessage -): Promise< - [MutableSingleTransactionPlan, CompilableTransactionMessage] | null -> { +): Promise { for (const candidate of candidates) { try { const message = await Promise.resolve( @@ -327,7 +304,8 @@ async function selectCandidateMessage( }) ); if (getTransactionSize(message) <= TRANSACTION_SIZE_LIMIT) { - return [candidate, message]; + candidate.message = message; + return candidate; } } catch (error) { if (!(error instanceof CannotIterateUsingProvidedMessageError)) { From 7f6aa26c9dc4e24a113aabb1e51f1dcff97ad252 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 23 Jun 2025 17:53:16 +0100 Subject: [PATCH 06/10] Refactor sequential branches that must fit in parents --- .../transactionPlannerBase.ts | 58 ++++++++----------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index fdda1d5..9484b24 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -117,20 +117,13 @@ async function traverseSequential( context.parent && (context.parent.kind === 'parallel' || !instructionPlan.divisible); if (mustEntirelyFitInParentCandidate) { - for (const parentCandidate of context.parentCandidates) { - const transactionPlan = fitEntirePlanInsideCandidate( - instructionPlan, - parentCandidate - ); - if (transactionPlan) { - const message = await Promise.resolve( - context.onTransactionMessageUpdated(transactionPlan.message, { - abortSignal: context.abortSignal, - }) - ); - parentCandidate.message = message; - return null; - } + const candidate = await selectAndMutateCandidate( + context, + context.parentCandidates, + (message) => fitEntirePlanInsideMessage(instructionPlan, message) + ); + if (candidate) { + return null; } } else { candidate = @@ -363,51 +356,46 @@ function isValidTransactionPlan(transactionPlan: TransactionPlan): boolean { return transactionPlan.plans.every(isValidTransactionPlan); } -function fitEntirePlanInsideCandidate( +function fitEntirePlanInsideMessage( instructionPlan: InstructionPlan, - candidate: SingleTransactionPlan -): SingleTransactionPlan | null { - let newCandidate: SingleTransactionPlan = candidate; + message: CompilableTransactionMessage +): CompilableTransactionMessage { + let newMessage: CompilableTransactionMessage = message; switch (instructionPlan.kind) { case 'sequential': case 'parallel': for (const plan of instructionPlan.plans) { - const result = fitEntirePlanInsideCandidate(plan, newCandidate); - if (result === null) { - return null; - } - newCandidate = result; + newMessage = fitEntirePlanInsideMessage(plan, newMessage); } - return newCandidate; + return newMessage; case 'single': // eslint-disable-next-line no-case-declarations - const message = appendTransactionMessageInstructions( + newMessage = appendTransactionMessageInstructions( [instructionPlan.instruction], - candidate.message + message ); - if (getTransactionSize(message) > TRANSACTION_SIZE_LIMIT) { - return null; + if (getTransactionSize(newMessage) > TRANSACTION_SIZE_LIMIT) { + throw new CannotIterateUsingProvidedMessageError(); // TODO: Different error } - return singleTransactionPlan(message); + return newMessage; case 'iterable': // eslint-disable-next-line no-case-declarations const iterator = instructionPlan.getIterator(); while (iterator.hasNext()) { try { - const message = iterator.next(candidate.message); - if (getTransactionSize(message) > TRANSACTION_SIZE_LIMIT) { - return null; + newMessage = iterator.next(message); + if (getTransactionSize(newMessage) > TRANSACTION_SIZE_LIMIT) { + throw new CannotIterateUsingProvidedMessageError(); // TODO: Different error } - newCandidate = singleTransactionPlan(message); } catch (error) { if (error instanceof CannotIterateUsingProvidedMessageError) { - return null; + throw new CannotIterateUsingProvidedMessageError(); // TODO: Different error } throw error; } } - return newCandidate; + return newMessage; default: instructionPlan satisfies never; throw new Error( From 02af1c10ebad37f907e5c6d31193a66fd1be2c43 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 23 Jun 2025 17:56:56 +0100 Subject: [PATCH 07/10] Add new error for fitEntirePlanInsideMessage --- .../transactionPlannerBase.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index 9484b24..fa43b1c 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -301,7 +301,12 @@ async function selectAndMutateCandidate( return candidate; } } catch (error) { - if (!(error instanceof CannotIterateUsingProvidedMessageError)) { + if ( + error instanceof CannotIterateUsingProvidedMessageError || + error instanceof CannotFitEntirePlanInsideMessageError + ) { + // Try the next candidate. + } else { throw error; } } @@ -356,6 +361,13 @@ function isValidTransactionPlan(transactionPlan: TransactionPlan): boolean { return transactionPlan.plans.every(isValidTransactionPlan); } +class CannotFitEntirePlanInsideMessageError extends Error { + constructor() { + super('Cannot fit the entire instruction plan inside the provided message'); + this.name = 'CannotFitEntirePlanInsideMessageError'; + } +} + function fitEntirePlanInsideMessage( instructionPlan: InstructionPlan, message: CompilableTransactionMessage @@ -376,7 +388,7 @@ function fitEntirePlanInsideMessage( message ); if (getTransactionSize(newMessage) > TRANSACTION_SIZE_LIMIT) { - throw new CannotIterateUsingProvidedMessageError(); // TODO: Different error + throw new CannotFitEntirePlanInsideMessageError(); } return newMessage; case 'iterable': @@ -386,11 +398,11 @@ function fitEntirePlanInsideMessage( try { newMessage = iterator.next(message); if (getTransactionSize(newMessage) > TRANSACTION_SIZE_LIMIT) { - throw new CannotIterateUsingProvidedMessageError(); // TODO: Different error + throw new CannotFitEntirePlanInsideMessageError(); } } catch (error) { if (error instanceof CannotIterateUsingProvidedMessageError) { - throw new CannotIterateUsingProvidedMessageError(); // TODO: Different error + throw new CannotFitEntirePlanInsideMessageError(); } throw error; } From 110ed67d588c09207368993ee33d6775b974863a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 23 Jun 2025 18:40:40 +0100 Subject: [PATCH 08/10] Rename iterable plan to message packer plan --- .../src/instructionPlans/instructionPlan.ts | 47 ++-- .../transactionPlannerBase.ts | 35 +-- clients/js/src/utils.ts | 14 +- .../_instructionPlanHelpers.ts | 24 ++- .../transactionPlanner.test.ts | 201 +++++++++++------- 5 files changed, 181 insertions(+), 140 deletions(-) diff --git a/clients/js/src/instructionPlans/instructionPlan.ts b/clients/js/src/instructionPlans/instructionPlan.ts index 5ab3112..01e2fa0 100644 --- a/clients/js/src/instructionPlans/instructionPlan.ts +++ b/clients/js/src/instructionPlans/instructionPlan.ts @@ -12,7 +12,7 @@ export type InstructionPlan = | SequentialInstructionPlan | ParallelInstructionPlan | SingleInstructionPlan - | IterableInstructionPlan; + | MessagePackerInstructionPlan; export type SequentialInstructionPlan = Readonly<{ kind: 'sequential'; @@ -32,17 +32,16 @@ export type SingleInstructionPlan< instruction: TInstruction; }>; -export type IterableInstructionPlan = Readonly<{ - kind: 'iterable'; - /** Get an iterator for the instructions. */ - getIterator: () => InstructionIterator; +export type MessagePackerInstructionPlan = Readonly<{ + kind: 'messagePacker'; + getMessagePacker: () => MessagePacker; }>; -export type InstructionIterator = Readonly<{ +export type MessagePacker = Readonly<{ /** Checks whether there are more instructions to retrieve. */ - hasNext: () => boolean; - /** Get the next instruction for the given transaction message or return `null` if not possible. */ - next: ( + done: () => boolean; + /** Pack the provided transaction message with the next instructions or throws if not possible. */ + packMessage: ( transactionMessage: CompilableTransactionMessage ) => CompilableTransactionMessage; }>; @@ -95,20 +94,20 @@ function parseSingleInstructionPlans( ); } -export function getLinearIterableInstructionPlan({ +export function getLinearMessagePackerInstructionPlan({ getInstruction, totalLength: totalBytes, }: { getInstruction: (offset: number, length: number) => IInstruction; totalLength: number; -}): IterableInstructionPlan { +}): MessagePackerInstructionPlan { return { - kind: 'iterable', - getIterator: () => { + kind: 'messagePacker', + getMessagePacker: () => { let offset = 0; return { - hasNext: () => offset < totalBytes, - next: (message: CompilableTransactionMessage) => { + done: () => offset < totalBytes, + packMessage: (message: CompilableTransactionMessage) => { const baseTransactionSize = getTransactionSize( appendTransactionMessageInstruction( getInstruction(offset, 0), @@ -134,16 +133,16 @@ export function getLinearIterableInstructionPlan({ }; } -export function getIterableInstructionPlanFromInstructions< +export function getMessagePackerInstructionPlanFromInstructions< TInstruction extends IInstruction = IInstruction, ->(instructions: TInstruction[]): IterableInstructionPlan { +>(instructions: TInstruction[]): MessagePackerInstructionPlan { return { - kind: 'iterable', - getIterator: () => { + kind: 'messagePacker', + getMessagePacker: () => { let instructionIndex = 0; return { - hasNext: () => instructionIndex < instructions.length, - next: (message: CompilableTransactionMessage) => { + done: () => instructionIndex < instructions.length, + packMessage: (message: CompilableTransactionMessage) => { if (instructionIndex >= instructions.length) { throw new CannotIterateUsingProvidedMessageError(); } @@ -167,13 +166,13 @@ export function getIterableInstructionPlanFromInstructions< const REALLOC_LIMIT = 10_240; -export function getReallocIterableInstructionPlan({ +export function getReallocMessagePackerInstructionPlan({ getInstruction, totalSize, }: { getInstruction: (size: number) => IInstruction; totalSize: number; -}): IterableInstructionPlan { +}): MessagePackerInstructionPlan { const numberOfInstructions = Math.ceil(totalSize / REALLOC_LIMIT); const lastInstructionSize = totalSize % REALLOC_LIMIT; const instructions = new Array(numberOfInstructions) @@ -184,5 +183,5 @@ export function getReallocIterableInstructionPlan({ ) ); - return getIterableInstructionPlanFromInstructions(instructions); + return getMessagePackerInstructionPlanFromInstructions(instructions); } diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index fa43b1c..5328185 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -5,7 +5,7 @@ import { import { CannotIterateUsingProvidedMessageError, InstructionPlan, - IterableInstructionPlan, + MessagePackerInstructionPlan, ParallelInstructionPlan, SequentialInstructionPlan, SingleInstructionPlan, @@ -98,8 +98,8 @@ async function traverse( return await traverseParallel(instructionPlan, context); case 'single': return await traverseSingle(instructionPlan, context); - case 'iterable': - return await traverseIterable(instructionPlan, context); + case 'messagePacker': + return await traverseMessagePacker(instructionPlan, context); default: instructionPlan satisfies never; throw new Error( @@ -169,10 +169,10 @@ async function traverseParallel( ]; const transactionPlans: TransactionPlan[] = []; - // Reorder children so iterable plans are last. + // Reorder children so message packer plans are last. const sortedChildren = [ - ...instructionPlan.plans.filter((plan) => plan.kind !== 'iterable'), - ...instructionPlan.plans.filter((plan) => plan.kind === 'iterable'), + ...instructionPlan.plans.filter((plan) => plan.kind !== 'messagePacker'), + ...instructionPlan.plans.filter((plan) => plan.kind === 'messagePacker'), ]; for (const plan of sortedChildren) { @@ -220,22 +220,25 @@ async function traverseSingle( return { kind: 'single', message }; } -async function traverseIterable( - instructionPlan: IterableInstructionPlan, +async function traverseMessagePacker( + instructionPlan: MessagePackerInstructionPlan, context: TraverseContext ): Promise { - const iterator = instructionPlan.getIterator(); + const messagePacker = instructionPlan.getMessagePacker(); const transactionPlans: SingleTransactionPlan[] = []; const candidates = [...context.parentCandidates]; - while (iterator.hasNext()) { + while (messagePacker.done()) { const candidate = await selectAndMutateCandidate( context, candidates, - iterator.next + messagePacker.packMessage ); if (!candidate) { - const message = await createNewMessage(context, iterator.next); + const message = await createNewMessage( + context, + messagePacker.packMessage + ); const newPlan: MutableSingleTransactionPlan = { kind: 'single', message }; transactionPlans.push(newPlan); @@ -391,12 +394,12 @@ function fitEntirePlanInsideMessage( throw new CannotFitEntirePlanInsideMessageError(); } return newMessage; - case 'iterable': + case 'messagePacker': // eslint-disable-next-line no-case-declarations - const iterator = instructionPlan.getIterator(); - while (iterator.hasNext()) { + const messagePacker = instructionPlan.getMessagePacker(); + while (messagePacker.done()) { try { - newMessage = iterator.next(message); + newMessage = messagePacker.packMessage(message); if (getTransactionSize(newMessage) > TRANSACTION_SIZE_LIMIT) { throw new CannotFitEntirePlanInsideMessageError(); } diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 4f97eac..288451c 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -27,9 +27,9 @@ import { SeedArgs, } from './generated'; import { - getLinearIterableInstructionPlan, - getReallocIterableInstructionPlan, - IterableInstructionPlan, + getLinearMessagePackerInstructionPlan, + getReallocMessagePackerInstructionPlan, + MessagePackerInstructionPlan, TransactionPlanResult, } from './instructionPlans'; @@ -176,8 +176,8 @@ export function getExtendInstructionPlan(input: { extraLength: number; program?: Address; programData?: Address; -}): IterableInstructionPlan { - return getReallocIterableInstructionPlan({ +}): MessagePackerInstructionPlan { + return getReallocMessagePackerInstructionPlan({ totalSize: input.extraLength, getInstruction: (size) => getExtendInstruction({ @@ -194,8 +194,8 @@ export function getWriteInstructionPlan(input: { buffer: Address; authority: TransactionSigner; data: ReadonlyUint8Array; -}): IterableInstructionPlan { - return getLinearIterableInstructionPlan({ +}): MessagePackerInstructionPlan { + return getLinearMessagePackerInstructionPlan({ totalLength: input.data.length, getInstruction: (offset, length) => getWriteInstruction({ diff --git a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts index 348ccb6..2a99c4d 100644 --- a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts @@ -10,7 +10,7 @@ import { import { CannotIterateUsingProvidedMessageError, getTransactionSize, - IterableInstructionPlan, + MessagePackerInstructionPlan, TRANSACTION_SIZE_LIMIT, } from '../../src'; @@ -18,27 +18,29 @@ const MINIMUM_INSTRUCTION_SIZE = 35; const MINIMUM_TRANSACTION_SIZE = 136; const MAXIMUM_TRANSACTION_SIZE = TRANSACTION_SIZE_LIMIT - 1; // (for shortU16) -export function instructionIteratorFactory() { +export function messagePackerFactory() { const baseCounter = 1_000_000_000n; - const iteratorIncrement = 1_000_000_000n; - let iteratorCounter = 0n; + const messagePackerIncrement = 1_000_000_000n; + let messagePackerCounter = 0n; return ( totalBytes: number - ): IterableInstructionPlan & { + ): MessagePackerInstructionPlan & { get: (bytes: number, index: number) => IInstruction; } => { - const getInstruction = instructionFactory(baseCounter + iteratorCounter); - iteratorCounter += iteratorIncrement; + const getInstruction = instructionFactory( + baseCounter + messagePackerCounter + ); + messagePackerCounter += messagePackerIncrement; const baseInstruction = getInstruction(MINIMUM_INSTRUCTION_SIZE, 0); return { get: getInstruction, - kind: 'iterable', - getIterator: () => { + kind: 'messagePacker', + getMessagePacker: () => { let offset = 0; return { - hasNext: () => offset < totalBytes, - next: (message) => { + done: () => offset < totalBytes, + packMessage: (message) => { const baseTransactionSize = getTransactionSize( appendTransactionMessageInstruction(baseInstruction, message) ); diff --git a/clients/js/test/instructionPlans/transactionPlanner.test.ts b/clients/js/test/instructionPlans/transactionPlanner.test.ts index d79e793..bc74eff 100644 --- a/clients/js/test/instructionPlans/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlans/transactionPlanner.test.ts @@ -13,7 +13,7 @@ import { } from '../../src'; import { instructionFactory, - instructionIteratorFactory, + messagePackerFactory, transactionPercentFactory, } from './_instructionPlanHelpers'; import { @@ -32,7 +32,7 @@ function defaultFactories( createTransactionMessage: effectiveCreateTransactionMessage, }), instruction: instructionFactory(), - iterator: instructionIteratorFactory(), + messagePacker: messagePackerFactory(), txPercent: transactionPercentFactory(effectiveCreateTransactionMessage), singleTransactionPlan: singleTransactionPlanFactory( effectiveCreateTransactionMessage @@ -1098,19 +1098,19 @@ test('it plans non-divisible sequentials plans with divisible sequential childre * ├── [Tx: A(2, 100%)] * └── [Tx: A(3, 50%)] */ -test('it iterate over iterable instruction plans', async (t) => { - const { createPlanner, txPercent, iterator, singleTransactionPlan } = +test('it iterate over message packer instruction plans', async (t) => { + const { createPlanner, txPercent, messagePacker, singleTransactionPlan } = defaultFactories(); const planner = createPlanner(); - const iteratorIx = iterator(txPercent(250)); + const messagePackerIx = messagePacker(txPercent(250)); t.deepEqual( - await planner(iteratorIx), + await planner(messagePackerIx), sequentialTransactionPlan([ - singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]), - singleTransactionPlan([iteratorIx.get(txPercent(100), 1)]), - singleTransactionPlan([iteratorIx.get(txPercent(50), 2)]), + singleTransactionPlan([messagePackerIx.get(txPercent(100), 0)]), + singleTransactionPlan([messagePackerIx.get(txPercent(100), 1)]), + singleTransactionPlan([messagePackerIx.get(txPercent(50), 2)]), ]) ); }); @@ -1121,27 +1121,27 @@ test('it iterate over iterable instruction plans', async (t) => { * ├── [A: 50%] * └── [B(x, 50%)] */ -test('it combines single instruction plans with iterable instruction plans', async (t) => { +test('it combines single instruction plans with message packer instruction plans', async (t) => { const { createPlanner, txPercent, - iterator, + messagePacker, instruction, singleTransactionPlan, } = defaultFactories(); const planner = createPlanner(); const instructionA = instruction(txPercent(50)); - const iteratorB = iterator(txPercent(50)); + const messagePackerB = messagePacker(txPercent(50)); t.deepEqual( await planner( sequentialInstructionPlan([ singleInstructionPlan(instructionA), - iteratorB, + messagePackerB, ]) ), - singleTransactionPlan([instructionA, iteratorB.get(txPercent(50), 0)]) + singleTransactionPlan([instructionA, messagePackerB.get(txPercent(50), 0)]) ); }); @@ -1152,19 +1152,19 @@ test('it combines single instruction plans with iterable instruction plans', asy * ├── [Tx: A(2, 100%)] * └── [Tx: A(3, 50%)] */ -test('it can handle parallel iterable instruction plans', async (t) => { - const { createPlanner, txPercent, iterator, singleTransactionPlan } = +test('it can handle parallel message packer instruction plans', async (t) => { + const { createPlanner, txPercent, messagePacker, singleTransactionPlan } = defaultFactories(); const planner = createPlanner(); - const iteratorA = iterator(txPercent(250)); + const messagePackerA = messagePacker(txPercent(250)); t.deepEqual( - await planner(parallelInstructionPlan([iteratorA])), + await planner(parallelInstructionPlan([messagePackerA])), parallelTransactionPlan([ - singleTransactionPlan([iteratorA.get(txPercent(100), 0)]), - singleTransactionPlan([iteratorA.get(txPercent(100), 1)]), - singleTransactionPlan([iteratorA.get(txPercent(50), 2)]), + singleTransactionPlan([messagePackerA.get(txPercent(100), 0)]), + singleTransactionPlan([messagePackerA.get(txPercent(100), 1)]), + singleTransactionPlan([messagePackerA.get(txPercent(50), 2)]), ]) ); }); @@ -1176,19 +1176,19 @@ test('it can handle parallel iterable instruction plans', async (t) => { * ├── [Tx: A(2, 100%)] * └── [Tx: A(3, 50%)] */ -test('it can handle non-divisible sequential iterable instruction plans', async (t) => { - const { createPlanner, txPercent, iterator, singleTransactionPlan } = +test('it can handle non-divisible sequential message packer instruction plans', async (t) => { + const { createPlanner, txPercent, messagePacker, singleTransactionPlan } = defaultFactories(); const planner = createPlanner(); - const iteratorA = iterator(txPercent(250)); + const messagePackerA = messagePacker(txPercent(250)); t.deepEqual( - await planner(nonDivisibleSequentialInstructionPlan([iteratorA])), + await planner(nonDivisibleSequentialInstructionPlan([messagePackerA])), nonDivisibleSequentialTransactionPlan([ - singleTransactionPlan([iteratorA.get(txPercent(100), 0)]), - singleTransactionPlan([iteratorA.get(txPercent(100), 1)]), - singleTransactionPlan([iteratorA.get(txPercent(50), 2)]), + singleTransactionPlan([messagePackerA.get(txPercent(100), 0)]), + singleTransactionPlan([messagePackerA.get(txPercent(100), 1)]), + singleTransactionPlan([messagePackerA.get(txPercent(50), 2)]), ]) ); }); @@ -1196,16 +1196,16 @@ test('it can handle non-divisible sequential iterable instruction plans', async /** * [A(x, 100%)] ─────────────▶ [Tx: A(1, 100%)] */ -test('it simplifies iterable instruction plans that fit in a single transaction', async (t) => { - const { createPlanner, txPercent, iterator, singleTransactionPlan } = +test('it simplifies message packer instruction plans that fit in a single transaction', async (t) => { + const { createPlanner, txPercent, messagePacker, singleTransactionPlan } = defaultFactories(); const planner = createPlanner(); - const iteratorA = iterator(txPercent(100)); + const messagePackerA = messagePacker(txPercent(100)); t.deepEqual( - await planner(iteratorA), - singleTransactionPlan([iteratorA.get(txPercent(100), 0)]) + await planner(messagePackerA), + singleTransactionPlan([messagePackerA.get(txPercent(100), 0)]) ); }); @@ -1216,32 +1216,40 @@ test('it simplifies iterable instruction plans that fit in a single transaction' * ├── [B: 50%] ├── [Tx: B + C(2, 50%)] * └── [C(x, 125%)] └── [Tx: C(3, 50%)] */ -test('it uses iterable instruction plans to fill gaps in parallel candidates', async (t) => { +test('it uses message packer instruction plans to fill gaps in parallel candidates', async (t) => { const { createPlanner, txPercent, instruction, - iterator, + messagePacker, singleTransactionPlan, } = defaultFactories(); const planner = createPlanner(); const instructionA = instruction(txPercent(75)); const instructionB = instruction(txPercent(50)); - const iteratorC = iterator(txPercent(25) + txPercent(50) + txPercent(50)); // 125% + const messagePackerC = messagePacker( + txPercent(25) + txPercent(50) + txPercent(50) + ); // 125% t.deepEqual( await planner( parallelInstructionPlan([ singleInstructionPlan(instructionA), singleInstructionPlan(instructionB), - iteratorC, + messagePackerC, ]) ), parallelTransactionPlan([ - singleTransactionPlan([instructionA, iteratorC.get(txPercent(25), 0)]), - singleTransactionPlan([instructionB, iteratorC.get(txPercent(50), 1)]), - singleTransactionPlan([iteratorC.get(txPercent(50), 2)]), + singleTransactionPlan([ + instructionA, + messagePackerC.get(txPercent(25), 0), + ]), + singleTransactionPlan([ + instructionB, + messagePackerC.get(txPercent(50), 1), + ]), + singleTransactionPlan([messagePackerC.get(txPercent(50), 2)]), ]) ); }); @@ -1253,32 +1261,40 @@ test('it uses iterable instruction plans to fill gaps in parallel candidates', a * ├── [C: 50%] ├── [Tx: C + A(2, 50%)] * └── [B: 75%] └── [Tx: A(3, 50%)] */ -test('it handles parallel iterable instruction plans last to fill gaps in previous parallel candidates', async (t) => { +test('it handles parallel message packer instruction plans last to fill gaps in previous parallel candidates', async (t) => { const { createPlanner, txPercent, instruction, - iterator, + messagePacker, singleTransactionPlan, } = defaultFactories(); const planner = createPlanner(); - const iteratorA = iterator(txPercent(25) + txPercent(50) + txPercent(50)); // 125% + const messagePackerA = messagePacker( + txPercent(25) + txPercent(50) + txPercent(50) + ); // 125% const instructionB = instruction(txPercent(75)); const instructionC = instruction(txPercent(50)); t.deepEqual( await planner( parallelInstructionPlan([ - iteratorA, + messagePackerA, singleInstructionPlan(instructionB), singleInstructionPlan(instructionC), ]) ), parallelTransactionPlan([ - singleTransactionPlan([instructionB, iteratorA.get(txPercent(25), 0)]), - singleTransactionPlan([instructionC, iteratorA.get(txPercent(50), 1)]), - singleTransactionPlan([iteratorA.get(txPercent(50), 2)]), + singleTransactionPlan([ + instructionB, + messagePackerA.get(txPercent(25), 0), + ]), + singleTransactionPlan([ + instructionC, + messagePackerA.get(txPercent(50), 1), + ]), + singleTransactionPlan([messagePackerA.get(txPercent(50), 2)]), ]) ); }); @@ -1290,31 +1306,37 @@ test('it handles parallel iterable instruction plans last to fill gaps in previo * ├── [B(x, 75%)] └── [Tx: B(2, 50%) + C] * └── [C: 50%] */ -test('it uses iterable instruction plans to fill gaps in sequential candidates', async (t) => { +test('it uses message packer instruction plans to fill gaps in sequential candidates', async (t) => { const { createPlanner, txPercent, instruction, - iterator, + messagePacker, singleTransactionPlan, } = defaultFactories(); const planner = createPlanner(); const instructionA = instruction(txPercent(75)); - const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% + const messagePackerB = messagePacker(txPercent(25) + txPercent(50)); // 75% const instructionC = instruction(txPercent(50)); t.deepEqual( await planner( sequentialInstructionPlan([ singleInstructionPlan(instructionA), - iteratorB, + messagePackerB, singleInstructionPlan(instructionC), ]) ), sequentialTransactionPlan([ - singleTransactionPlan([instructionA, iteratorB.get(txPercent(25), 0)]), - singleTransactionPlan([iteratorB.get(txPercent(50), 1), instructionC]), + singleTransactionPlan([ + instructionA, + messagePackerB.get(txPercent(25), 0), + ]), + singleTransactionPlan([ + messagePackerB.get(txPercent(50), 1), + instructionC, + ]), ]) ); }); @@ -1326,31 +1348,37 @@ test('it uses iterable instruction plans to fill gaps in sequential candidates', * ├── [B(x, 75%)] └── [Tx: B(2, 50%) + C] * └── [C: 50%] */ -test('it uses iterable instruction plans to fill gaps in non-divisible sequential candidates', async (t) => { +test('it uses message packer instruction plans to fill gaps in non-divisible sequential candidates', async (t) => { const { createPlanner, txPercent, instruction, - iterator, + messagePacker, singleTransactionPlan, } = defaultFactories(); const planner = createPlanner(); const instructionA = instruction(txPercent(75)); - const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% + const messagePackerB = messagePacker(txPercent(25) + txPercent(50)); // 75% const instructionC = instruction(txPercent(50)); t.deepEqual( await planner( nonDivisibleSequentialInstructionPlan([ singleInstructionPlan(instructionA), - iteratorB, + messagePackerB, singleInstructionPlan(instructionC), ]) ), nonDivisibleSequentialTransactionPlan([ - singleTransactionPlan([instructionA, iteratorB.get(txPercent(25), 0)]), - singleTransactionPlan([iteratorB.get(txPercent(50), 1), instructionC]), + singleTransactionPlan([ + instructionA, + messagePackerB.get(txPercent(25), 0), + ]), + singleTransactionPlan([ + messagePackerB.get(txPercent(50), 1), + instructionC, + ]), ]) ); }); @@ -1363,18 +1391,18 @@ test('it uses iterable instruction plans to fill gaps in non-divisible sequentia * ├── [B(x, 75%)] * └── [C: 50%] */ -test('it uses parallel iterable instruction plans to fill gaps in sequential candidates', async (t) => { +test('it uses parallel message packer instruction plans to fill gaps in sequential candidates', async (t) => { const { createPlanner, txPercent, instruction, - iterator, + messagePacker, singleTransactionPlan, } = defaultFactories(); const planner = createPlanner(); const instructionA = instruction(txPercent(75)); - const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% + const messagePackerB = messagePacker(txPercent(25) + txPercent(50)); // 75% const instructionC = instruction(txPercent(50)); t.deepEqual( @@ -1382,14 +1410,20 @@ test('it uses parallel iterable instruction plans to fill gaps in sequential can sequentialInstructionPlan([ singleInstructionPlan(instructionA), parallelInstructionPlan([ - iteratorB, + messagePackerB, singleInstructionPlan(instructionC), ]), ]) ), sequentialTransactionPlan([ - singleTransactionPlan([instructionA, iteratorB.get(txPercent(25), 0)]), - singleTransactionPlan([instructionC, iteratorB.get(txPercent(50), 1)]), + singleTransactionPlan([ + instructionA, + messagePackerB.get(txPercent(25), 0), + ]), + singleTransactionPlan([ + instructionC, + messagePackerB.get(txPercent(50), 1), + ]), ]) ); }); @@ -1402,18 +1436,18 @@ test('it uses parallel iterable instruction plans to fill gaps in sequential can * ├── [B(x, 50%)] * └── [C: 25%] */ -test('it uses the whole sequential iterable instruction plan when it fits in the parent parallel candidate', async (t) => { +test('it uses the whole sequential message packer instruction plan when it fits in the parent parallel candidate', async (t) => { const { createPlanner, txPercent, instruction, - iterator, + messagePacker, singleTransactionPlan, } = defaultFactories(); const planner = createPlanner(); const instructionA = instruction(txPercent(25)); - const iteratorB = iterator(txPercent(50)); + const messagePackerB = messagePacker(txPercent(50)); const instructionC = instruction(txPercent(25)); t.deepEqual( @@ -1421,14 +1455,14 @@ test('it uses the whole sequential iterable instruction plan when it fits in the parallelInstructionPlan([ singleInstructionPlan(instructionA), sequentialInstructionPlan([ - iteratorB, + messagePackerB, singleInstructionPlan(instructionC), ]), ]) ), singleTransactionPlan([ instructionA, - iteratorB.get(txPercent(50), 0), + messagePackerB.get(txPercent(50), 0), instructionC, ]) ); @@ -1442,18 +1476,18 @@ test('it uses the whole sequential iterable instruction plan when it fits in the * ├── [B(x, 50%)] * └── [C: 25%] */ -test('it uses the whole non-divisible sequential iterable instruction plan when it fits in the parent sequential candidate', async (t) => { +test('it uses the whole non-divisible sequential message packer instruction plan when it fits in the parent sequential candidate', async (t) => { const { createPlanner, txPercent, instruction, - iterator, + messagePacker, singleTransactionPlan, } = defaultFactories(); const planner = createPlanner(); const instructionA = instruction(txPercent(25)); - const iteratorB = iterator(txPercent(50)); + const messagePackerB = messagePacker(txPercent(50)); const instructionC = instruction(txPercent(25)); t.deepEqual( @@ -1461,14 +1495,14 @@ test('it uses the whole non-divisible sequential iterable instruction plan when sequentialInstructionPlan([ singleInstructionPlan(instructionA), nonDivisibleSequentialInstructionPlan([ - iteratorB, + messagePackerB, singleInstructionPlan(instructionC), ]), ]) ), singleTransactionPlan([ instructionA, - iteratorB.get(txPercent(50), 0), + messagePackerB.get(txPercent(50), 0), instructionC, ]) ); @@ -1554,7 +1588,7 @@ test('complex example 2', async (t) => { const { createPlanner, instruction, - iterator, + messagePacker, txPercent, singleTransactionPlan, } = defaultFactories(); @@ -1564,7 +1598,7 @@ test('complex example 2', async (t) => { const instructionB = instruction(txPercent(20)); const instructionC = instruction(txPercent(20)); const instructionD = instruction(txPercent(50)); - const iteratorE = iterator(txPercent(250)); + const messagePackerE = messagePacker(txPercent(250)); const instructionF = instruction(txPercent(50)); const instructionG = instruction(txPercent(50)); @@ -1578,7 +1612,7 @@ test('complex example 2', async (t) => { ]), parallelInstructionPlan([ singleInstructionPlan(instructionD), - iteratorE, + messagePackerE, ]), singleInstructionPlan(instructionF), singleInstructionPlan(instructionG), @@ -1589,12 +1623,15 @@ test('complex example 2', async (t) => { instructionA, instructionB, instructionC, - iteratorE.get(txPercent(40) - 3, 0), + messagePackerE.get(txPercent(40) - 3, 0), ]), parallelTransactionPlan([ - singleTransactionPlan([instructionD, iteratorE.get(txPercent(50), 1)]), - singleTransactionPlan([iteratorE.get(txPercent(100), 2)]), - singleTransactionPlan([iteratorE.get(txPercent(60) + 3, 3)]), + singleTransactionPlan([ + instructionD, + messagePackerE.get(txPercent(50), 1), + ]), + singleTransactionPlan([messagePackerE.get(txPercent(100), 2)]), + singleTransactionPlan([messagePackerE.get(txPercent(60) + 3, 3)]), ]), singleTransactionPlan([instructionF, instructionG]), ]) From 8c547ef3af340a6373bc584f1128613f2e842211 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 24 Jun 2025 10:07:35 +0100 Subject: [PATCH 09/10] Refactor errors --- .../src/instructionPlans/instructionPlan.ts | 31 +++++-- .../transactionPlannerBase.ts | 92 ++++++++++++------- .../_instructionPlanHelpers.ts | 4 +- .../transactionPlanner.test.ts | 27 ++++-- 4 files changed, 100 insertions(+), 54 deletions(-) diff --git a/clients/js/src/instructionPlans/instructionPlan.ts b/clients/js/src/instructionPlans/instructionPlan.ts index 01e2fa0..39e8c9a 100644 --- a/clients/js/src/instructionPlans/instructionPlan.ts +++ b/clients/js/src/instructionPlans/instructionPlan.ts @@ -47,10 +47,20 @@ export type MessagePacker = Readonly<{ }>; // TODO: Make SolanaError instead. -export class CannotIterateUsingProvidedMessageError extends Error { +export class CannotPackUsingProvidedMessageError extends Error { constructor() { - super('Cannot iterate the next instruction using the provided message'); - this.name = 'CannotIterateUsingProvidedMessageError'; + super('Cannot pack the next instructions using the provided message'); + this.name = 'CannotPackUsingProvidedMessageError'; + } +} + +// TODO: Make SolanaError instead. +export class MessagePackerIsAlreadyDoneError extends Error { + constructor() { + super( + 'Failed to pack the next message because the message packer is already done' + ); + this.name = 'MessagePackerIsAlreadyDoneError'; } } @@ -120,7 +130,7 @@ export function getLinearMessagePackerInstructionPlan({ 1; /* Leeway for shortU16 numbers in transaction headers. */ if (maxLength <= 0) { - throw new CannotIterateUsingProvidedMessageError(); + throw new CannotPackUsingProvidedMessageError(); } const length = Math.min(totalBytes - offset, maxLength); @@ -144,20 +154,21 @@ export function getMessagePackerInstructionPlanFromInstructions< done: () => instructionIndex < instructions.length, packMessage: (message: CompilableTransactionMessage) => { if (instructionIndex >= instructions.length) { - throw new CannotIterateUsingProvidedMessageError(); + throw new MessagePackerIsAlreadyDoneError(); } const instruction = instructions[instructionIndex]; - const transactionSize = getTransactionSize( - appendTransactionMessageInstruction(instruction, message) + const updatedMessage = appendTransactionMessageInstruction( + instruction, + message ); - if (transactionSize > TRANSACTION_SIZE_LIMIT) { - throw new CannotIterateUsingProvidedMessageError(); + if (getTransactionSize(updatedMessage) > TRANSACTION_SIZE_LIMIT) { + throw new CannotPackUsingProvidedMessageError(); } instructionIndex++; - return appendTransactionMessageInstruction(instruction, message); + return updatedMessage; }, }; }, diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index 5328185..bac613e 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -1,9 +1,10 @@ import { appendTransactionMessageInstructions, CompilableTransactionMessage, + TransactionMessage, } from '@solana/kit'; import { - CannotIterateUsingProvidedMessageError, + CannotPackUsingProvidedMessageError as CannotPackUsingProvidedMessageError, InstructionPlan, MessagePackerInstructionPlan, ParallelInstructionPlan, @@ -46,10 +47,10 @@ export function createBaseTransactionPlanner( config: TransactionPlannerConfig ): TransactionPlanner { return async ( - originalInstructionPlan, + instructionPlan, { abortSignal } = {} ): Promise => { - const plan = await traverse(originalInstructionPlan, { + const plan = await traverse(instructionPlan, { abortSignal, parent: null, parentCandidates: [], @@ -59,16 +60,7 @@ export function createBaseTransactionPlanner( }); if (!plan) { - throw new Error('No instructions were found in the instruction plan.'); - } - - if (!isValidTransactionPlan(plan)) { - // TODO: Coded error. - const error = new Error( - 'Instruction plan results in invalid transaction plan' - ) as Error & { plan: TransactionPlan }; - error.plan = plan; - throw error; + throw new NoInstructionsFoundInInstructionPlanError(); } return freezeTransactionPlan(plan); @@ -113,19 +105,28 @@ async function traverseSequential( context: TraverseContext ): Promise { let candidate: MutableSingleTransactionPlan | null = null; + + // Check if the sequential plan must fit entirely in its parent candidates + // due to constraints like being inside a parallel plan or not being divisible. const mustEntirelyFitInParentCandidate = context.parent && (context.parent.kind === 'parallel' || !instructionPlan.divisible); + + // If so, try to fit the entire plan inside one of the parent candidates. if (mustEntirelyFitInParentCandidate) { const candidate = await selectAndMutateCandidate( context, context.parentCandidates, (message) => fitEntirePlanInsideMessage(instructionPlan, message) ); + // If that's possible, we the candidate is mutated and we can return null. + // Otherwise, we proceed with the normal traversal and no parent candidate. if (candidate) { return null; } } else { + // Otherwise, we can use the first parent candidate, if any, + // since we know it must be a divisible sequential plan. candidate = context.parentCandidates.length > 0 ? context.parentCandidates[0] : null; } @@ -147,6 +148,8 @@ async function traverseSequential( transactionPlans.push(...newPlans); } } + + // Wrap in a sequential plan or simplify. if (transactionPlans.length === 1) { return transactionPlans[0]; } @@ -190,6 +193,8 @@ async function traverseParallel( transactionPlans.push(...newPlans); } } + + // Wrap in a parallel plan or simplify. if (transactionPlans.length === 1) { return transactionPlans[0]; } @@ -216,7 +221,7 @@ async function traverseSingle( if (candidate) { return null; } - const message = await createNewMessage(context, predicate); + const message = await createNewMessage(context, instructionPlan, predicate); return { kind: 'single', message }; } @@ -237,6 +242,7 @@ async function traverseMessagePacker( if (!candidate) { const message = await createNewMessage( context, + instructionPlan, messagePacker.packMessage ); const newPlan: MutableSingleTransactionPlan = { kind: 'single', message }; @@ -305,7 +311,7 @@ async function selectAndMutateCandidate( } } catch (error) { if ( - error instanceof CannotIterateUsingProvidedMessageError || + error instanceof CannotPackUsingProvidedMessageError || error instanceof CannotFitEntirePlanInsideMessageError ) { // Try the next candidate. @@ -322,18 +328,23 @@ async function createNewMessage( TraverseContext, 'createTransactionMessage' | 'onTransactionMessageUpdated' | 'abortSignal' >, + instructionPlan: InstructionPlan, predicate: ( message: CompilableTransactionMessage ) => CompilableTransactionMessage ): Promise { - const message = await Promise.resolve( + const newMessage = await Promise.resolve( context.createTransactionMessage({ abortSignal: context.abortSignal }) ); - return await Promise.resolve( - context.onTransactionMessageUpdated(predicate(message), { + const updatedMessage = await Promise.resolve( + context.onTransactionMessageUpdated(predicate(newMessage), { abortSignal: context.abortSignal, }) ); + if (getTransactionSize(updatedMessage) > TRANSACTION_SIZE_LIMIT) { + throw new FailedToFitPlanInNewMessageError(instructionPlan, updatedMessage); + } + return updatedMessage; } function freezeTransactionPlan(plan: MutableTransactionPlan): TransactionPlan { @@ -356,21 +367,6 @@ function freezeTransactionPlan(plan: MutableTransactionPlan): TransactionPlan { } } -function isValidTransactionPlan(transactionPlan: TransactionPlan): boolean { - if (transactionPlan.kind === 'single') { - const transactionSize = getTransactionSize(transactionPlan.message); - return transactionSize <= TRANSACTION_SIZE_LIMIT; - } - return transactionPlan.plans.every(isValidTransactionPlan); -} - -class CannotFitEntirePlanInsideMessageError extends Error { - constructor() { - super('Cannot fit the entire instruction plan inside the provided message'); - this.name = 'CannotFitEntirePlanInsideMessageError'; - } -} - function fitEntirePlanInsideMessage( instructionPlan: InstructionPlan, message: CompilableTransactionMessage @@ -404,7 +400,7 @@ function fitEntirePlanInsideMessage( throw new CannotFitEntirePlanInsideMessageError(); } } catch (error) { - if (error instanceof CannotIterateUsingProvidedMessageError) { + if (error instanceof CannotPackUsingProvidedMessageError) { throw new CannotFitEntirePlanInsideMessageError(); } throw error; @@ -418,3 +414,31 @@ function fitEntirePlanInsideMessage( ); } } + +// TODO: Below should be SolanaErrors. + +export class FailedToFitPlanInNewMessageError extends Error { + constructor( + public readonly instructionPlan: InstructionPlan, + public readonly transactionMessage: TransactionMessage + ) { + super( + `The provided instruction plan could not fit in a new transaction message.` + ); + this.name = 'FailedToFitPlanInNewMessageError'; + } +} + +class NoInstructionsFoundInInstructionPlanError extends Error { + constructor() { + super('No instructions were found in the provided instruction plan.'); + this.name = 'NoInstructionsFoundInInstructionPlanError'; + } +} + +class CannotFitEntirePlanInsideMessageError extends Error { + constructor() { + super('Cannot fit the entire instruction plan inside the provided message'); + this.name = 'CannotFitEntirePlanInsideMessageError'; + } +} diff --git a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts index 2a99c4d..f263ff1 100644 --- a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts @@ -8,7 +8,7 @@ import { IInstruction, } from '@solana/kit'; import { - CannotIterateUsingProvidedMessageError, + CannotPackUsingProvidedMessageError, getTransactionSize, MessagePackerInstructionPlan, TRANSACTION_SIZE_LIMIT, @@ -50,7 +50,7 @@ export function messagePackerFactory() { 1; /* Leeway for shortU16 numbers in transaction headers. */ if (maxLength <= 0) { - throw new CannotIterateUsingProvidedMessageError(); + throw new CannotPackUsingProvidedMessageError(); } const length = Math.min( diff --git a/clients/js/test/instructionPlans/transactionPlanner.test.ts b/clients/js/test/instructionPlans/transactionPlanner.test.ts index bc74eff..5ce9c55 100644 --- a/clients/js/test/instructionPlans/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlans/transactionPlanner.test.ts @@ -1,7 +1,11 @@ -import { CompilableTransactionMessage } from '@solana/kit'; +import { + appendTransactionMessageInstruction, + CompilableTransactionMessage, +} from '@solana/kit'; import test from 'ava'; import { createBaseTransactionPlanner, + FailedToFitPlanInNewMessageError, nonDivisibleSequentialInstructionPlan, nonDivisibleSequentialTransactionPlan, parallelInstructionPlan, @@ -9,7 +13,6 @@ import { sequentialInstructionPlan, sequentialTransactionPlan, singleInstructionPlan, - TransactionPlan, } from '../../src'; import { instructionFactory, @@ -60,17 +63,25 @@ test('it plans a single instruction', async (t) => { * [A: 200%] ───────────────────▶ Error */ test('it fail if a single instruction is too large', async (t) => { - const { createPlanner, instruction, txPercent, singleTransactionPlan } = - defaultFactories(); + const { createPlanner, instruction, txPercent } = defaultFactories(); const planner = createPlanner(); const instructionA = instruction(txPercent(200)); const promise = planner(singleInstructionPlan(instructionA)); - const error = (await t.throwsAsync(promise)) as Error & { - plan: TransactionPlan; - }; - t.deepEqual(error.plan, singleTransactionPlan([instructionA])); + const error = (await t.throwsAsync(promise, { + message: + 'The provided instruction plan could not fit in a new transaction message.', + })) as FailedToFitPlanInNewMessageError; + + t.deepEqual(error.instructionPlan, singleInstructionPlan(instructionA)); + t.deepEqual( + error.transactionMessage, + appendTransactionMessageInstruction( + instructionA, + getMockCreateTransactionMessage() + ) + ); }); /** From e82d42631f633fc5d51059bc16c91e1fffc1dd22 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 24 Jun 2025 11:32:46 +0100 Subject: [PATCH 10/10] Make the API pack messages to capacity Instead of relying on multiple `packMessage` calls. --- .../src/instructionPlans/instructionPlan.ts | 34 ++++++++++++------- .../transactionPlannerBase.ts | 10 ++---- .../_instructionPlanHelpers.ts | 2 +- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/clients/js/src/instructionPlans/instructionPlan.ts b/clients/js/src/instructionPlans/instructionPlan.ts index 39e8c9a..ea795cd 100644 --- a/clients/js/src/instructionPlans/instructionPlan.ts +++ b/clients/js/src/instructionPlans/instructionPlan.ts @@ -41,7 +41,7 @@ export type MessagePacker = Readonly<{ /** Checks whether there are more instructions to retrieve. */ done: () => boolean; /** Pack the provided transaction message with the next instructions or throws if not possible. */ - packMessage: ( + packMessageToCapacity: ( transactionMessage: CompilableTransactionMessage ) => CompilableTransactionMessage; }>; @@ -117,7 +117,7 @@ export function getLinearMessagePackerInstructionPlan({ let offset = 0; return { done: () => offset < totalBytes, - packMessage: (message: CompilableTransactionMessage) => { + packMessageToCapacity: (message: CompilableTransactionMessage) => { const baseTransactionSize = getTransactionSize( appendTransactionMessageInstruction( getInstruction(offset, 0), @@ -152,22 +152,32 @@ export function getMessagePackerInstructionPlanFromInstructions< let instructionIndex = 0; return { done: () => instructionIndex < instructions.length, - packMessage: (message: CompilableTransactionMessage) => { + packMessageToCapacity: (message: CompilableTransactionMessage) => { if (instructionIndex >= instructions.length) { throw new MessagePackerIsAlreadyDoneError(); } - const instruction = instructions[instructionIndex]; - const updatedMessage = appendTransactionMessageInstruction( - instruction, - message - ); - - if (getTransactionSize(updatedMessage) > TRANSACTION_SIZE_LIMIT) { - throw new CannotPackUsingProvidedMessageError(); + let updatedMessage: CompilableTransactionMessage = message; + for ( + let index = instructionIndex; + index < instructions.length; + index++ + ) { + updatedMessage = appendTransactionMessageInstruction( + instructions[index], + message + ); + + if (getTransactionSize(updatedMessage) > TRANSACTION_SIZE_LIMIT) { + if (index === instructionIndex) { + throw new CannotPackUsingProvidedMessageError(); + } + instructionIndex = index; + return updatedMessage; + } } - instructionIndex++; + instructionIndex = instructions.length; return updatedMessage; }, }; diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index bac613e..23f1d86 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -237,20 +237,16 @@ async function traverseMessagePacker( const candidate = await selectAndMutateCandidate( context, candidates, - messagePacker.packMessage + messagePacker.packMessageToCapacity ); if (!candidate) { const message = await createNewMessage( context, instructionPlan, - messagePacker.packMessage + messagePacker.packMessageToCapacity ); const newPlan: MutableSingleTransactionPlan = { kind: 'single', message }; transactionPlans.push(newPlan); - - // Adding the new plan to the candidates is important for cases - // where the next instruction doesn't fill the entire transaction. - candidates.push(newPlan); } } @@ -395,7 +391,7 @@ function fitEntirePlanInsideMessage( const messagePacker = instructionPlan.getMessagePacker(); while (messagePacker.done()) { try { - newMessage = messagePacker.packMessage(message); + newMessage = messagePacker.packMessageToCapacity(message); if (getTransactionSize(newMessage) > TRANSACTION_SIZE_LIMIT) { throw new CannotFitEntirePlanInsideMessageError(); } diff --git a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts index f263ff1..ca5a647 100644 --- a/clients/js/test/instructionPlans/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts @@ -40,7 +40,7 @@ export function messagePackerFactory() { let offset = 0; return { done: () => offset < totalBytes, - packMessage: (message) => { + packMessageToCapacity: (message) => { const baseTransactionSize = getTransactionSize( appendTransactionMessageInstruction(baseInstruction, message) );