From 10f0df91e9469aa5ab6eeb2fc9a81e7246d5f421 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 28 Mar 2025 10:25:25 +0000 Subject: [PATCH 001/112] Draft instruction plans --- clients/js/src/instructionPlansDraft/index.ts | 1 + .../instructionPlansDraft/instructionPlan.ts | 35 ++++++ .../instructionPlansDraft/transactionPlan.ts | 23 ++++ .../transactionPlanExecutor.ts | 102 ++++++++++++++++++ .../transactionPlanResult.ts | 36 +++++++ .../transactionPlanner.ts | 23 ++++ 6 files changed, 220 insertions(+) create mode 100644 clients/js/src/instructionPlansDraft/index.ts create mode 100644 clients/js/src/instructionPlansDraft/instructionPlan.ts create mode 100644 clients/js/src/instructionPlansDraft/transactionPlan.ts create mode 100644 clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts create mode 100644 clients/js/src/instructionPlansDraft/transactionPlanResult.ts create mode 100644 clients/js/src/instructionPlansDraft/transactionPlanner.ts diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts new file mode 100644 index 0000000..0df71b8 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -0,0 +1 @@ +export * from './instructionPlan'; diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts new file mode 100644 index 0000000..9319331 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -0,0 +1,35 @@ +import { IInstruction } from '@solana/kit'; + +export type InstructionPlan = + | SequentialInstructionPlan + | ParallelInstructionPlan + | StaticInstructionPlan + | DynamicInstructionPlan; + +export type SequentialInstructionPlan = Readonly<{ + kind: 'sequential'; + plans: InstructionPlan[]; + divisible: boolean; +}>; + +export type ParallelInstructionPlan = Readonly<{ + kind: 'parallel'; + plans: InstructionPlan[]; +}>; + +export type StaticInstructionPlan< + TInstruction extends IInstruction = IInstruction, +> = Readonly<{ + kind: 'static'; + instruction: TInstruction; +}>; + +export type DynamicInstructionPlan< + TInstruction extends IInstruction = IInstruction, +> = Readonly<{ + kind: 'dynamic'; + instructionFactory: (bytesAvailable: number) => { + instruction: TInstruction | null; + hasMore: boolean; + }; +}>; diff --git a/clients/js/src/instructionPlansDraft/transactionPlan.ts b/clients/js/src/instructionPlansDraft/transactionPlan.ts new file mode 100644 index 0000000..5885daf --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlan.ts @@ -0,0 +1,23 @@ +import { BaseTransactionMessage } from '@solana/kit'; + +export type TransactionPlan = + | SequentialTransactionPlan + | ParallelTransactionPlan + | StaticTransactionPlan; + +export type SequentialTransactionPlan = Readonly<{ + kind: 'sequential'; + plans: TransactionPlan[]; +}>; + +export type ParallelTransactionPlan = Readonly<{ + kind: 'parallel'; + plans: TransactionPlan[]; +}>; + +export type StaticTransactionPlan< + TTransactionMessage extends BaseTransactionMessage = BaseTransactionMessage, +> = Readonly<{ + kind: 'static'; + message: TTransactionMessage; +}>; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts new file mode 100644 index 0000000..97f82e0 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -0,0 +1,102 @@ +import { + Blockhash, + GetLatestBlockhashApi, + isSolanaError, + pipe, + Rpc, + RpcSubscriptions, + Signature, + SolanaRpcSubscriptionsApi, +} from '@solana/kit'; +import { StaticTransactionPlan, TransactionPlan } from './transactionPlan'; +import { TransactionPlanResult } from './transactionPlanResult'; + +export type TransactionPlanExecutor = ( + transactionPlan: TransactionPlan +) => Promise>; + +export function getDefaultTransactionPlanExecutor(options: { + rpc: Rpc; + rpcSubscriptions: RpcSubscriptions; // TODO: narrow +}): TransactionPlanExecutor { + const { rpc } = options; + + // TODO: implement + // - Refetch blockhash if it's expired + // - Retry on failure + // - Chunk parallel transactions + // - Handle cancellation (i.e. don't continue past a failing sequential plan) + + const executor: TransactionPlanExecutor = async (plan) => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return { + context: null, + kind: 'static', + message: (plan as StaticTransactionPlan).message, + signature: 'signature' as Signature, + status: { kind: 'success' }, + }; + }; + + return pipe( + executor, + (ex) => refreshBlockheightTransactionPlanExecutor(rpc, ex), + (ex) => retryTransactionPlanExecutor(5, ex) + ); +} + +export function refreshBlockheightTransactionPlanExecutor( + rpc: Rpc, + executor: TransactionPlanExecutor +): TransactionPlanExecutor { + let latestBlockhash: { + blockhash: Blockhash; + lastValidBlockHeight: bigint; + } | null = null; + return async (transactionPlan) => { + if (transactionPlan.kind !== 'static') { + return await executor(transactionPlan); + } + + if (latestBlockhash) { + // Replace the blockhash in the message + } + try { + return await executor(transactionPlan); + } catch (error) { + if (isSolanaError(error)) { + // TODO: Retry on blockhash expired error + const result = await rpc.getLatestBlockhash().send(); + latestBlockhash = result.value; + return await executor(transactionPlan); + } else { + throw error; + } + } + }; +} + +export function retryTransactionPlanExecutor( + maxRetries: number, + executor: TransactionPlanExecutor +): TransactionPlanExecutor { + return async (transactionPlan) => { + if (transactionPlan.kind !== 'static') { + return await executor(transactionPlan); + } + + let retries = 0; + let lastError: Error | null = null; + + while (retries < maxRetries) { + try { + return await executor(transactionPlan); + } catch (error) { + retries++; + lastError = error as Error; + } + } + + throw lastError; + }; +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts new file mode 100644 index 0000000..30305c4 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts @@ -0,0 +1,36 @@ +import { BaseTransactionMessage, Signature, SolanaError } from '@solana/kit'; + +export type TransactionPlanResult = + | SequentialTransactionPlanResult + | ParallelTransactionPlanResult + | StaticTransactionPlanResult; + +export type SequentialTransactionPlanResult< + TContext extends object | null = null, +> = Readonly<{ + kind: 'sequential'; + plans: TransactionPlanResult[]; +}>; + +export type ParallelTransactionPlanResult< + TContext extends object | null = null, +> = Readonly<{ + kind: 'parallel'; + plans: TransactionPlanResult[]; +}>; + +export type StaticTransactionPlanResult< + TContext extends object | null = null, + TTransactionMessage extends BaseTransactionMessage = BaseTransactionMessage, +> = Readonly<{ + context: TContext; + kind: 'static'; + message: TTransactionMessage; + signature: Signature; + status: TransactionPlanResultStatus; +}>; + +export type TransactionPlanResultStatus = + | { kind: 'success' } + | { kind: 'error'; error: SolanaError } + | { kind: 'canceled' }; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts new file mode 100644 index 0000000..5695367 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -0,0 +1,23 @@ +import { InstructionPlan } from './instructionPlan'; +import { TransactionPlan } from './transactionPlan'; + +export type TransactionPlanner = ( + instructionPlan: InstructionPlan +) => Promise; + +export function getDefaultTransactionPlanner(): TransactionPlanner { + // TODO: Implement + // - Ask for additional instructions for each message. Maybe `getDefaultMessage` or `messageModifier` functions? + // - Add Compute Unit instructions. + // - Split instruction by sizes. + // - Provide remaining bytes to dynamic instructions. + // - Pack transaction messages as much as possible. + // - simulate CU. + return async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return { + kind: 'static', + message: { version: 0, instructions: [] }, + }; + }; +} From 16f8192cda6c314a6e5471a3c784e0abf60795e3 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 08:37:13 +0100 Subject: [PATCH 002/112] wip --- .../instructionPlanExecutor.ts | 4 +- .../instructionPlansDraft/instructionPlan.ts | 6 +- .../instructionPlansDraft/transactionPlan.ts | 7 +- .../transactionPlanExecutor.ts | 10 +- .../transactionPlanResult.ts | 6 +- .../transactionPlanner.ts | 130 +++++++++++++++++- 6 files changed, 141 insertions(+), 22 deletions(-) diff --git a/clients/js/src/instructionPlans/instructionPlanExecutor.ts b/clients/js/src/instructionPlans/instructionPlanExecutor.ts index 5291c60..1a261c1 100644 --- a/clients/js/src/instructionPlans/instructionPlanExecutor.ts +++ b/clients/js/src/instructionPlans/instructionPlanExecutor.ts @@ -37,7 +37,9 @@ export function chunkParallelInstructionPlans( executor: InstructionPlanExecutor, chunkSize: number ): InstructionPlanExecutor { - const chunkPlan = (plan: ParallelInstructionPlan) => { + const chunkPlan = ( + plan: ParallelInstructionPlan + ): ParallelInstructionPlan[] => { return plan.plans .reduce( (chunks, subPlan) => { diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 9319331..06452a3 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -3,7 +3,7 @@ import { IInstruction } from '@solana/kit'; export type InstructionPlan = | SequentialInstructionPlan | ParallelInstructionPlan - | StaticInstructionPlan + | SingleInstructionPlan | DynamicInstructionPlan; export type SequentialInstructionPlan = Readonly<{ @@ -17,10 +17,10 @@ export type ParallelInstructionPlan = Readonly<{ plans: InstructionPlan[]; }>; -export type StaticInstructionPlan< +export type SingleInstructionPlan< TInstruction extends IInstruction = IInstruction, > = Readonly<{ - kind: 'static'; + kind: 'single'; instruction: TInstruction; }>; diff --git a/clients/js/src/instructionPlansDraft/transactionPlan.ts b/clients/js/src/instructionPlansDraft/transactionPlan.ts index 5885daf..a2e1ebb 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlan.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlan.ts @@ -3,11 +3,12 @@ import { BaseTransactionMessage } from '@solana/kit'; export type TransactionPlan = | SequentialTransactionPlan | ParallelTransactionPlan - | StaticTransactionPlan; + | SingleTransactionPlan; export type SequentialTransactionPlan = Readonly<{ kind: 'sequential'; plans: TransactionPlan[]; + divisible: boolean; }>; export type ParallelTransactionPlan = Readonly<{ @@ -15,9 +16,9 @@ export type ParallelTransactionPlan = Readonly<{ plans: TransactionPlan[]; }>; -export type StaticTransactionPlan< +export type SingleTransactionPlan< TTransactionMessage extends BaseTransactionMessage = BaseTransactionMessage, > = Readonly<{ - kind: 'static'; + kind: 'single'; message: TTransactionMessage; }>; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index 97f82e0..42a2377 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -8,7 +8,7 @@ import { Signature, SolanaRpcSubscriptionsApi, } from '@solana/kit'; -import { StaticTransactionPlan, TransactionPlan } from './transactionPlan'; +import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; import { TransactionPlanResult } from './transactionPlanResult'; export type TransactionPlanExecutor = ( @@ -31,8 +31,8 @@ export function getDefaultTransactionPlanExecutor(options: { await new Promise((resolve) => setTimeout(resolve, 500)); return { context: null, - kind: 'static', - message: (plan as StaticTransactionPlan).message, + kind: 'single', + message: (plan as SingleTransactionPlan).message, signature: 'signature' as Signature, status: { kind: 'success' }, }; @@ -54,7 +54,7 @@ export function refreshBlockheightTransactionPlanExecutor( lastValidBlockHeight: bigint; } | null = null; return async (transactionPlan) => { - if (transactionPlan.kind !== 'static') { + if (transactionPlan.kind !== 'single') { return await executor(transactionPlan); } @@ -81,7 +81,7 @@ export function retryTransactionPlanExecutor( executor: TransactionPlanExecutor ): TransactionPlanExecutor { return async (transactionPlan) => { - if (transactionPlan.kind !== 'static') { + if (transactionPlan.kind !== 'single') { return await executor(transactionPlan); } diff --git a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts index 30305c4..a0e0342 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts @@ -3,7 +3,7 @@ import { BaseTransactionMessage, Signature, SolanaError } from '@solana/kit'; export type TransactionPlanResult = | SequentialTransactionPlanResult | ParallelTransactionPlanResult - | StaticTransactionPlanResult; + | SingleTransactionPlanResult; export type SequentialTransactionPlanResult< TContext extends object | null = null, @@ -19,12 +19,12 @@ export type ParallelTransactionPlanResult< plans: TransactionPlanResult[]; }>; -export type StaticTransactionPlanResult< +export type SingleTransactionPlanResult< TContext extends object | null = null, TTransactionMessage extends BaseTransactionMessage = BaseTransactionMessage, > = Readonly<{ context: TContext; - kind: 'static'; + kind: 'single'; message: TTransactionMessage; signature: Signature; status: TransactionPlanResultStatus; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 5695367..9bac095 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -1,11 +1,46 @@ +import { + Address, + appendTransactionMessageInstruction, + BaseTransactionMessage, + Blockhash, + CompilableTransactionMessage, + compileTransaction, + createTransactionMessage, + getTransactionEncoder, + ITransactionMessageWithFeePayer, + pipe, + setTransactionMessageFeePayer, + setTransactionMessageLifetimeUsingBlockhash, + TransactionMessageWithBlockhashLifetime, + TransactionVersion, +} from '@solana/kit'; import { InstructionPlan } from './instructionPlan'; import { TransactionPlan } from './transactionPlan'; +const TRANSACTION_SIZE_LIMIT = + 1_280 - + 40 /* 40 bytes is the size of the IPv6 header. */ - + 8; /* 8 bytes is the size of the fragment header. */ + +export type TransactionPlannerConfig = { + preTransformer?: ( + transactionMessage: TTransactionMessage + ) => Promise; + postTransformer?: ( + transactionMessage: TTransactionMessage + ) => Promise; +}; + export type TransactionPlanner = ( - instructionPlan: InstructionPlan + instructionPlan: InstructionPlan, + config?: TransactionPlannerConfig ) => Promise; -export function getDefaultTransactionPlanner(): TransactionPlanner { +export function createBaseTransactionPlanner({ + version, +}: { + version: TransactionVersion; +}): TransactionPlanner { // TODO: Implement // - Ask for additional instructions for each message. Maybe `getDefaultMessage` or `messageModifier` functions? // - Add Compute Unit instructions. @@ -13,11 +48,92 @@ export function getDefaultTransactionPlanner(): TransactionPlanner { // - Provide remaining bytes to dynamic instructions. // - Pack transaction messages as much as possible. // - simulate CU. - return async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - return { - kind: 'static', - message: { version: 0, instructions: [] }, + return async (instructionPlan, config) => { + // Use when starting a new transaction message. + const startTransactionMessage = async < + TTransactionMessage extends BaseTransactionMessage, + >(): Promise => { + const transactionMessage = createTransactionMessage({ + version, + }) as TTransactionMessage; + return config?.preTransformer + ? await config.preTransformer(transactionMessage) + : transactionMessage; + }; + + // Use once the transaction message is fully built. + // const finishTransactionMessage = async < + // TTransactionMessage extends BaseTransactionMessage, + // >( + // transactionMessage: TTransactionMessage + // ) => { + // return config?.postTransformer + // ? await config.postTransformer(transactionMessage) + // : transactionMessage; + // }; + + // State. + let finalPlan: TransactionPlan | undefined; + // let parentPlan: TransactionPlan | undefined; + let currentTransaction: BaseTransactionMessage | undefined; + // let remainingTransactionSize: number = 0; + + // Loop. + const loop = async (plan: InstructionPlan): Promise => { + if (!currentTransaction) { + currentTransaction = await startTransactionMessage(); + // remainingTransactionSize = getRemainingTransactionSize(currentTransaction); + } + switch (plan.kind) { + case 'sequential': + break; + case 'parallel': + break; + case 'dynamic': + break; + default: + case 'single': + currentTransaction = appendTransactionMessageInstruction( + plan.instruction, + currentTransaction + ); + } }; + + await loop(instructionPlan); + return finalPlan as TransactionPlan; + }; +} + +export function getRemainingTransactionSize(message: BaseTransactionMessage) { + return ( + TRANSACTION_SIZE_LIMIT - + getTransactionSize(message) - + 1 /* Subtract 1 byte buffer to account for shortvec encoding. */ + ); +} + +function getTransactionSize( + message: BaseTransactionMessage & Partial +): number { + const mockFeePayer = '11111111111111111111111111111111' as Address; + const mockBlockhash = { + blockhash: '11111111111111111111111111111111' as Blockhash, + lastValidBlockHeight: 0n, }; + const transaction = pipe( + message, + (tx) => { + return tx.feePayer + ? (tx as typeof tx & ITransactionMessageWithFeePayer) + : setTransactionMessageFeePayer(mockFeePayer, tx); + }, + (tx) => { + return tx.lifetimeConstraint + ? (tx as typeof tx & TransactionMessageWithBlockhashLifetime) + : setTransactionMessageLifetimeUsingBlockhash(mockBlockhash, tx); + }, + (tx) => compileTransaction(tx) + ); + return getTransactionEncoder().encode(transaction).length; } From c1216dc89a8c411b6ac0481585459b829db2fd84 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 13:30:49 +0100 Subject: [PATCH 003/112] wip --- clients/js/src/instructionPlansDraft/transactionPlanner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 9bac095..9fa7154 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -135,5 +135,5 @@ function getTransactionSize( }, (tx) => compileTransaction(tx) ); - return getTransactionEncoder().encode(transaction).length; + return getTransactionEncoder().getSizeFromValue(transaction); } From 9eb4660d4f28e4c49e9a473d2514b87ed82a0371 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 15:43:20 +0100 Subject: [PATCH 004/112] wip --- .../transactionPlanner.ts | 236 +++++++++++++----- 1 file changed, 176 insertions(+), 60 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 9fa7154..2d06c10 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -1,6 +1,6 @@ import { Address, - appendTransactionMessageInstruction, + appendTransactionMessageInstructions, BaseTransactionMessage, Blockhash, CompilableTransactionMessage, @@ -14,19 +14,25 @@ import { TransactionMessageWithBlockhashLifetime, TransactionVersion, } from '@solana/kit'; -import { InstructionPlan } from './instructionPlan'; -import { TransactionPlan } from './transactionPlan'; +import { InstructionPlan, SingleInstructionPlan } from './instructionPlan'; +import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; const TRANSACTION_SIZE_LIMIT = 1_280 - 40 /* 40 bytes is the size of the IPv6 header. */ - 8; /* 8 bytes is the size of the fragment header. */ +type Mutable = { -readonly [P in keyof T]: T[P] }; + export type TransactionPlannerConfig = { - preTransformer?: ( + newTransactionTransformer?: < + TTransactionMessage extends BaseTransactionMessage, + >( transactionMessage: TTransactionMessage ) => Promise; - postTransformer?: ( + newInstructionsTransformer?: < + TTransactionMessage extends BaseTransactionMessage, + >( transactionMessage: TTransactionMessage ) => Promise; }; @@ -36,76 +42,186 @@ export type TransactionPlanner = ( config?: TransactionPlannerConfig ) => Promise; +// TODO: Implement +// - Ask for additional instructions for each message. Maybe `getDefaultMessage` or `messageModifier` functions? +// - Add Compute Unit instructions. +// - Split instruction by sizes. +// - Provide remaining bytes to dynamic instructions. +// - Pack transaction messages as much as possible. +// - simulate CU. export function createBaseTransactionPlanner({ version, }: { version: TransactionVersion; }): TransactionPlanner { - // TODO: Implement - // - Ask for additional instructions for each message. Maybe `getDefaultMessage` or `messageModifier` functions? - // - Add Compute Unit instructions. - // - Split instruction by sizes. - // - Provide remaining bytes to dynamic instructions. - // - Pack transaction messages as much as possible. - // - simulate CU. - return async (instructionPlan, config) => { - // Use when starting a new transaction message. - const startTransactionMessage = async < - TTransactionMessage extends BaseTransactionMessage, - >(): Promise => { - const transactionMessage = createTransactionMessage({ - version, - }) as TTransactionMessage; - return config?.preTransformer - ? await config.preTransformer(transactionMessage) - : transactionMessage; + return async (originalInstructionPlan, config): Promise => { + const createSingleTransactionPlan = async ( + instructions: SingleInstructionPlan[] = [] + ): Promise => { + const plan: SingleTransactionPlan = { + kind: 'single', + message: createTransactionMessage({ version }), + }; + if (config?.newTransactionTransformer) { + (plan as Mutable).message = + await config.newTransactionTransformer(plan.message); + } + if (instructions.length > 0) { + await addInstructionsToSingleTransactionPlan(plan, instructions); + } + return plan; }; - // Use once the transaction message is fully built. - // const finishTransactionMessage = async < - // TTransactionMessage extends BaseTransactionMessage, - // >( - // transactionMessage: TTransactionMessage - // ) => { - // return config?.postTransformer - // ? await config.postTransformer(transactionMessage) - // : transactionMessage; - // }; - - // State. - let finalPlan: TransactionPlan | undefined; - // let parentPlan: TransactionPlan | undefined; - let currentTransaction: BaseTransactionMessage | undefined; - // let remainingTransactionSize: number = 0; - - // Loop. - const loop = async (plan: InstructionPlan): Promise => { - if (!currentTransaction) { - currentTransaction = await startTransactionMessage(); - // remainingTransactionSize = getRemainingTransactionSize(currentTransaction); + const addInstructionsToSingleTransactionPlan = async ( + plan: SingleTransactionPlan, + instructions: SingleInstructionPlan[] + ): Promise => { + let message = appendTransactionMessageInstructions( + instructions.map((i) => i.instruction), + plan.message + ); + if (config?.newInstructionsTransformer) { + message = await config.newInstructionsTransformer(plan.message); } - switch (plan.kind) { - case 'sequential': - break; - case 'parallel': - break; - case 'dynamic': - break; - default: - case 'single': - currentTransaction = appendTransactionMessageInstruction( - plan.instruction, - currentTransaction + (plan as Mutable).message = message; + }; + + // Recursive function that traverses the instruction plan and constructs the transaction plan. + const traverse = async ( + instructionPlan: InstructionPlan, + candidates: SingleTransactionPlan[] = [] + ): Promise => { + if (instructionPlan.kind === 'sequential' && !instructionPlan.divisible) { + throw new Error( + 'Non-divisible sequential plans are not supported yet.' + ); + } + + if (instructionPlan.kind === 'sequential') { + let candidate: SingleTransactionPlan | null = null; + const transactionPlans: TransactionPlan[] = []; + for (const plan of instructionPlan.plans) { + const transactionPlan = await traverse( + plan, + candidate ? [candidate] : [] ); + if (transactionPlan) { + transactionPlans.push(transactionPlan); + candidate = getSequentialCandidate(transactionPlan); + } + } + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length > 0) { + return null; + } + return { kind: 'sequential', divisible: true, plans: transactionPlans }; } + + if (instructionPlan.kind === 'parallel') { + const candidates: SingleTransactionPlan[] = []; + const transactionPlans: TransactionPlan[] = []; + for (const plan of instructionPlan.plans) { + const transactionPlan = await traverse(plan, candidates); + if (transactionPlan) { + transactionPlans.push(transactionPlan); + candidates.push(...getParallelCandidates(transactionPlan)); + } + } + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length > 0) { + return null; + } + return { kind: 'parallel', plans: transactionPlans }; + } + + if (instructionPlan.kind === 'dynamic') { + throw new Error('Dynamic plans are not supported yet.'); + } + + if (instructionPlan.kind === 'single') { + const candidate = selectCandidate(candidates, [instructionPlan]); + if (candidate) { + await addInstructionsToSingleTransactionPlan(candidate, [ + instructionPlan, + ]); + return null; + } + return await createSingleTransactionPlan([instructionPlan]); + } + + instructionPlan satisfies never; + throw new Error( + `Unknown instruction plan kind: ${(instructionPlan as { kind: string }).kind}` + ); }; - await loop(instructionPlan); - return finalPlan as TransactionPlan; + const plan = await traverse(originalInstructionPlan); + if (!plan) { + throw new Error('Transaction plan is null.'); // Should never happen. + } + + return plan; }; } -export function getRemainingTransactionSize(message: BaseTransactionMessage) { +function getSequentialCandidate( + latestPlan: TransactionPlan +): SingleTransactionPlan | null { + if (latestPlan.kind === 'single') { + return latestPlan; + } + if (latestPlan.kind === 'sequential' && latestPlan.plans.length > 0) { + return getSequentialCandidate( + latestPlan.plans[latestPlan.plans.length - 1] + ); + } + return null; +} + +function getParallelCandidates( + latestPlan: TransactionPlan +): SingleTransactionPlan[] { + return getAllSingleTransactionPlans(latestPlan); +} + +function getAllSingleTransactionPlans( + transactionPlan: TransactionPlan +): SingleTransactionPlan[] { + if (transactionPlan.kind === 'sequential') { + return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); + } + if (transactionPlan.kind === 'parallel') { + return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); + } + return [transactionPlan]; +} + +function selectCandidate( + candidates: SingleTransactionPlan[], + instructionPlans: SingleInstructionPlan[] +): SingleTransactionPlan | null { + const firstValidCandidate = candidates.find((candidate) => + isValidCandidate(candidate, instructionPlans) + ); + return firstValidCandidate ?? null; +} + +function isValidCandidate( + candidate: SingleTransactionPlan, + instructionPlans: SingleInstructionPlan[] +): boolean { + const message = appendTransactionMessageInstructions( + instructionPlans.map((i) => i.instruction), + candidate.message + ); + return getRemainingTransactionSize(message) >= 0; +} + +function getRemainingTransactionSize(message: BaseTransactionMessage) { return ( TRANSACTION_SIZE_LIMIT - getTransactionSize(message) - From 7152c2c683a759b7501e02b634e8801dd6fdbedc Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 15:56:47 +0100 Subject: [PATCH 005/112] wip --- .../transactionPlanner.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 2d06c10..e92dce8 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -89,7 +89,7 @@ export function createBaseTransactionPlanner({ // Recursive function that traverses the instruction plan and constructs the transaction plan. const traverse = async ( instructionPlan: InstructionPlan, - candidates: SingleTransactionPlan[] = [] + parentCandidates: SingleTransactionPlan[] = [] ): Promise => { if (instructionPlan.kind === 'sequential' && !instructionPlan.divisible) { throw new Error( @@ -98,7 +98,21 @@ export function createBaseTransactionPlanner({ } if (instructionPlan.kind === 'sequential') { - let candidate: SingleTransactionPlan | null = null; + // TODO: This is true if the parent is parallel. We don't need to be as strict with sequential parents. + // TODO: But we may need to be just as strict with non-divisible sequential parents. + const allInstructions = getAllSingleInstructionPlans(instructionPlan); + let candidate: SingleTransactionPlan | null = selectCandidate( + parentCandidates, + allInstructions + ); + if (candidate) { + // The whole branch can be added to the candidate. + await addInstructionsToSingleTransactionPlan( + candidate, + allInstructions + ); + return null; + } const transactionPlans: TransactionPlan[] = []; for (const plan of instructionPlan.plans) { const transactionPlan = await traverse( @@ -120,7 +134,7 @@ export function createBaseTransactionPlanner({ } if (instructionPlan.kind === 'parallel') { - const candidates: SingleTransactionPlan[] = []; + const candidates: SingleTransactionPlan[] = [...parentCandidates]; const transactionPlans: TransactionPlan[] = []; for (const plan of instructionPlan.plans) { const transactionPlan = await traverse(plan, candidates); @@ -143,7 +157,7 @@ export function createBaseTransactionPlanner({ } if (instructionPlan.kind === 'single') { - const candidate = selectCandidate(candidates, [instructionPlan]); + const candidate = selectCandidate(parentCandidates, [instructionPlan]); if (candidate) { await addInstructionsToSingleTransactionPlan(candidate, [ instructionPlan, @@ -200,6 +214,22 @@ function getAllSingleTransactionPlans( return [transactionPlan]; } +// TODO: This will need tweaking when adding support for dynamic instructions. +function getAllSingleInstructionPlans( + instructionPlan: InstructionPlan +): SingleInstructionPlan[] { + if (instructionPlan.kind === 'sequential') { + return instructionPlan.plans.flatMap(getAllSingleInstructionPlans); + } + if (instructionPlan.kind === 'parallel') { + return instructionPlan.plans.flatMap(getAllSingleInstructionPlans); + } + if (instructionPlan.kind === 'dynamic') { + throw new Error('Dynamic plans are not supported yet.'); + } + return [instructionPlan]; +} + function selectCandidate( candidates: SingleTransactionPlan[], instructionPlans: SingleInstructionPlan[] From ab649fd455b49215e5ca8aa83f046e85f809eab4 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 16:12:37 +0100 Subject: [PATCH 006/112] wip --- .../transactionPlanner.ts | 186 ++++++++++-------- 1 file changed, 104 insertions(+), 82 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index e92dce8..950feaa 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -86,100 +86,122 @@ export function createBaseTransactionPlanner({ (plan as Mutable).message = message; }; - // Recursive function that traverses the instruction plan and constructs the transaction plan. - const traverse = async ( - instructionPlan: InstructionPlan, - parentCandidates: SingleTransactionPlan[] = [] - ): Promise => { - if (instructionPlan.kind === 'sequential' && !instructionPlan.divisible) { - throw new Error( - 'Non-divisible sequential plans are not supported yet.' - ); - } + const plan = await traverse(originalInstructionPlan, { + parent: null, + parentCandidates: [], + createSingleTransactionPlan, + addInstructionsToSingleTransactionPlan, + }); + + if (!plan) { + throw new Error('Transaction plan is null.'); // Should never happen. + } + + return plan; + }; +} - if (instructionPlan.kind === 'sequential') { - // TODO: This is true if the parent is parallel. We don't need to be as strict with sequential parents. - // TODO: But we may need to be just as strict with non-divisible sequential parents. - const allInstructions = getAllSingleInstructionPlans(instructionPlan); - let candidate: SingleTransactionPlan | null = selectCandidate( - parentCandidates, +// Recursive function that traverses the instruction plan and constructs the transaction plan. +async function traverse( + instructionPlan: InstructionPlan, + context: { + parent: InstructionPlan | null; + parentCandidates: SingleTransactionPlan[]; + createSingleTransactionPlan: ( + instructions?: SingleInstructionPlan[] + ) => Promise; + addInstructionsToSingleTransactionPlan: ( + plan: SingleTransactionPlan, + instructions: SingleInstructionPlan[] + ) => Promise; + } +): Promise { + if (instructionPlan.kind === 'sequential') { + let candidate: SingleTransactionPlan | null = null; + const mustEntirelyFitInCandidate = + context.parent?.kind === 'parallel' || !instructionPlan.divisible; + if (mustEntirelyFitInCandidate) { + const allInstructions = getAllSingleInstructionPlans(instructionPlan); + candidate = selectCandidate(context.parentCandidates, allInstructions); + if (candidate) { + await context.addInstructionsToSingleTransactionPlan( + candidate, allInstructions ); - if (candidate) { - // The whole branch can be added to the candidate. - await addInstructionsToSingleTransactionPlan( - candidate, - allInstructions - ); - return null; - } - const transactionPlans: TransactionPlan[] = []; - for (const plan of instructionPlan.plans) { - const transactionPlan = await traverse( - plan, - candidate ? [candidate] : [] - ); - if (transactionPlan) { - transactionPlans.push(transactionPlan); - candidate = getSequentialCandidate(transactionPlan); - } - } - if (transactionPlans.length === 1) { - return transactionPlans[0]; - } - if (transactionPlans.length > 0) { - return null; - } - return { kind: 'sequential', divisible: true, plans: transactionPlans }; - } - - if (instructionPlan.kind === 'parallel') { - const candidates: SingleTransactionPlan[] = [...parentCandidates]; - const transactionPlans: TransactionPlan[] = []; - for (const plan of instructionPlan.plans) { - const transactionPlan = await traverse(plan, candidates); - if (transactionPlan) { - transactionPlans.push(transactionPlan); - candidates.push(...getParallelCandidates(transactionPlan)); - } - } - if (transactionPlans.length === 1) { - return transactionPlans[0]; - } - if (transactionPlans.length > 0) { - return null; - } - return { kind: 'parallel', plans: transactionPlans }; + return null; } + } else { + candidate = + context.parentCandidates.length > 0 + ? context.parentCandidates[0] + : null; + } - if (instructionPlan.kind === 'dynamic') { - throw new Error('Dynamic plans are not supported yet.'); + const transactionPlans: TransactionPlan[] = []; + for (const plan of instructionPlan.plans) { + const transactionPlan = await traverse(plan, { + ...context, + parent: instructionPlan, + parentCandidates: candidate ? [candidate] : [], + }); + if (transactionPlan) { + transactionPlans.push(transactionPlan); + candidate = getSequentialCandidate(transactionPlan); } + } + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length > 0) { + return null; + } + return { kind: 'sequential', divisible: true, plans: transactionPlans }; + } - if (instructionPlan.kind === 'single') { - const candidate = selectCandidate(parentCandidates, [instructionPlan]); - if (candidate) { - await addInstructionsToSingleTransactionPlan(candidate, [ - instructionPlan, - ]); - return null; - } - return await createSingleTransactionPlan([instructionPlan]); + if (instructionPlan.kind === 'parallel') { + const candidates: SingleTransactionPlan[] = [...context.parentCandidates]; + const transactionPlans: TransactionPlan[] = []; + for (const plan of instructionPlan.plans) { + const transactionPlan = await traverse(plan, { + ...context, + parent: instructionPlan, + parentCandidates: candidates, + }); + if (transactionPlan) { + transactionPlans.push(transactionPlan); + candidates.push(...getParallelCandidates(transactionPlan)); } + } + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length > 0) { + return null; + } + return { kind: 'parallel', plans: transactionPlans }; + } - instructionPlan satisfies never; - throw new Error( - `Unknown instruction plan kind: ${(instructionPlan as { kind: string }).kind}` - ); - }; + if (instructionPlan.kind === 'dynamic') { + throw new Error('Dynamic plans are not supported yet.'); + } - const plan = await traverse(originalInstructionPlan); - if (!plan) { - throw new Error('Transaction plan is null.'); // Should never happen. + if (instructionPlan.kind === 'single') { + const candidate = selectCandidate(context.parentCandidates, [ + instructionPlan, + ]); + if (candidate) { + await context.addInstructionsToSingleTransactionPlan(candidate, [ + instructionPlan, + ]); + return null; } + return await context.createSingleTransactionPlan([instructionPlan]); + } - return plan; - }; + instructionPlan satisfies never; + throw new Error( + `Unknown instruction plan kind: ${(instructionPlan as { kind: string }).kind}` + ); } function getSequentialCandidate( From 0f70410a192faa0cb6eb9f51c7e14c68aa6ded38 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 16:33:29 +0100 Subject: [PATCH 007/112] wip --- .../transactionPlanner.ts | 192 ++++++++++-------- 1 file changed, 106 insertions(+), 86 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 950feaa..9f28715 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -14,7 +14,12 @@ import { TransactionMessageWithBlockhashLifetime, TransactionVersion, } from '@solana/kit'; -import { InstructionPlan, SingleInstructionPlan } from './instructionPlan'; +import { + InstructionPlan, + ParallelInstructionPlan, + SequentialInstructionPlan, + SingleInstructionPlan, +} from './instructionPlan'; import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; const TRANSACTION_SIZE_LIMIT = @@ -101,107 +106,122 @@ export function createBaseTransactionPlanner({ }; } -// Recursive function that traverses the instruction plan and constructs the transaction plan. +type TraverseContext = { + parent: InstructionPlan | null; + parentCandidates: SingleTransactionPlan[]; + createSingleTransactionPlan: ( + instructions?: SingleInstructionPlan[] + ) => Promise; + addInstructionsToSingleTransactionPlan: ( + plan: SingleTransactionPlan, + instructions: SingleInstructionPlan[] + ) => Promise; +}; + async function traverse( instructionPlan: InstructionPlan, - context: { - parent: InstructionPlan | null; - parentCandidates: SingleTransactionPlan[]; - createSingleTransactionPlan: ( - instructions?: SingleInstructionPlan[] - ) => Promise; - addInstructionsToSingleTransactionPlan: ( - plan: SingleTransactionPlan, - instructions: SingleInstructionPlan[] - ) => Promise; - } + context: TraverseContext ): Promise { - if (instructionPlan.kind === 'sequential') { - let candidate: SingleTransactionPlan | null = null; - const mustEntirelyFitInCandidate = - context.parent?.kind === 'parallel' || !instructionPlan.divisible; - if (mustEntirelyFitInCandidate) { - const allInstructions = getAllSingleInstructionPlans(instructionPlan); - candidate = selectCandidate(context.parentCandidates, allInstructions); - if (candidate) { - await context.addInstructionsToSingleTransactionPlan( - candidate, - allInstructions - ); - return null; - } - } else { - candidate = - context.parentCandidates.length > 0 - ? context.parentCandidates[0] - : null; - } + switch (instructionPlan.kind) { + case 'sequential': + return await traverseSequential(instructionPlan, context); + case 'parallel': + return await traverseParallel(instructionPlan, context); + case 'single': + return await traverseSingle(instructionPlan, context); + case 'dynamic': + throw new Error('Dynamic plans are not supported yet.'); + default: + instructionPlan satisfies never; + throw new Error( + `Unknown instruction plan kind: ${(instructionPlan as { kind: string }).kind}` + ); + } +} - const transactionPlans: TransactionPlan[] = []; - for (const plan of instructionPlan.plans) { - const transactionPlan = await traverse(plan, { - ...context, - parent: instructionPlan, - parentCandidates: candidate ? [candidate] : [], - }); - if (transactionPlan) { - transactionPlans.push(transactionPlan); - candidate = getSequentialCandidate(transactionPlan); - } - } - if (transactionPlans.length === 1) { - return transactionPlans[0]; - } - if (transactionPlans.length > 0) { +async function traverseSequential( + instructionPlan: SequentialInstructionPlan, + context: TraverseContext +): Promise { + let candidate: SingleTransactionPlan | null = null; + const mustEntirelyFitInCandidate = + context.parent?.kind === 'parallel' || !instructionPlan.divisible; + if (mustEntirelyFitInCandidate) { + const allInstructions = getAllSingleInstructionPlans(instructionPlan); + candidate = selectCandidate(context.parentCandidates, allInstructions); + if (candidate) { + await context.addInstructionsToSingleTransactionPlan( + candidate, + allInstructions + ); return null; } - return { kind: 'sequential', divisible: true, plans: transactionPlans }; + } else { + candidate = + context.parentCandidates.length > 0 ? context.parentCandidates[0] : null; } - if (instructionPlan.kind === 'parallel') { - const candidates: SingleTransactionPlan[] = [...context.parentCandidates]; - const transactionPlans: TransactionPlan[] = []; - for (const plan of instructionPlan.plans) { - const transactionPlan = await traverse(plan, { - ...context, - parent: instructionPlan, - parentCandidates: candidates, - }); - if (transactionPlan) { - transactionPlans.push(transactionPlan); - candidates.push(...getParallelCandidates(transactionPlan)); - } - } - if (transactionPlans.length === 1) { - return transactionPlans[0]; - } - if (transactionPlans.length > 0) { - return null; + const transactionPlans: TransactionPlan[] = []; + for (const plan of instructionPlan.plans) { + const transactionPlan = await traverse(plan, { + ...context, + parent: instructionPlan, + parentCandidates: candidate ? [candidate] : [], + }); + if (transactionPlan) { + transactionPlans.push(transactionPlan); + candidate = getSequentialCandidate(transactionPlan); } - return { kind: 'parallel', plans: transactionPlans }; } + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length > 0) { + return null; + } + return { kind: 'sequential', divisible: true, plans: transactionPlans }; +} - if (instructionPlan.kind === 'dynamic') { - throw new Error('Dynamic plans are not supported yet.'); +async function traverseParallel( + instructionPlan: ParallelInstructionPlan, + context: TraverseContext +): Promise { + const candidates: SingleTransactionPlan[] = [...context.parentCandidates]; + const transactionPlans: TransactionPlan[] = []; + for (const plan of instructionPlan.plans) { + const transactionPlan = await traverse(plan, { + ...context, + parent: instructionPlan, + parentCandidates: candidates, + }); + if (transactionPlan) { + transactionPlans.push(transactionPlan); + candidates.push(...getParallelCandidates(transactionPlan)); + } + } + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length > 0) { + return null; } + return { kind: 'parallel', plans: transactionPlans }; +} - if (instructionPlan.kind === 'single') { - const candidate = selectCandidate(context.parentCandidates, [ +async function traverseSingle( + instructionPlan: SingleInstructionPlan, + context: TraverseContext +): Promise { + const candidate = selectCandidate(context.parentCandidates, [ + instructionPlan, + ]); + if (candidate) { + await context.addInstructionsToSingleTransactionPlan(candidate, [ instructionPlan, ]); - if (candidate) { - await context.addInstructionsToSingleTransactionPlan(candidate, [ - instructionPlan, - ]); - return null; - } - return await context.createSingleTransactionPlan([instructionPlan]); + return null; } - - instructionPlan satisfies never; - throw new Error( - `Unknown instruction plan kind: ${(instructionPlan as { kind: string }).kind}` - ); + return await context.createSingleTransactionPlan([instructionPlan]); } function getSequentialCandidate( From 4b060cafb90ad3d043a05395c7414530cd1d6c0a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 17:38:00 +0100 Subject: [PATCH 008/112] wip --- clients/js/src/index.ts | 1 + clients/js/src/instructionPlansDraft/index.ts | 2 + .../transactionPlanner.ts | 15 ++--- .../_instructionPlanHelpers.ts | 55 +++++++++++++++++++ .../_transactionPlanHelpers.ts | 46 ++++++++++++++++ .../transactionPlanner.test.ts | 21 +++++++ 6 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts create mode 100644 clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts create mode 100644 clients/js/test/instructionPlansDraft/transactionPlanner.test.ts diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index 32f0679..df2d459 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -1,4 +1,5 @@ export * from './generated'; +export * from './instructionPlansDraft'; export * from './createMetadata'; export * from './downloadMetadata'; diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts index 0df71b8..797a0b8 100644 --- a/clients/js/src/instructionPlansDraft/index.ts +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -1 +1,3 @@ export * from './instructionPlan'; +export * from './transactionPlan'; +export * from './transactionPlanner'; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 9f28715..a70d376 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -293,20 +293,17 @@ function isValidCandidate( return getRemainingTransactionSize(message) >= 0; } -function getRemainingTransactionSize(message: BaseTransactionMessage) { - return ( - TRANSACTION_SIZE_LIMIT - - getTransactionSize(message) - - 1 /* Subtract 1 byte buffer to account for shortvec encoding. */ - ); +export function getRemainingTransactionSize(message: BaseTransactionMessage) { + return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); } -function getTransactionSize( +export function getTransactionSize( message: BaseTransactionMessage & Partial ): number { - const mockFeePayer = '11111111111111111111111111111111' as Address; + const mockFeePayer = + 'Gm1uVH3JxiLgafByNNmnoxLncB7ytpyWNqX3kRM9tSxN' as Address; const mockBlockhash = { - blockhash: '11111111111111111111111111111111' as Blockhash, + blockhash: '2WCjwT4P5tJF7tjMtTVEnN6o53bcZ8MhszcfXMERtU3z' as Blockhash, lastValidBlockHeight: 0n, }; const transaction = pipe( diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts new file mode 100644 index 0000000..0f6f024 --- /dev/null +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -0,0 +1,55 @@ +import { Address } from '@solana/kit'; +import { + InstructionPlan, + ParallelInstructionPlan, + SequentialInstructionPlan, + SingleInstructionPlan, +} from '../../src'; + +const MINIMUM_INSTRUCTION_SIZE = 35; +const MINIMUM_TRANSACTION_SIZE = 136; +const MAXIMUM_TRANSACTION_SIZE = 1230; // 1280 - 48 (for header) - 2 (for shortU16) + +export function parallelInstructionPlan( + plans: InstructionPlan[] +): ParallelInstructionPlan { + return { kind: 'parallel', plans }; +} + +export function sequentialInstructionPlan( + plans: InstructionPlan[] +): SequentialInstructionPlan { + return { kind: 'sequential', divisible: true, plans }; +} + +export function nonDivisibleSequentialInstructionPlan( + plans: InstructionPlan[] +): SequentialInstructionPlan { + return { kind: 'sequential', divisible: false, plans }; +} + +export function getSingleInstructionPlanFactory() { + let counter = 0n; + return (bytes: number): SingleInstructionPlan => { + if (bytes < MINIMUM_INSTRUCTION_SIZE) { + throw new Error( + `Instruction size must be at least ${MINIMUM_INSTRUCTION_SIZE} bytes` + ); + } + const programAddress = BigInt('11111111111111111111111111111111') + counter; + counter += 1n; + return { + kind: 'single', + instruction: { + programAddress: programAddress.toString() as Address, + data: new Uint8Array(bytes - MINIMUM_INSTRUCTION_SIZE), + }, + }; + }; +} + +export function txPercent(percent: number) { + return Math.floor( + ((MAXIMUM_TRANSACTION_SIZE - MINIMUM_TRANSACTION_SIZE) * percent) / 100 + ); +} diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts new file mode 100644 index 0000000..a5699d2 --- /dev/null +++ b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts @@ -0,0 +1,46 @@ +import { + appendTransactionMessageInstructions, + BaseTransactionMessage, + createTransactionMessage, + IInstruction, +} from '@solana/kit'; +import { + TransactionPlan, + ParallelTransactionPlan, + SequentialTransactionPlan, + SingleTransactionPlan, +} from '../../src'; + +export function parallelTransactionPlan( + plans: TransactionPlan[] +): ParallelTransactionPlan { + return { kind: 'parallel', plans }; +} + +export function sequentialTransactionPlan( + plans: TransactionPlan[] +): SequentialTransactionPlan { + return { kind: 'sequential', divisible: true, plans }; +} + +export function nonDivisibleSequentialTransactionPlan( + plans: TransactionPlan[] +): SequentialTransactionPlan { + return { kind: 'sequential', divisible: false, plans }; +} + +export function getSingleTransactionPlanFactory( + defaultMessage?: () => BaseTransactionMessage +) { + const defaultMessageFn = + defaultMessage ?? (() => createTransactionMessage({ version: 0 })); + return (instructions: IInstruction[]): SingleTransactionPlan => { + return { + kind: 'single', + message: appendTransactionMessageInstructions( + instructions, + defaultMessageFn() + ), + }; + }; +} diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts new file mode 100644 index 0000000..0d3c328 --- /dev/null +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -0,0 +1,21 @@ +import test from 'ava'; +import { createBaseTransactionPlanner } from '../../src'; +import { getSingleInstructionPlanFactory } from './_instructionPlanHelpers'; +import { getSingleTransactionPlanFactory } from './_transactionPlanHelpers'; + +/** + * [Ix: A] => [Tx: A] + */ +test('it plans a single instruction', async (t) => { + const singleInstructionPlan = getSingleInstructionPlanFactory(); + const singleTransactionPlan = getSingleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionPlan = singleInstructionPlan(500); + const transactionPlan = await planner(singleInstructionPlan(500)); + + t.deepEqual( + transactionPlan, + singleTransactionPlan([instructionPlan.instruction]) + ); +}); From 963015bb23421d78deab054978bfaa0ccd548ffd Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 17:48:51 +0100 Subject: [PATCH 009/112] wip --- .../_instructionPlanHelpers.ts | 19 ++++++++++------- .../_transactionPlanHelpers.ts | 2 +- .../transactionPlanner.test.ts | 21 +++++++++---------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index 0f6f024..2bc2d5d 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -1,4 +1,4 @@ -import { Address } from '@solana/kit'; +import { Address, IInstruction } from '@solana/kit'; import { InstructionPlan, ParallelInstructionPlan, @@ -28,9 +28,15 @@ export function nonDivisibleSequentialInstructionPlan( return { kind: 'sequential', divisible: false, plans }; } -export function getSingleInstructionPlanFactory() { +export function singleInstructionPlan( + instruction: IInstruction +): SingleInstructionPlan { + return { kind: 'single', instruction }; +} + +export function instructionFactory() { let counter = 0n; - return (bytes: number): SingleInstructionPlan => { + return (bytes: number): IInstruction => { if (bytes < MINIMUM_INSTRUCTION_SIZE) { throw new Error( `Instruction size must be at least ${MINIMUM_INSTRUCTION_SIZE} bytes` @@ -39,11 +45,8 @@ export function getSingleInstructionPlanFactory() { const programAddress = BigInt('11111111111111111111111111111111') + counter; counter += 1n; return { - kind: 'single', - instruction: { - programAddress: programAddress.toString() as Address, - data: new Uint8Array(bytes - MINIMUM_INSTRUCTION_SIZE), - }, + programAddress: programAddress.toString() as Address, + data: new Uint8Array(bytes - MINIMUM_INSTRUCTION_SIZE), }; }; } diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts index a5699d2..af24dc7 100644 --- a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts @@ -29,7 +29,7 @@ export function nonDivisibleSequentialTransactionPlan( return { kind: 'sequential', divisible: false, plans }; } -export function getSingleTransactionPlanFactory( +export function singleTransactionPlanFactory( defaultMessage?: () => BaseTransactionMessage ) { const defaultMessageFn = diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 0d3c328..6c631c5 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1,21 +1,20 @@ import test from 'ava'; import { createBaseTransactionPlanner } from '../../src'; -import { getSingleInstructionPlanFactory } from './_instructionPlanHelpers'; -import { getSingleTransactionPlanFactory } from './_transactionPlanHelpers'; +import { + instructionFactory, + singleInstructionPlan, +} from './_instructionPlanHelpers'; +import { singleTransactionPlanFactory } from './_transactionPlanHelpers'; /** * [Ix: A] => [Tx: A] */ test('it plans a single instruction', async (t) => { - const singleInstructionPlan = getSingleInstructionPlanFactory(); - const singleTransactionPlan = getSingleTransactionPlanFactory(); + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); const planner = createBaseTransactionPlanner({ version: 0 }); - const instructionPlan = singleInstructionPlan(500); - const transactionPlan = await planner(singleInstructionPlan(500)); - - t.deepEqual( - transactionPlan, - singleTransactionPlan([instructionPlan.instruction]) - ); + const instructionA = instruction(42); + const transactionPlan = await planner(singleInstructionPlan(instructionA)); + t.deepEqual(transactionPlan, singleTransactionPlan([instructionA])); }); From 128bc7b46e0ab0e387d7faa92fad26cb6a153962 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 18:00:43 +0100 Subject: [PATCH 010/112] wip --- .../transactionPlanner.test.ts | 82 ++++++++++++++++++- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 6c631c5..883990e 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -2,12 +2,20 @@ import test from 'ava'; import { createBaseTransactionPlanner } from '../../src'; import { instructionFactory, + sequentialInstructionPlan, singleInstructionPlan, + txPercent, } from './_instructionPlanHelpers'; -import { singleTransactionPlanFactory } from './_transactionPlanHelpers'; +import { + sequentialTransactionPlan, + singleTransactionPlanFactory, +} from './_transactionPlanHelpers'; /** - * [Ix: A] => [Tx: A] + * [Ix: A] + * │ + * ▼ + * [Tx: A] */ test('it plans a single instruction', async (t) => { const instruction = instructionFactory(); @@ -15,6 +23,72 @@ test('it plans a single instruction', async (t) => { const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(42); - const transactionPlan = await planner(singleInstructionPlan(instructionA)); - t.deepEqual(transactionPlan, singleTransactionPlan([instructionA])); + + t.deepEqual( + await planner(singleInstructionPlan(instructionA)), + singleTransactionPlan([instructionA]) + ); +}); + +/** + * [Seq] + * | | + * ┌──────┘ └──────┐ + * [Ix: A] [Ix: B] + * │ + * ▼ + * [Tx: A + B] + */ +test('it plans a sequential plan with instructions that all fit in a single transaction', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]) + ), + singleTransactionPlan([instructionA, instructionB]) + ); +}); + +/** + * [Seq] + * / | \ + * [Ix: A] [Ix: B] [Ix: C] + * │ + * ▼ + * [Seq] + * | | + * ┌──────┘ └──────┐ + * [Tx: A + B] [Tx: C] + */ +test('it plans a sequential plan with instructions that must be split accross multiple transactions', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(50)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + singleTransactionPlan([instructionC]), + ]) + ); }); From 0b8e260de10bf73ce6a95f8f583fe5aeabec8c48 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 18:04:36 +0100 Subject: [PATCH 011/112] wip --- clients/js/src/instructionPlansDraft/transactionPlanner.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index a70d376..b0f5b6c 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -145,7 +145,8 @@ async function traverseSequential( ): Promise { let candidate: SingleTransactionPlan | null = null; const mustEntirelyFitInCandidate = - context.parent?.kind === 'parallel' || !instructionPlan.divisible; + context.parent && + (context.parent.kind === 'parallel' || !instructionPlan.divisible); if (mustEntirelyFitInCandidate) { const allInstructions = getAllSingleInstructionPlans(instructionPlan); candidate = selectCandidate(context.parentCandidates, allInstructions); @@ -176,7 +177,7 @@ async function traverseSequential( if (transactionPlans.length === 1) { return transactionPlans[0]; } - if (transactionPlans.length > 0) { + if (transactionPlans.length === 0) { return null; } return { kind: 'sequential', divisible: true, plans: transactionPlans }; @@ -202,7 +203,7 @@ async function traverseParallel( if (transactionPlans.length === 1) { return transactionPlans[0]; } - if (transactionPlans.length > 0) { + if (transactionPlans.length === 0) { return null; } return { kind: 'parallel', plans: transactionPlans }; From 327bde67123e1d0be56e68d77c883efbb764ced8 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 18:19:53 +0100 Subject: [PATCH 012/112] wip --- .../transactionPlanner.ts | 2 +- .../transactionPlanner.test.ts | 49 ++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index b0f5b6c..a21f4a2 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -99,7 +99,7 @@ export function createBaseTransactionPlanner({ }); if (!plan) { - throw new Error('Transaction plan is null.'); // Should never happen. + throw new Error('No instructions were found in the instruction plan.'); } return plan; diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 883990e..fb74c49 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -12,10 +12,7 @@ import { } from './_transactionPlanHelpers'; /** - * [Ix: A] - * │ - * ▼ - * [Tx: A] + * [Ix: A] ──────────────▶ [Tx: A] */ test('it plans a single instruction', async (t) => { const instruction = instructionFactory(); @@ -31,13 +28,10 @@ test('it plans a single instruction', async (t) => { }); /** - * [Seq] + * [Seq] ──────────────▶ [Tx: A + B] * | | * ┌──────┘ └──────┐ * [Ix: A] [Ix: B] - * │ - * ▼ - * [Tx: A + B] */ test('it plans a sequential plan with instructions that all fit in a single transaction', async (t) => { const instruction = instructionFactory(); @@ -59,15 +53,10 @@ test('it plans a sequential plan with instructions that all fit in a single tran }); /** - * [Seq] - * / | \ - * [Ix: A] [Ix: B] [Ix: C] - * │ - * ▼ - * [Seq] - * | | - * ┌──────┘ └──────┐ - * [Tx: A + B] [Tx: C] + * [Seq] ────────────────────────▶ [Seq] + * / | \ | | + * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ + * [Tx: A + B] [Tx: C] */ test('it plans a sequential plan with instructions that must be split accross multiple transactions', async (t) => { const instruction = instructionFactory(); @@ -92,3 +81,29 @@ test('it plans a sequential plan with instructions that must be split accross mu ]) ); }); + +/** + * [Seq] ──────────────▶ [Tx: A + B] + * / \ + * [Ix: A] [Seq] + * | + * [Ix: B] + */ +test('it simplifies nested sequential plans', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + sequentialInstructionPlan([singleInstructionPlan(instructionB)]), + ]) + ), + singleTransactionPlan([instructionA, instructionB]) + ); +}); From d7d3c8ad396315afdb41d29b0d7dfe20cf9944de Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 18:27:13 +0100 Subject: [PATCH 013/112] wip --- .../transactionPlanner.test.ts | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index fb74c49..45ae2a7 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -2,11 +2,13 @@ import test from 'ava'; import { createBaseTransactionPlanner } from '../../src'; import { instructionFactory, + parallelInstructionPlan, sequentialInstructionPlan, singleInstructionPlan, txPercent, } from './_instructionPlanHelpers'; import { + parallelTransactionPlan, sequentialTransactionPlan, singleTransactionPlanFactory, } from './_transactionPlanHelpers'; @@ -107,3 +109,84 @@ test('it simplifies nested sequential plans', async (t) => { singleTransactionPlan([instructionA, instructionB]) ); }); + +/** + * [Par] ──────────────▶ [Tx: A + B] + * | | + * ┌──────┘ └──────┐ + * [Ix: A] [Ix: B] + */ +test('it plans a parallel plan with instructions that all fit in a single transaction', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]) + ), + singleTransactionPlan([instructionA, instructionB]) + ); +}); + +/** + * [Par] ────────────────────────▶ [Par] + * / | \ | | + * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ + * [Tx: A + B] [Tx: C] + */ +test('it plans a parallel plan with instructions that must be split accross multiple transactions', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(50)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + singleTransactionPlan([instructionC]), + ]) + ); +}); + +/** + * [Par] ──────────────▶ [Tx: A + B] + * / \ + * [Ix: A] [Par] + * | + * [Ix: B] + */ +test('it simplifies nested parallel plans', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + parallelInstructionPlan([singleInstructionPlan(instructionB)]), + ]) + ), + singleTransactionPlan([instructionA, instructionB]) + ); +}); From 7aeb7b998e563fc9daef97705a423baa746cefbb Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 3 Apr 2025 19:19:53 +0100 Subject: [PATCH 014/112] wip --- .../transactionPlanner.test.ts | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 45ae2a7..0307f26 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -85,11 +85,11 @@ test('it plans a sequential plan with instructions that must be split accross mu }); /** - * [Seq] ──────────────▶ [Tx: A + B] - * / \ - * [Ix: A] [Seq] - * | - * [Ix: B] + * [Seq] ──────────────▶ [Tx: A + B] + * / \ + * [Ix: A] [Seq] + * | + * [Ix: B] */ test('it simplifies nested sequential plans', async (t) => { const instruction = instructionFactory(); @@ -166,11 +166,11 @@ test('it plans a parallel plan with instructions that must be split accross mult }); /** - * [Par] ──────────────▶ [Tx: A + B] - * / \ - * [Ix: A] [Par] - * | - * [Ix: B] + * [Par] ──────────────▶ [Tx: A + B] + * / \ + * [Ix: A] [Par] + * | + * [Ix: B] */ test('it simplifies nested parallel plans', async (t) => { const instruction = instructionFactory(); @@ -190,3 +190,38 @@ test('it simplifies nested parallel plans', async (t) => { singleTransactionPlan([instructionA, instructionB]) ); }); + +/** + * [Par] ──────────────────────────▶ [Par] + * / | \ / \ + * [Seq] [Ix: C] [Ix: D] [Tx: A + B + D] [Tx: C] + * / \ + * [Ix: A] [Ix: B] + */ +test('it re-uses previous parallel transactions if there is space', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(25)); + const instructionC = instruction(txPercent(90)); + const instructionD = instruction(txPercent(25)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA, instructionB, instructionD]), + singleTransactionPlan([instructionC]), + ]) + ); +}); From f3587f8e6b8f7fcc77825d4cab64cfd02006ee69 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 10:50:24 +0100 Subject: [PATCH 015/112] wip --- .../transactionPlanner.test.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 0307f26..bad3940 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -225,3 +225,79 @@ test('it re-uses previous parallel transactions if there is space', async (t) => ]) ); }); + +/** + * [Par] ──────────────▶ [Tx: A + B + C + D] + * / \ + * [Seq] [Seq] + * / \ / \ + * [Ix: A] [Ix: B] [Ix: C] [Ix: D] + */ +test('it can merge sequential plans in a parallel plan if the whole sequential plan fits', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(25)); + const instructionB = instruction(txPercent(25)); + const instructionC = instruction(txPercent(25)); + const instructionD = instruction(txPercent(25)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + sequentialInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + singleTransactionPlan([ + instructionA, + instructionB, + instructionC, + instructionD, + ]) + ); +}); + +/** + * [Par] ──────────────────────────▶ [Par] + * / \ / \ + * [Seq] [Seq] [Tx: A + B] [Tx: C + D] + * / \ / \ + * [Ix: A] [Ix: B] [Ix: C] [Ix: D] + */ +test('it does not split a sequential plan on a parallel parent', async (t) => { + const instruction = instructionFactory(); + const singleTransactionPlan = singleTransactionPlanFactory(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(33)); + const instructionB = instruction(txPercent(33)); + const instructionC = instruction(txPercent(33)); + const instructionD = instruction(txPercent(33)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + sequentialInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + singleTransactionPlan([instructionC, instructionD]), + ]) + ); +}); From dd036930c425355bc483c78740287dbac80a167e Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 10:58:57 +0100 Subject: [PATCH 016/112] wip --- .../_instructionPlanHelpers.ts | 18 ++++++--- .../transactionPlanner.test.ts | 40 +++++++++---------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index 2bc2d5d..ace62cd 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -1,5 +1,6 @@ -import { Address, IInstruction } from '@solana/kit'; +import { Address, BaseTransactionMessage, IInstruction } from '@solana/kit'; import { + getTransactionSize, InstructionPlan, ParallelInstructionPlan, SequentialInstructionPlan, @@ -51,8 +52,15 @@ export function instructionFactory() { }; } -export function txPercent(percent: number) { - return Math.floor( - ((MAXIMUM_TRANSACTION_SIZE - MINIMUM_TRANSACTION_SIZE) * percent) / 100 - ); +export function transactionPercentFactory( + defaultMessage?: () => BaseTransactionMessage +) { + const minimumTransactionSize = defaultMessage + ? getTransactionSize(defaultMessage()) + : MINIMUM_TRANSACTION_SIZE; + return (percent: number) => { + return Math.floor( + ((MAXIMUM_TRANSACTION_SIZE - minimumTransactionSize) * percent) / 100 + ); + }; } diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index bad3940..a4f0df3 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -5,7 +5,7 @@ import { parallelInstructionPlan, sequentialInstructionPlan, singleInstructionPlan, - txPercent, + transactionPercentFactory, } from './_instructionPlanHelpers'; import { parallelTransactionPlan, @@ -13,12 +13,19 @@ import { singleTransactionPlanFactory, } from './_transactionPlanHelpers'; +function defaultFactories() { + return { + instruction: instructionFactory(), + txPercent: transactionPercentFactory(), + singleTransactionPlan: singleTransactionPlanFactory(), + }; +} + /** * [Ix: A] ──────────────▶ [Tx: A] */ test('it plans a single instruction', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(42); @@ -36,8 +43,7 @@ test('it plans a single instruction', async (t) => { * [Ix: A] [Ix: B] */ test('it plans a sequential plan with instructions that all fit in a single transaction', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(50)); @@ -61,8 +67,7 @@ test('it plans a sequential plan with instructions that all fit in a single tran * [Tx: A + B] [Tx: C] */ test('it plans a sequential plan with instructions that must be split accross multiple transactions', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(50)); @@ -92,8 +97,7 @@ test('it plans a sequential plan with instructions that must be split accross mu * [Ix: B] */ test('it simplifies nested sequential plans', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(50)); @@ -117,8 +121,7 @@ test('it simplifies nested sequential plans', async (t) => { * [Ix: A] [Ix: B] */ test('it plans a parallel plan with instructions that all fit in a single transaction', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(50)); @@ -142,8 +145,7 @@ test('it plans a parallel plan with instructions that all fit in a single transa * [Tx: A + B] [Tx: C] */ test('it plans a parallel plan with instructions that must be split accross multiple transactions', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(50)); @@ -173,8 +175,7 @@ test('it plans a parallel plan with instructions that must be split accross mult * [Ix: B] */ test('it simplifies nested parallel plans', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(50)); @@ -199,8 +200,7 @@ test('it simplifies nested parallel plans', async (t) => { * [Ix: A] [Ix: B] */ test('it re-uses previous parallel transactions if there is space', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(50)); @@ -234,8 +234,7 @@ test('it re-uses previous parallel transactions if there is space', async (t) => * [Ix: A] [Ix: B] [Ix: C] [Ix: D] */ test('it can merge sequential plans in a parallel plan if the whole sequential plan fits', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(25)); @@ -273,8 +272,7 @@ test('it can merge sequential plans in a parallel plan if the whole sequential p * [Ix: A] [Ix: B] [Ix: C] [Ix: D] */ test('it does not split a sequential plan on a parallel parent', async (t) => { - const instruction = instructionFactory(); - const singleTransactionPlan = singleTransactionPlanFactory(); + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(33)); From e6213eea48f78c20a7b821703c4d76b3ce4064db Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 11:13:30 +0100 Subject: [PATCH 017/112] wip --- .../transactionPlanner.test.ts | 98 ++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index a4f0df3..44be3b1 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -66,7 +66,7 @@ test('it plans a sequential plan with instructions that all fit in a single tran * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ * [Tx: A + B] [Tx: C] */ -test('it plans a sequential plan with instructions that must be split accross multiple transactions', async (t) => { +test('it plans a sequential plan with instructions that must be split accross multiple transactions (v1)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); @@ -89,6 +89,35 @@ test('it plans a sequential plan with instructions that must be split accross mu ); }); +/** + * [Seq] ────────────────────────▶ [Seq] + * / | \ | | + * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ + * [Tx: A] [Tx: B + C] + */ +test('it plans a sequential plan with instructions that must be split accross multiple transactions (v2)', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(50)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA]), + singleTransactionPlan([instructionB, instructionC]), + ]) + ); +}); + /** * [Seq] ──────────────▶ [Tx: A + B] * / \ @@ -144,7 +173,7 @@ test('it plans a parallel plan with instructions that all fit in a single transa * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ * [Tx: A + B] [Tx: C] */ -test('it plans a parallel plan with instructions that must be split accross multiple transactions', async (t) => { +test('it plans a parallel plan with instructions that must be split accross multiple transactions (v1)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); @@ -167,6 +196,35 @@ test('it plans a parallel plan with instructions that must be split accross mult ); }); +/** + * [Par] ────────────────────────▶ [Par] + * / | \ | | + * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ + * [Tx: A] [Tx: B + C] + */ +test('it plans a parallel plan with instructions that must be split accross multiple transactions (v2)', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(50)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA]), + singleTransactionPlan([instructionB, instructionC]), + ]) + ); +}); + /** * [Par] ──────────────▶ [Tx: A + B] * / \ @@ -299,3 +357,39 @@ test('it does not split a sequential plan on a parallel parent', async (t) => { ]) ); }); + +/** + * [Seq] ──────────────────────────▶ [Seq] + * / \ / \ + * [Par] [Par] [Tx: A + B + C] [Tx: D] + * / \ / \ + * [Ix: A] [Ix: B] [Ix: C] [Ix: D] + */ +test('it can split parallel plans inside sequential plans as long as they follow the sequence', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(33)); + const instructionB = instruction(txPercent(33)); + const instructionC = instruction(txPercent(33)); + const instructionD = instruction(txPercent(33)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + parallelInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA, instructionB, instructionC]), + singleTransactionPlan([instructionD]), + ]) + ); +}); From bd79ee9927f295b1c885c8748d94b57584aef3a4 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 11:28:58 +0100 Subject: [PATCH 018/112] wip --- .../transactionPlanner.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 44be3b1..348354e 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -393,3 +393,52 @@ test('it can split parallel plans inside sequential plans as long as they follow ]) ); }); + +/** + * [Seq] ───────────────────▶ [Seq] + * │ │ + * ├── [Par] ├── [Tx: A + B] + * │ ├── [A: 33%] ├── [Tx: C + D] + * │ └── [B: 33%] └── [Tx: E + F] + * ├── [Par] + * │ ├── [C: 50%] + * │ └── [D: 50%] + * └── [Par] + * ├── [E: 33%] + * └── [F: 33%] + */ +test('it cannnot split a parallel plan in a sequential plan if that would break the sequence', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(33)); + const instructionB = instruction(txPercent(33)); + const instructionC = instruction(txPercent(50)); + const instructionD = instruction(txPercent(50)); + const instructionE = instruction(txPercent(33)); + const instructionF = instruction(txPercent(33)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + parallelInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + parallelInstructionPlan([ + singleInstructionPlan(instructionE), + singleInstructionPlan(instructionF), + ]), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + singleTransactionPlan([instructionC, instructionD]), + singleTransactionPlan([instructionE, instructionF]), + ]) + ); +}); From ddbd604cbbcf416019792d1ee903dd376110480d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 11:38:31 +0100 Subject: [PATCH 019/112] wip --- .../transactionPlanner.test.ts | 125 ++++++++++-------- 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 348354e..35947d8 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -22,7 +22,7 @@ function defaultFactories() { } /** - * [Ix: A] ──────────────▶ [Tx: A] + * [A: 42] ───────────────────▶ [Tx: A] */ test('it plans a single instruction', async (t) => { const { instruction, singleTransactionPlan } = defaultFactories(); @@ -37,10 +37,10 @@ test('it plans a single instruction', async (t) => { }); /** - * [Seq] ──────────────▶ [Tx: A + B] - * | | - * ┌──────┘ └──────┐ - * [Ix: A] [Ix: B] + * [Seq] ───────────────────▶ [Tx: A + B] + * │ + * ├── [A: 50%] + * └── [B: 50%] */ test('it plans a sequential plan with instructions that all fit in a single transaction', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -61,10 +61,11 @@ test('it plans a sequential plan with instructions that all fit in a single tran }); /** - * [Seq] ────────────────────────▶ [Seq] - * / | \ | | - * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ - * [Tx: A + B] [Tx: C] + * [Seq] ───────────────────▶ [Seq] + * │ │ + * ├── [A: 50%] ├── [Tx: A + B] + * ├── [B: 50%] └── [Tx: C] + * └── [C: 50%] */ test('it plans a sequential plan with instructions that must be split accross multiple transactions (v1)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -90,10 +91,11 @@ test('it plans a sequential plan with instructions that must be split accross mu }); /** - * [Seq] ────────────────────────▶ [Seq] - * / | \ | | - * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ - * [Tx: A] [Tx: B + C] + * [Seq] ───────────────────▶ [Seq] + * │ │ + * ├── [A: 60%] ├── [Tx: A] + * ├── [B: 50%] └── [Tx: B + C] + * └── [C: 50%] */ test('it plans a sequential plan with instructions that must be split accross multiple transactions (v2)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -119,11 +121,11 @@ test('it plans a sequential plan with instructions that must be split accross mu }); /** - * [Seq] ──────────────▶ [Tx: A + B] - * / \ - * [Ix: A] [Seq] - * | - * [Ix: B] + * [Seq] ───────────────────▶ [Tx: A + B] + * │ + * ├── [A: 50%] + * └── [Seq] + * └── [B: 50%] */ test('it simplifies nested sequential plans', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -144,10 +146,10 @@ test('it simplifies nested sequential plans', async (t) => { }); /** - * [Par] ──────────────▶ [Tx: A + B] - * | | - * ┌──────┘ └──────┐ - * [Ix: A] [Ix: B] + * [Par] ───────────────────▶ [Tx: A + B] + * │ + * ├── [A: 50%] + * └── [B: 50%] */ test('it plans a parallel plan with instructions that all fit in a single transaction', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -168,10 +170,11 @@ test('it plans a parallel plan with instructions that all fit in a single transa }); /** - * [Par] ────────────────────────▶ [Par] - * / | \ | | - * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ - * [Tx: A + B] [Tx: C] + * [Par] ───────────────────▶ [Par] + * │ │ + * ├── [A: 50%] ├── [Tx: A + B] + * ├── [B: 50%] └── [Tx: C] + * └── [C: 50%] */ test('it plans a parallel plan with instructions that must be split accross multiple transactions (v1)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -197,10 +200,11 @@ test('it plans a parallel plan with instructions that must be split accross mult }); /** - * [Par] ────────────────────────▶ [Par] - * / | \ | | - * [Ix: A] [Ix: B] [Ix: C] ┌────────┘ └───────┐ - * [Tx: A] [Tx: B + C] + * [Par] ───────────────────▶ [Par] + * │ │ + * ├── [A: 60%] ├── [Tx: A] + * ├── [B: 50%] └── [Tx: B + C] + * └── [C: 50%] */ test('it plans a parallel plan with instructions that must be split accross multiple transactions (v2)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -226,11 +230,11 @@ test('it plans a parallel plan with instructions that must be split accross mult }); /** - * [Par] ──────────────▶ [Tx: A + B] - * / \ - * [Ix: A] [Par] - * | - * [Ix: B] + * [Par] ───────────────────▶ [Tx: A + B] + * │ + * ├── [A: 50%] + * └── [Par] + * └── [B: 50%] */ test('it simplifies nested parallel plans', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -251,11 +255,13 @@ test('it simplifies nested parallel plans', async (t) => { }); /** - * [Par] ──────────────────────────▶ [Par] - * / | \ / \ - * [Seq] [Ix: C] [Ix: D] [Tx: A + B + D] [Tx: C] - * / \ - * [Ix: A] [Ix: B] + * [Par] ───────────────────▶ [Par] + * │ │ + * ├── [Seq] ├── [Tx: A + B + D] + * │ ├── [A: 50%] └── [Tx: C] + * │ └── [B: 25%] + * ├── [C: 90%] + * └── [D: 25%] */ test('it re-uses previous parallel transactions if there is space', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -285,11 +291,14 @@ test('it re-uses previous parallel transactions if there is space', async (t) => }); /** - * [Par] ──────────────▶ [Tx: A + B + C + D] - * / \ - * [Seq] [Seq] - * / \ / \ - * [Ix: A] [Ix: B] [Ix: C] [Ix: D] + * [Par] ───────────────────▶ [Tx: A + B + C + D] + * │ + * ├── [Seq] + * │ ├── [A: 25%] + * │ └── [B: 25%] + * └── [Seq] + * ├── [C: 25%] + * └── [D: 25%] */ test('it can merge sequential plans in a parallel plan if the whole sequential plan fits', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -323,11 +332,14 @@ test('it can merge sequential plans in a parallel plan if the whole sequential p }); /** - * [Par] ──────────────────────────▶ [Par] - * / \ / \ - * [Seq] [Seq] [Tx: A + B] [Tx: C + D] - * / \ / \ - * [Ix: A] [Ix: B] [Ix: C] [Ix: D] + * [Par] ───────────────────▶ [Par] + * │ │ + * ├── [Seq] ├── [Tx: A + B] + * │ ├── [A: 33%] └── [Tx: C + D] + * │ └── [B: 33%] + * └── [Seq] + * ├── [C: 33%] + * └── [D: 33%] */ test('it does not split a sequential plan on a parallel parent', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); @@ -359,11 +371,14 @@ test('it does not split a sequential plan on a parallel parent', async (t) => { }); /** - * [Seq] ──────────────────────────▶ [Seq] - * / \ / \ - * [Par] [Par] [Tx: A + B + C] [Tx: D] - * / \ / \ - * [Ix: A] [Ix: B] [Ix: C] [Ix: D] + * [Seq] ───────────────────▶ [Seq] + * │ │ + * ├── [Par] ├── [Tx: A + B + C] + * │ ├── [A: 33%] └── [Tx: D] + * │ └── [B: 33%] + * └── [Par] + * ├── [C: 33%] + * └── [D: 33%] */ test('it can split parallel plans inside sequential plans as long as they follow the sequence', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); From aac5d242596e89c7f7cefee08e09c4b2516223e6 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 11:58:26 +0100 Subject: [PATCH 020/112] Add tests for non-divisible sequential plans --- .../transactionPlanner.ts | 6 +- .../transactionPlanner.test.ts | 273 ++++++++++++++++++ 2 files changed, 278 insertions(+), 1 deletion(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index a21f4a2..549719c 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -180,7 +180,11 @@ async function traverseSequential( if (transactionPlans.length === 0) { return null; } - return { kind: 'sequential', divisible: true, plans: transactionPlans }; + return { + kind: 'sequential', + divisible: instructionPlan.divisible, + plans: transactionPlans, + }; } async function traverseParallel( diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 35947d8..512e4db 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -2,12 +2,14 @@ import test from 'ava'; import { createBaseTransactionPlanner } from '../../src'; import { instructionFactory, + nonDivisibleSequentialInstructionPlan, parallelInstructionPlan, sequentialInstructionPlan, singleInstructionPlan, transactionPercentFactory, } from './_instructionPlanHelpers'; import { + nonDivisibleSequentialTransactionPlan, parallelTransactionPlan, sequentialTransactionPlan, singleTransactionPlanFactory, @@ -457,3 +459,274 @@ test('it cannnot split a parallel plan in a sequential plan if that would break ]) ); }); + +/** + * [NonDivSeq] ───────────────────▶ [Tx: A + B] + * │ + * ├── [A: 50%] + * └── [B: 50%] + */ +test('it plans an non-divisible sequential plan with instructions that all fit in a single transaction', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]) + ), + singleTransactionPlan([instructionA, instructionB]) + ); +}); + +/** + * [NonDivSeq] ─────────────▶ [NonDivSeq] + * │ │ + * ├── [A: 50%] ├── [Tx: A + B] + * ├── [B: 50%] └── [Tx: C] + * └── [C: 50%] + */ +test('it plans a non-divisible sequential plan with instructions that must be split accross multiple transactions (v1)', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(50)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]) + ), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + singleTransactionPlan([instructionC]), + ]) + ); +}); + +/** + * [NonDivSeq] ─────────────▶ [NonDivSeq] + * │ │ + * ├── [A: 60%] ├── [Tx: A] + * ├── [B: 50%] └── [Tx: B + C] + * └── [C: 50%] + */ +test('it plans a non-divisible sequential plan with instructions that must be split accross multiple transactions (v2)', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(50)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]) + ), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([instructionA]), + singleTransactionPlan([instructionB, instructionC]), + ]) + ); +}); + +/** + * [NonDivSeq] ─────────────▶ [Tx: A + B] + * │ + * ├── [A: 50%] + * └── [NonDivSeq] + * └── [B: 50%] + */ +test('it simplifies nested non-divisible sequential plans', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionB), + ]), + ]) + ), + singleTransactionPlan([instructionA, instructionB]) + ); +}); + +/** + * [Par] ───────────────────▶ [Tx: A + B + C + D] + * │ + * ├── [NonDivSeq] + * │ ├── [A: 25%] + * │ └── [B: 25%] + * └── [NonDivSeq] + * ├── [C: 25%] + * └── [D: 25%] + */ +test('it can merge non-divisible sequential plans in a parallel plan if the whole sequential plan fits', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(25)); + const instructionB = instruction(txPercent(25)); + const instructionC = instruction(txPercent(25)); + const instructionD = instruction(txPercent(25)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + singleTransactionPlan([ + instructionA, + instructionB, + instructionC, + instructionD, + ]) + ); +}); + +/** + * [Par] ───────────────────▶ [Par] + * │ │ + * ├── [NonDivSeq] ├── [Tx: A + B] + * │ ├── [A: 33%] └── [Tx: C + D] + * │ └── [B: 33%] + * └── [NonDivSeq] + * ├── [C: 33%] + * └── [D: 33%] + */ +test('it does not split a non-divisible sequential plan on a parallel parent', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(33)); + const instructionB = instruction(txPercent(33)); + const instructionC = instruction(txPercent(33)); + const instructionD = instruction(txPercent(33)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + singleTransactionPlan([instructionC, instructionD]), + ]) + ); +}); + +/** + * [Seq] ───────────────────▶ [Tx: A + B + C + D] + * │ + * ├── [NonDivSeq] + * │ ├── [A: 25%] + * │ └── [B: 25%] + * └── [NonDivSeq] + * ├── [C: 25%] + * └── [D: 25%] + */ +test('it can merge non-divisible sequential plans in a sequential plan if the whole plan fits', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(25)); + const instructionB = instruction(txPercent(25)); + const instructionC = instruction(txPercent(25)); + const instructionD = instruction(txPercent(25)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + singleTransactionPlan([ + instructionA, + instructionB, + instructionC, + instructionD, + ]) + ); +}); + +/** + * [Seq] ───────────────────▶ [Seq] + * │ │ + * ├── [NonDivSeq] ├── [Tx: A + B] + * │ ├── [A: 33%] └── [Tx: C + D] + * │ └── [B: 33%] + * └── [NonDivSeq] + * ├── [C: 33%] + * └── [D: 33%] + */ +test('it does not split a non-divisible sequential plan on a sequential parent', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(33)); + const instructionB = instruction(txPercent(33)); + const instructionC = instruction(txPercent(33)); + const instructionD = instruction(txPercent(33)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + singleTransactionPlan([instructionC, instructionD]), + ]) + ); +}); From 072383e854be200b0fa05608332d36fbcad3e978 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 12:09:25 +0100 Subject: [PATCH 021/112] wip --- .../transactionPlanner.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 512e4db..960a3b3 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -730,3 +730,67 @@ test('it does not split a non-divisible sequential plan on a sequential parent', ]) ); }); + +// TODO: NonDivSeq with Par children + +/** + * [Par] ───────────────────────────▶ [Par] + * │ │ + * ├── [Seq] ├── [Tx: A + B] + * │ ├── [A: 40%] └── [NonDivSeq] + * │ └── [B: 40%] ├── [Tx: C + D + E + G] + * ├── [NonDivSeq] └── [Tx: F] + * │ ├── [Par] + * │ │ ├── [C: 25%] + * │ │ └── [D: 25%] + * │ └── [Par] + * │ ├── [E: 25%] + * │ └── [F: 50%] + * └── [G: 25%] + */ +test('complex example 1', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(40)); + const instructionB = instruction(txPercent(40)); + const instructionC = instruction(txPercent(25)); + const instructionD = instruction(txPercent(25)); + const instructionE = instruction(txPercent(25)); + const instructionF = instruction(txPercent(50)); + const instructionG = instruction(txPercent(25)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + nonDivisibleSequentialInstructionPlan([ + parallelInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + parallelInstructionPlan([ + singleInstructionPlan(instructionE), + singleInstructionPlan(instructionF), + ]), + ]), + singleInstructionPlan(instructionG), + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([ + instructionC, + instructionD, + instructionE, + instructionG, + ]), + singleTransactionPlan([instructionF]), + ]), + ]) + ); +}); From d50ff83b0ca4a3e23d205258895b501094966900 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 13:37:32 +0100 Subject: [PATCH 022/112] wip --- .../transactionPlanner.test.ts | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 960a3b3..59afdf1 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -731,7 +731,89 @@ test('it does not split a non-divisible sequential plan on a sequential parent', ); }); -// TODO: NonDivSeq with Par children +/** + * [NonDivSeq] ─────────────▶ [NonDivSeq] + * │ │ + * ├── [Par] ├── [Tx: A + B] + * │ ├── [A: 50%] └── [Par] + * │ └── [B: 50%] ├── [Tx: C] + * └── [Par] └── [Tx: D] + * ├── [C: 100%] + * └── [D: 100%] + */ +test('it plans non-divisible sequentials plans with parallel children', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(100)); + const instructionD = instruction(txPercent(100)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + parallelInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + parallelTransactionPlan([ + singleTransactionPlan([instructionC]), + singleTransactionPlan([instructionD]), + ]), + ]) + ); +}); + +/** + * [NonDivSeq] ─────────────▶ [NonDivSeq] + * │ │ + * ├── [Seq] ├── [Tx: A + B] + * │ ├── [A: 50%] └── [Seq] + * │ └── [B: 50%] ├── [Tx: C] + * └── [Seq] └── [Tx: D] + * ├── [C: 100%] + * └── [D: 100%] + */ +test('it plans non-divisible sequentials plans with divisible sequential children', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const instructionB = instruction(txPercent(50)); + const instructionC = instruction(txPercent(100)); + const instructionD = instruction(txPercent(100)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + ]), + sequentialInstructionPlan([ + singleInstructionPlan(instructionC), + singleInstructionPlan(instructionD), + ]), + ]) + ), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([instructionA, instructionB]), + sequentialTransactionPlan([ + singleTransactionPlan([instructionC]), + singleTransactionPlan([instructionD]), + ]), + ]) + ); +}); /** * [Par] ───────────────────────────▶ [Par] From b75ca480cdc7698b4773b1de0a155349287d86c2 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 13:41:39 +0100 Subject: [PATCH 023/112] wip --- .../instructionPlansDraft/transactionPlanner.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 59afdf1..f112399 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -127,9 +127,10 @@ test('it plans a sequential plan with instructions that must be split accross mu * │ * ├── [A: 50%] * └── [Seq] + * └── [Seq] * └── [B: 50%] */ -test('it simplifies nested sequential plans', async (t) => { +test('it simplifies sequential plans with one child or less', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); @@ -140,6 +141,7 @@ test('it simplifies nested sequential plans', async (t) => { await planner( sequentialInstructionPlan([ singleInstructionPlan(instructionA), + sequentialInstructionPlan([]), sequentialInstructionPlan([singleInstructionPlan(instructionB)]), ]) ), @@ -236,9 +238,10 @@ test('it plans a parallel plan with instructions that must be split accross mult * │ * ├── [A: 50%] * └── [Par] + * └── [Par] * └── [B: 50%] */ -test('it simplifies nested parallel plans', async (t) => { +test('it simplifies parallel plans with one child or less', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); @@ -249,6 +252,7 @@ test('it simplifies nested parallel plans', async (t) => { await planner( parallelInstructionPlan([ singleInstructionPlan(instructionA), + parallelInstructionPlan([]), parallelInstructionPlan([singleInstructionPlan(instructionB)]), ]) ), @@ -549,9 +553,10 @@ test('it plans a non-divisible sequential plan with instructions that must be sp * │ * ├── [A: 50%] * └── [NonDivSeq] + * └── [NonDivSeq] * └── [B: 50%] */ -test('it simplifies nested non-divisible sequential plans', async (t) => { +test('it simplifies non-divisible sequential plans with one child or less', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); @@ -562,6 +567,7 @@ test('it simplifies nested non-divisible sequential plans', async (t) => { await planner( nonDivisibleSequentialInstructionPlan([ singleInstructionPlan(instructionA), + nonDivisibleSequentialInstructionPlan([]), nonDivisibleSequentialInstructionPlan([ singleInstructionPlan(instructionB), ]), From 5b6e47120e19b611a588a5a22943989f81d36872 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 13:59:10 +0100 Subject: [PATCH 024/112] wip --- .../transactionPlanner.ts | 12 +- .../transactionPlanner.test.ts | 190 +++++++++++++++++- 2 files changed, 190 insertions(+), 12 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 549719c..f6e72de 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -170,8 +170,12 @@ async function traverseSequential( parentCandidates: candidate ? [candidate] : [], }); if (transactionPlan) { - transactionPlans.push(transactionPlan); candidate = getSequentialCandidate(transactionPlan); + const newPlans = + transactionPlan.kind === 'sequential' && transactionPlan.divisible + ? transactionPlan.plans + : [transactionPlan]; + transactionPlans.push(...newPlans); } } if (transactionPlans.length === 1) { @@ -200,8 +204,12 @@ async function traverseParallel( parentCandidates: candidates, }); if (transactionPlan) { - transactionPlans.push(transactionPlan); candidates.push(...getParallelCandidates(transactionPlan)); + const newPlans = + transactionPlan.kind === 'parallel' + ? transactionPlan.plans + : [transactionPlan]; + transactionPlans.push(...newPlans); } } if (transactionPlans.length === 1) { diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index f112399..629daba 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -126,7 +126,7 @@ test('it plans a sequential plan with instructions that must be split accross mu * [Seq] ───────────────────▶ [Tx: A + B] * │ * ├── [A: 50%] - * └── [Seq] + * ├── [Seq] * └── [Seq] * └── [B: 50%] */ @@ -149,6 +149,40 @@ test('it simplifies sequential plans with one child or less', async (t) => { ); }); +/** + * [Seq] ──────────────────────▶ [Seq] + * │ │ + * ├── [A: 100%] ├── [Tx: A] + * └── [Seq] ├── [Tx: B] + * ├── [B: 100%] └── [Tx: C] + * └── [C: 100%] + */ +test('it simplifies nested sequential plans', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(100)); + const instructionB = instruction(txPercent(100)); + const instructionC = instruction(txPercent(100)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + sequentialInstructionPlan([ + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA]), + singleTransactionPlan([instructionB]), + singleTransactionPlan([instructionC]), + ]) + ); +}); + /** * [Par] ───────────────────▶ [Tx: A + B] * │ @@ -237,7 +271,7 @@ test('it plans a parallel plan with instructions that must be split accross mult * [Par] ───────────────────▶ [Tx: A + B] * │ * ├── [A: 50%] - * └── [Par] + * ├── [Par] * └── [Par] * └── [B: 50%] */ @@ -260,6 +294,40 @@ test('it simplifies parallel plans with one child or less', async (t) => { ); }); +/** + * [Par] ──────────────────────▶ [Par] + * │ │ + * ├── [A: 100%] ├── [Tx: A] + * └── [Par] ├── [Tx: B] + * ├── [B: 100%] └── [Tx: C] + * └── [C: 100%] + */ +test('it simplifies nested parallel plans', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(100)); + const instructionB = instruction(txPercent(100)); + const instructionC = instruction(txPercent(100)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + parallelInstructionPlan([ + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]), + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA]), + singleTransactionPlan([instructionB]), + singleTransactionPlan([instructionC]), + ]) + ); +}); + /** * [Par] ───────────────────▶ [Par] * │ │ @@ -552,7 +620,7 @@ test('it plans a non-divisible sequential plan with instructions that must be sp * [NonDivSeq] ─────────────▶ [Tx: A + B] * │ * ├── [A: 50%] - * └── [NonDivSeq] + * ├── [NonDivSeq] * └── [NonDivSeq] * └── [B: 50%] */ @@ -577,6 +645,110 @@ test('it simplifies non-divisible sequential plans with one child or less', asyn ); }); +/** + * [NonDivSeq] ────────────────▶ [NonDivSeq] + * │ │ + * ├── [A: 100%] ├── [Tx: A] + * └── [NonDivSeq] ├── [Tx: B] + * ├── [B: 100%] └── [Tx: C] + * └── [C: 100%] + */ +test.skip('it simplifies nested non-divisible sequential plans', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(100)); + const instructionB = instruction(txPercent(100)); + const instructionC = instruction(txPercent(100)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]), + ]) + ), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([instructionA]), + singleTransactionPlan([instructionB]), + singleTransactionPlan([instructionC]), + ]) + ); +}); + +/** + * [NonDivSeq] ────────────────▶ [NonDivSeq] + * │ │ + * ├── [A: 100%] ├── [Tx: A] + * └── [Seq] ├── [Tx: B] + * ├── [B: 100%] └── [Tx: C] + * └── [C: 100%] + */ +test('it simplifies divisible sequential plans inside non-divisible sequential plans', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(100)); + const instructionB = instruction(txPercent(100)); + const instructionC = instruction(txPercent(100)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + sequentialInstructionPlan([ + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]), + ]) + ), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([instructionA]), + singleTransactionPlan([instructionB]), + singleTransactionPlan([instructionC]), + ]) + ); +}); + +/** + * [Seq] ──────────────────────▶ [Seq] + * │ │ + * ├── [A: 100%] ├── [Tx: A] + * └── [NonDivSeq] └── [NonDivSeq] + * ├── [B: 100%] ├── [Tx: B] + * └── [C: 100%] └── [Tx: C] + */ +test('it does not simplify non-divisible sequential plans inside divisible sequential plans', async (t) => { + const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(100)); + const instructionB = instruction(txPercent(100)); + const instructionC = instruction(txPercent(100)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA]), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([instructionB]), + singleTransactionPlan([instructionC]), + ]), + ]) + ); +}); + /** * [Par] ───────────────────▶ [Tx: A + B + C + D] * │ @@ -783,9 +955,9 @@ test('it plans non-divisible sequentials plans with parallel children', async (t * [NonDivSeq] ─────────────▶ [NonDivSeq] * │ │ * ├── [Seq] ├── [Tx: A + B] - * │ ├── [A: 50%] └── [Seq] - * │ └── [B: 50%] ├── [Tx: C] - * └── [Seq] └── [Tx: D] + * │ ├── [A: 50%] ├── [Tx: C] + * │ └── [B: 50%] └── [Tx: D] + * └── [Seq] * ├── [C: 100%] * └── [D: 100%] */ @@ -813,10 +985,8 @@ test('it plans non-divisible sequentials plans with divisible sequential childre ), nonDivisibleSequentialTransactionPlan([ singleTransactionPlan([instructionA, instructionB]), - sequentialTransactionPlan([ - singleTransactionPlan([instructionC]), - singleTransactionPlan([instructionD]), - ]), + singleTransactionPlan([instructionC]), + singleTransactionPlan([instructionD]), ]) ); }); From f9c08602462f99ad9420031583c4d04eecd50d2a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 14:01:29 +0100 Subject: [PATCH 025/112] wip --- clients/js/src/instructionPlansDraft/transactionPlanner.ts | 3 ++- .../js/test/instructionPlansDraft/transactionPlanner.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index f6e72de..9ec441c 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -172,7 +172,8 @@ async function traverseSequential( if (transactionPlan) { candidate = getSequentialCandidate(transactionPlan); const newPlans = - transactionPlan.kind === 'sequential' && transactionPlan.divisible + transactionPlan.kind === 'sequential' && + (transactionPlan.divisible || !instructionPlan.divisible) ? transactionPlan.plans : [transactionPlan]; transactionPlans.push(...newPlans); diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 629daba..7331496 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -653,7 +653,7 @@ test('it simplifies non-divisible sequential plans with one child or less', asyn * ├── [B: 100%] └── [Tx: C] * └── [C: 100%] */ -test.skip('it simplifies nested non-divisible sequential plans', async (t) => { +test('it simplifies nested non-divisible sequential plans', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); From 3d7b052048cb4987144177ecbff1fe31ee069f70 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 14:13:24 +0100 Subject: [PATCH 026/112] wip --- .../instructionPlansDraft/instructionPlan.ts | 20 ++++++++++++------- .../transactionPlanner.ts | 8 ++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 06452a3..f7d6021 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -4,7 +4,7 @@ export type InstructionPlan = | SequentialInstructionPlan | ParallelInstructionPlan | SingleInstructionPlan - | DynamicInstructionPlan; + | IterableInstructionPlan; export type SequentialInstructionPlan = Readonly<{ kind: 'sequential'; @@ -24,12 +24,18 @@ export type SingleInstructionPlan< instruction: TInstruction; }>; -export type DynamicInstructionPlan< +export type IterableInstructionPlan< TInstruction extends IInstruction = IInstruction, > = Readonly<{ - kind: 'dynamic'; - instructionFactory: (bytesAvailable: number) => { - instruction: TInstruction | null; - hasMore: boolean; - }; + kind: 'iterable'; + getIterator: () => InstructionIterator; +}>; + +export type InstructionIterator< + TInstruction extends IInstruction = IInstruction, +> = Readonly<{ + hasNext: () => boolean; + getNext: (bytes: number) => TInstruction | null; + getMax: () => TInstruction | null; + commitNext: (bytes: number) => void; }>; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 9ec441c..365919d 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -129,8 +129,8 @@ async function traverse( return await traverseParallel(instructionPlan, context); case 'single': return await traverseSingle(instructionPlan, context); - case 'dynamic': - throw new Error('Dynamic plans are not supported yet.'); + case 'iterable': + throw new Error('Iterable plans are not supported yet.'); default: instructionPlan satisfies never; throw new Error( @@ -280,8 +280,8 @@ function getAllSingleInstructionPlans( if (instructionPlan.kind === 'parallel') { return instructionPlan.plans.flatMap(getAllSingleInstructionPlans); } - if (instructionPlan.kind === 'dynamic') { - throw new Error('Dynamic plans are not supported yet.'); + if (instructionPlan.kind === 'iterable') { + throw new Error('Iterable plans are not supported yet.'); } return [instructionPlan]; } From f780871b109e89a0d82ea20c349d57d4889826ab Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 14:24:49 +0100 Subject: [PATCH 027/112] wip --- .../js/src/instructionPlansDraft/instructionPlan.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index f7d6021..972d1bd 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -34,8 +34,12 @@ export type IterableInstructionPlan< export type InstructionIterator< TInstruction extends IInstruction = IInstruction, > = Readonly<{ + /** Checks whether there are more instructions to retrieve. */ hasNext: () => boolean; - getNext: (bytes: number) => TInstruction | null; - getMax: () => TInstruction | null; - commitNext: (bytes: number) => void; + /** Get the next instruction. */ + next: (bytes: number) => TInstruction; + /** Get the next instruction without advancing the iterator. */ + peek: (bytes: number) => TInstruction; + /** Tries to get all remaining instructions or return `null` if not possible */ + peekAll: () => TInstruction[] | null; }>; From aa42009394aee1ecddadabf18e21aff41bb6471e Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 4 Apr 2025 14:59:39 +0100 Subject: [PATCH 028/112] wip --- .../instructionPlansDraft/instructionPlan.ts | 55 ++++++++++++++++--- .../transactionPlanner.ts | 8 ++- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 972d1bd..486442c 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -1,4 +1,12 @@ -import { IInstruction } from '@solana/kit'; +import { + appendTransactionMessageInstruction, + BaseTransactionMessage, + IInstruction, +} from '@solana/kit'; +import { + getTransactionSize, + TRANSACTION_SIZE_LIMIT, +} from './transactionPlanner'; export type InstructionPlan = | SequentialInstructionPlan @@ -34,12 +42,45 @@ export type IterableInstructionPlan< export type InstructionIterator< TInstruction extends IInstruction = IInstruction, > = Readonly<{ + /** Get all remaining instructions or return `null` if not possible */ + all: () => TInstruction[] | null; /** Checks whether there are more instructions to retrieve. */ hasNext: () => boolean; - /** Get the next instruction. */ - next: (bytes: number) => TInstruction; - /** Get the next instruction without advancing the iterator. */ - peek: (bytes: number) => TInstruction; - /** Tries to get all remaining instructions or return `null` if not possible */ - peekAll: () => TInstruction[] | null; + /** Get the next instruction for the given transaction message or return `null` if not possible. */ + next: (transactionMessage: BaseTransactionMessage) => TInstruction | null; }>; + +export function getLinearIterableInstructionPlan({ + getInstruction, + totalBytes, +}: { + getInstruction: (offset: number, length: number) => IInstruction; + totalBytes: number; +}): IterableInstructionPlan { + return { + kind: 'iterable', + getIterator: () => { + let offset = 0; + return { + all: () => [getInstruction(offset, totalBytes - offset)], + hasNext: () => offset < totalBytes, + next: (tx: BaseTransactionMessage) => { + const baseTransactionSize = getTransactionSize( + appendTransactionMessageInstruction(getInstruction(offset, 0), tx) + ); + const length = + TRANSACTION_SIZE_LIMIT - + baseTransactionSize - + 2; /* Leeway for shortU16 numbers in transaction headers. */ + + if (length <= 0) { + return null; + } + + offset += length; + return getInstruction(offset, length); + }, + }; + }, + }; +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 365919d..1874091 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -22,7 +22,9 @@ import { } from './instructionPlan'; import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; -const TRANSACTION_SIZE_LIMIT = +// TODO: This would need to be a first-class citizen of @solana/kit. +// We should consider two constants. One for `1280` and another for `1_280 - 48`. +export const TRANSACTION_SIZE_LIMIT = 1_280 - 40 /* 40 bytes is the size of the IPv6 header. */ - 8; /* 8 bytes is the size of the fragment header. */ @@ -311,6 +313,10 @@ export function getRemainingTransactionSize(message: BaseTransactionMessage) { return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); } +// TODO: This would need to be a first-class citizen of @solana/kit. +// It should accepts both `Transaction` and `BaseTransactionMessage` instances. +// Over time, efforts should be made to improve the performance of this function. +// E.g. maybe we don't need to compile the transaction message to get the size. export function getTransactionSize( message: BaseTransactionMessage & Partial ): number { From 849aca969f78b940a452a052c71b86c6c9699443 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 11:36:46 +0100 Subject: [PATCH 029/112] wip --- .../instructionPlansDraft/instructionPlan.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 486442c..f5cfc21 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -84,3 +84,39 @@ export function getLinearIterableInstructionPlan({ }, }; } + +const REALLOC_LIMIT = 10_240; + +export function getReallocIterableInstructionPlan({ + getInstruction, + totalSize, +}: { + getInstruction: (size: number) => IInstruction; + totalSize: number; +}): IterableInstructionPlan { + return { + kind: 'iterable', + getIterator: () => { + let instructionIndex = 0; + const numberOfInstructions = Math.ceil(totalSize / REALLOC_LIMIT); + const lastInstructionSize = totalSize % REALLOC_LIMIT; + const instructions = new Array(numberOfInstructions) + .fill(0) + .map((_, i) => { + const size = + i === numberOfInstructions - 1 + ? lastInstructionSize + : REALLOC_LIMIT; + return getInstruction(size); + }); + return { + all: () => instructions, + hasNext: () => instructionIndex < numberOfInstructions, + next: () => + instructionIndex < numberOfInstructions + ? instructions[instructionIndex++] + : null, + }; + }, + }; +} From a4fbb9684779e6d7f260f353ac565f4e54a22a9c Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 11:40:47 +0100 Subject: [PATCH 030/112] wip --- .../js/src/instructionPlansDraft/transactionPlanner.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 1874091..467b146 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -23,11 +23,12 @@ import { import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; // TODO: This would need to be a first-class citizen of @solana/kit. -// We should consider two constants. One for `1280` and another for `1_280 - 48`. -export const TRANSACTION_SIZE_LIMIT = - 1_280 - - 40 /* 40 bytes is the size of the IPv6 header. */ - +export const TRANSACTION_PACKET_SIZE = 1280; +export const TRANSACTION_PACKET_HEADER = + 40 /* 40 bytes is the size of the IPv6 header. */ + 8; /* 8 bytes is the size of the fragment header. */ +export const TRANSACTION_SIZE_LIMIT = + TRANSACTION_PACKET_SIZE - TRANSACTION_PACKET_HEADER; type Mutable = { -readonly [P in keyof T]: T[P] }; From 08eb4c911a6dc1846aca008580a4a05a1ffb7d7d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 11:58:29 +0100 Subject: [PATCH 031/112] wip --- .../transactionPlanner.ts | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 467b146..20745ac 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -7,6 +7,7 @@ import { compileTransaction, createTransactionMessage, getTransactionEncoder, + IInstruction, ITransactionMessageWithFeePayer, pipe, setTransactionMessageFeePayer, @@ -16,6 +17,7 @@ import { } from '@solana/kit'; import { InstructionPlan, + IterableInstructionPlan, ParallelInstructionPlan, SequentialInstructionPlan, SingleInstructionPlan, @@ -64,7 +66,7 @@ export function createBaseTransactionPlanner({ }): TransactionPlanner { return async (originalInstructionPlan, config): Promise => { const createSingleTransactionPlan = async ( - instructions: SingleInstructionPlan[] = [] + instructions: IInstruction[] = [] ): Promise => { const plan: SingleTransactionPlan = { kind: 'single', @@ -82,10 +84,10 @@ export function createBaseTransactionPlanner({ const addInstructionsToSingleTransactionPlan = async ( plan: SingleTransactionPlan, - instructions: SingleInstructionPlan[] + instructions: IInstruction[] ): Promise => { let message = appendTransactionMessageInstructions( - instructions.map((i) => i.instruction), + instructions, plan.message ); if (config?.newInstructionsTransformer) { @@ -113,11 +115,11 @@ type TraverseContext = { parent: InstructionPlan | null; parentCandidates: SingleTransactionPlan[]; createSingleTransactionPlan: ( - instructions?: SingleInstructionPlan[] + instructions?: IInstruction[] ) => Promise; addInstructionsToSingleTransactionPlan: ( plan: SingleTransactionPlan, - instructions: SingleInstructionPlan[] + instructions: IInstruction[] ) => Promise; }; @@ -133,7 +135,7 @@ async function traverse( case 'single': return await traverseSingle(instructionPlan, context); case 'iterable': - throw new Error('Iterable plans are not supported yet.'); + throw await traverseIterable(instructionPlan, context); default: instructionPlan satisfies never; throw new Error( @@ -151,9 +153,11 @@ async function traverseSequential( context.parent && (context.parent.kind === 'parallel' || !instructionPlan.divisible); if (mustEntirelyFitInCandidate) { - const allInstructions = getAllSingleInstructionPlans(instructionPlan); - candidate = selectCandidate(context.parentCandidates, allInstructions); - if (candidate) { + const allInstructions = getAllInstructions(instructionPlan); + candidate = allInstructions + ? selectCandidate(context.parentCandidates, allInstructions) + : null; + if (candidate && allInstructions) { await context.addInstructionsToSingleTransactionPlan( candidate, allInstructions @@ -229,16 +233,27 @@ async function traverseSingle( instructionPlan: SingleInstructionPlan, context: TraverseContext ): Promise { - const candidate = selectCandidate(context.parentCandidates, [ - instructionPlan, - ]); + const ix = instructionPlan.instruction; + const candidate = selectCandidate(context.parentCandidates, [ix]); if (candidate) { - await context.addInstructionsToSingleTransactionPlan(candidate, [ - instructionPlan, - ]); + await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); return null; } - return await context.createSingleTransactionPlan([instructionPlan]); + return await context.createSingleTransactionPlan([ix]); +} + +async function traverseIterable( + _instructionPlan: IterableInstructionPlan, + _context: TraverseContext +): Promise { + return await Promise.resolve(null); + // const ix = instructionPlan.instruction; + // const candidate = selectCandidate(context.parentCandidates, [ix]); + // if (candidate) { + // await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); + // return null; + // } + // return await context.createSingleTransactionPlan([ix]); } function getSequentialCandidate( @@ -273,38 +288,43 @@ function getAllSingleTransactionPlans( return [transactionPlan]; } -// TODO: This will need tweaking when adding support for dynamic instructions. -function getAllSingleInstructionPlans( +function getAllInstructions( instructionPlan: InstructionPlan -): SingleInstructionPlan[] { - if (instructionPlan.kind === 'sequential') { - return instructionPlan.plans.flatMap(getAllSingleInstructionPlans); - } - if (instructionPlan.kind === 'parallel') { - return instructionPlan.plans.flatMap(getAllSingleInstructionPlans); +): IInstruction[] | null { + if (instructionPlan.kind === 'single') { + return [instructionPlan.instruction]; } if (instructionPlan.kind === 'iterable') { throw new Error('Iterable plans are not supported yet.'); } - return [instructionPlan]; + return instructionPlan.plans.reduce( + (acc, plan) => { + if (acc === null) return null; + const instructions = getAllInstructions(plan); + if (instructions === null) return null; + acc.push(...instructions); + return acc; + }, + [] as IInstruction[] | null + ); } function selectCandidate( candidates: SingleTransactionPlan[], - instructionPlans: SingleInstructionPlan[] + instructions: IInstruction[] ): SingleTransactionPlan | null { const firstValidCandidate = candidates.find((candidate) => - isValidCandidate(candidate, instructionPlans) + isValidCandidate(candidate, instructions) ); return firstValidCandidate ?? null; } function isValidCandidate( candidate: SingleTransactionPlan, - instructionPlans: SingleInstructionPlan[] + instructions: IInstruction[] ): boolean { const message = appendTransactionMessageInstructions( - instructionPlans.map((i) => i.instruction), + instructions, candidate.message ); return getRemainingTransactionSize(message) >= 0; From b2b9e0e4bf90e1a64eb2f8911cf37f463f15ba5f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 12:06:26 +0100 Subject: [PATCH 032/112] wip --- .../instructionPlansDraft/instructionPlan.ts | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index f5cfc21..5ba58b1 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -36,14 +36,15 @@ export type IterableInstructionPlan< TInstruction extends IInstruction = IInstruction, > = Readonly<{ kind: 'iterable'; + /** Get all the instructions in one go or return `null` if not possible */ + getAll: () => TInstruction[] | null; + /** Get an iterator for the instructions. */ getIterator: () => InstructionIterator; }>; export type InstructionIterator< TInstruction extends IInstruction = IInstruction, > = Readonly<{ - /** Get all remaining instructions or return `null` if not possible */ - all: () => TInstruction[] | null; /** 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. */ @@ -59,10 +60,10 @@ export function getLinearIterableInstructionPlan({ }): IterableInstructionPlan { return { kind: 'iterable', + getAll: () => [getInstruction(0, totalBytes)], getIterator: () => { let offset = 0; return { - all: () => [getInstruction(offset, totalBytes - offset)], hasNext: () => offset < totalBytes, next: (tx: BaseTransactionMessage) => { const baseTransactionSize = getTransactionSize( @@ -85,38 +86,40 @@ export function getLinearIterableInstructionPlan({ }; } -const REALLOC_LIMIT = 10_240; - -export function getReallocIterableInstructionPlan({ - getInstruction, - totalSize, -}: { - getInstruction: (size: number) => IInstruction; - totalSize: number; -}): IterableInstructionPlan { +export function getIterableInstructionPlanFromInstructions< + TInstruction extends IInstruction = IInstruction, +>(instructions: TInstruction[]): IterableInstructionPlan { return { kind: 'iterable', + getAll: () => instructions, getIterator: () => { let instructionIndex = 0; - const numberOfInstructions = Math.ceil(totalSize / REALLOC_LIMIT); - const lastInstructionSize = totalSize % REALLOC_LIMIT; - const instructions = new Array(numberOfInstructions) - .fill(0) - .map((_, i) => { - const size = - i === numberOfInstructions - 1 - ? lastInstructionSize - : REALLOC_LIMIT; - return getInstruction(size); - }); return { - all: () => instructions, - hasNext: () => instructionIndex < numberOfInstructions, + hasNext: () => instructionIndex < instructions.length, next: () => - instructionIndex < numberOfInstructions + instructionIndex < instructions.length ? instructions[instructionIndex++] : null, }; }, }; } + +const REALLOC_LIMIT = 10_240; + +export function getReallocIterableInstructionPlan({ + getInstruction, + totalSize, +}: { + getInstruction: (size: number) => IInstruction; + totalSize: number; +}): IterableInstructionPlan { + const numberOfInstructions = Math.ceil(totalSize / REALLOC_LIMIT); + const lastInstructionSize = totalSize % REALLOC_LIMIT; + const instructions = new Array(numberOfInstructions).fill(0).map((_, i) => { + const size = + i === numberOfInstructions - 1 ? lastInstructionSize : REALLOC_LIMIT; + return getInstruction(size); + }); + return getIterableInstructionPlanFromInstructions(instructions); +} From fe2990c1333c10730874d3eeb621c5d84f938900 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 12:08:31 +0100 Subject: [PATCH 033/112] wip --- .../src/instructionPlansDraft/transactionPlanner.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 20745ac..0fe451b 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -279,13 +279,10 @@ function getParallelCandidates( function getAllSingleTransactionPlans( transactionPlan: TransactionPlan ): SingleTransactionPlan[] { - if (transactionPlan.kind === 'sequential') { - return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); + if (transactionPlan.kind === 'single') { + return [transactionPlan]; } - if (transactionPlan.kind === 'parallel') { - return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); - } - return [transactionPlan]; + return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); } function getAllInstructions( @@ -295,7 +292,7 @@ function getAllInstructions( return [instructionPlan.instruction]; } if (instructionPlan.kind === 'iterable') { - throw new Error('Iterable plans are not supported yet.'); + return instructionPlan.getAll(); } return instructionPlan.plans.reduce( (acc, plan) => { From bfaa1d8fa18aebca4669e170ec5769b183eb19bf Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 12:27:29 +0100 Subject: [PATCH 034/112] wip --- .../instructionPlansDraft/instructionPlan.ts | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 5ba58b1..33d3d01 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -96,10 +96,23 @@ export function getIterableInstructionPlanFromInstructions< let instructionIndex = 0; return { hasNext: () => instructionIndex < instructions.length, - next: () => - instructionIndex < instructions.length - ? instructions[instructionIndex++] - : null, + next: (tx: BaseTransactionMessage) => { + if (instructionIndex >= instructions.length) { + return null; + } + + const instruction = instructions[instructionIndex]; + const transactionSize = getTransactionSize( + appendTransactionMessageInstruction(instruction, tx) + ); + + if (transactionSize > TRANSACTION_SIZE_LIMIT) { + return null; + } + + instructionIndex++; + return instruction; + }, }; }, }; @@ -116,10 +129,13 @@ export function getReallocIterableInstructionPlan({ }): IterableInstructionPlan { const numberOfInstructions = Math.ceil(totalSize / REALLOC_LIMIT); const lastInstructionSize = totalSize % REALLOC_LIMIT; - const instructions = new Array(numberOfInstructions).fill(0).map((_, i) => { - const size = - i === numberOfInstructions - 1 ? lastInstructionSize : REALLOC_LIMIT; - return getInstruction(size); - }); + const instructions = new Array(numberOfInstructions) + .fill(0) + .map((_, i) => + getInstruction( + i === numberOfInstructions - 1 ? lastInstructionSize : REALLOC_LIMIT + ) + ); + return getIterableInstructionPlanFromInstructions(instructions); } From 6856500ff726dd1120b4c72c6558fe6f97092ca3 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 12:47:23 +0100 Subject: [PATCH 035/112] wip --- .../transactionPlanner.ts | 63 ++++++++++++++++--- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 0fe451b..07bca94 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -16,6 +16,7 @@ import { TransactionVersion, } from '@solana/kit'; import { + InstructionIterator, InstructionPlan, IterableInstructionPlan, ParallelInstructionPlan, @@ -243,17 +244,46 @@ async function traverseSingle( } async function traverseIterable( - _instructionPlan: IterableInstructionPlan, - _context: TraverseContext + instructionPlan: IterableInstructionPlan, + context: TraverseContext ): Promise { - return await Promise.resolve(null); - // const ix = instructionPlan.instruction; - // const candidate = selectCandidate(context.parentCandidates, [ix]); - // if (candidate) { - // await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); - // return null; - // } - // return await context.createSingleTransactionPlan([ix]); + const iterator = instructionPlan.getIterator(); + const transactionPlans: SingleTransactionPlan[] = []; + const candidates = [...context.parentCandidates]; // TODO: Use some caching mechanism to avoid trying filled candidates. + + while (iterator.hasNext()) { + const candidateResult = selectCandidateForIterator(candidates, iterator); + if (candidateResult) { + const [candidate, ix] = candidateResult; + await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); + } else { + const newPlan = await context.createSingleTransactionPlan(); + const ix = iterator.next(newPlan.message); + if (!ix) { + throw new Error( + 'Could not fit `InterableInstructionPlan` into a transaction' + ); + } + await context.addInstructionsToSingleTransactionPlan(newPlan, [ix]); + transactionPlans.push(newPlan); + candidates.push(newPlan); + } + } + + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length === 0) { + return null; + } + if (context.parent?.kind === 'sequential') { + return { + kind: 'sequential', + divisible: context.parent.divisible, + plans: transactionPlans, + }; + } + return { kind: 'parallel', plans: transactionPlans }; } function getSequentialCandidate( @@ -306,6 +336,19 @@ function getAllInstructions( ); } +function selectCandidateForIterator( + candidates: SingleTransactionPlan[], + iterator: InstructionIterator +): [SingleTransactionPlan, IInstruction] | null { + for (const candidate of candidates) { + const ix = iterator.next(candidate.message); + if (ix) { + return [candidate, ix]; + } + } + return null; +} + function selectCandidate( candidates: SingleTransactionPlan[], instructions: IInstruction[] From 4f13f743e592194863fe75c9dbd28ee9fcb1636b Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 13:01:47 +0100 Subject: [PATCH 036/112] wip --- .../instructionPlansDraft/transactionPlanner.ts | 15 ++++++++------- .../_instructionPlanHelpers.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 07bca94..9e4b3c4 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -276,14 +276,15 @@ async function traverseIterable( if (transactionPlans.length === 0) { return null; } - if (context.parent?.kind === 'sequential') { - return { - kind: 'sequential', - divisible: context.parent.divisible, - plans: transactionPlans, - }; + if (context.parent?.kind === 'parallel') { + return { kind: 'parallel', plans: transactionPlans }; } - return { kind: 'parallel', plans: transactionPlans }; + return { + kind: 'sequential', + divisible: + context.parent?.kind === 'sequential' ? context.parent.divisible : true, + plans: transactionPlans, + }; } function getSequentialCandidate( diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index ace62cd..4a5610f 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -1,7 +1,9 @@ import { Address, BaseTransactionMessage, IInstruction } from '@solana/kit'; import { + getLinearIterableInstructionPlan, getTransactionSize, InstructionPlan, + IterableInstructionPlan, ParallelInstructionPlan, SequentialInstructionPlan, SingleInstructionPlan, @@ -35,6 +37,16 @@ export function singleInstructionPlan( return { kind: 'single', instruction }; } +export function linearIterableInstructionPlan( + totalBytes: number, + instructionFactory: (bytes: number) => IInstruction +): IterableInstructionPlan { + return getLinearIterableInstructionPlan({ + totalBytes, + getInstruction: (offset, length) => instructionFactory(length - offset), + }); +} + export function instructionFactory() { let counter = 0n; return (bytes: number): IInstruction => { From ef9a11c38532d207b0f69d2b14367f037c582c68 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 14:28:39 +0100 Subject: [PATCH 037/112] wip --- .../js/src/instructionPlansDraft/instructionPlan.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 33d3d01..9f7f367 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -69,17 +69,19 @@ export function getLinearIterableInstructionPlan({ const baseTransactionSize = getTransactionSize( appendTransactionMessageInstruction(getInstruction(offset, 0), tx) ); - const length = + const maxLength = TRANSACTION_SIZE_LIMIT - baseTransactionSize - 2; /* Leeway for shortU16 numbers in transaction headers. */ - if (length <= 0) { + if (maxLength <= 0) { return null; } - offset += length; - return getInstruction(offset, length); + const length = Math.min(totalBytes - offset, maxLength); + const instruction = getInstruction(offset, length); + offset += maxLength; + return instruction; }, }; }, From 1c238ac19736ae76370f4691db34d9f7a66791c6 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 15:23:57 +0100 Subject: [PATCH 038/112] wip --- .../_instructionPlanHelpers.ts | 58 ++++++++++++++----- .../transactionPlanner.test.ts | 32 ++++++++++ 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index 4a5610f..fabff5c 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -1,4 +1,11 @@ -import { Address, BaseTransactionMessage, IInstruction } from '@solana/kit'; +import { + Address, + BaseTransactionMessage, + fixEncoderSize, + getAddressDecoder, + getU64Encoder, + IInstruction, +} from '@solana/kit'; import { getLinearIterableInstructionPlan, getTransactionSize, @@ -37,28 +44,51 @@ export function singleInstructionPlan( return { kind: 'single', instruction }; } -export function linearIterableInstructionPlan( - totalBytes: number, - instructionFactory: (bytes: number) => IInstruction -): IterableInstructionPlan { - return getLinearIterableInstructionPlan({ - totalBytes, - getInstruction: (offset, length) => instructionFactory(length - offset), - }); +export function instructionIteratorFactory() { + const baseCounter = 1_000_000_000n; + const iteratorIncrement = 1_000_000_000n; + let iteratorCounter = 0n; + return ( + totalBytes: number + ): IterableInstructionPlan & { + get: (bytes: number, index: number) => IInstruction; + } => { + const get = instructionFactory(baseCounter + iteratorCounter); + iteratorCounter += iteratorIncrement; + + const iterator = getLinearIterableInstructionPlan({ + totalBytes, + getInstruction: (offset, length) => { + console.log({ offset, length }); + return get(length + MINIMUM_INSTRUCTION_SIZE); + }, + }); + + return { ...iterator, get }; + }; } -export function instructionFactory() { +export function instructionFactory(baseCounter: bigint = 0n) { + const counterEncoder = fixEncoderSize(getU64Encoder(), 32); + const addressDecoder = getAddressDecoder(); + const getProgramAddress = (counter: bigint): Address => + addressDecoder.decode(counterEncoder.encode(counter)); + let counter = 0n; - return (bytes: number): IInstruction => { + return (bytes: number, counterOverride?: number): IInstruction => { if (bytes < MINIMUM_INSTRUCTION_SIZE) { throw new Error( `Instruction size must be at least ${MINIMUM_INSTRUCTION_SIZE} bytes` ); } - const programAddress = BigInt('11111111111111111111111111111111') + counter; - counter += 1n; + const currentCounter = + baseCounter + + (counterOverride === undefined ? counter : BigInt(counterOverride)); + if (counterOverride === undefined) { + counter += 1n; + } return { - programAddress: programAddress.toString() as Address, + programAddress: getProgramAddress(currentCounter), data: new Uint8Array(bytes - MINIMUM_INSTRUCTION_SIZE), }; }; diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 7331496..a49201a 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -2,6 +2,7 @@ import test from 'ava'; import { createBaseTransactionPlanner } from '../../src'; import { instructionFactory, + instructionIteratorFactory, nonDivisibleSequentialInstructionPlan, parallelInstructionPlan, sequentialInstructionPlan, @@ -14,10 +15,12 @@ import { sequentialTransactionPlan, singleTransactionPlanFactory, } from './_transactionPlanHelpers'; +import { createTransactionMessage, IInstruction } from '@solana/kit'; function defaultFactories() { return { instruction: instructionFactory(), + iterator: instructionIteratorFactory(), txPercent: transactionPercentFactory(), singleTransactionPlan: singleTransactionPlanFactory(), }; @@ -991,6 +994,35 @@ test('it plans non-divisible sequentials plans with divisible sequential childre ); }); +// TODO +test.skip('it iterate over iterable instruction plans', async (t) => { + const { txPercent, iterator } = defaultFactories(); + // const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); + // const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionAx = iterator(txPercent(200)); + + const all = instructionAx.getAll(); + const foo = instructionAx.getIterator(); + const ixs: IInstruction[] = []; + while (foo.hasNext()) { + console.log('HAS_NEXT'); + const ix = foo.next(createTransactionMessage({ version: 0 })); + if (ix) ixs.push(ix); + } + console.log({ all, ixs }); + + await Promise.resolve(null); + t.pass(); + // t.deepEqual( + // await planner(instructionAx), + // sequentialTransactionPlan([ + // singleTransactionPlan([instruction(txPercent(100))]), + // singleTransactionPlan([instruction(txPercent(100))]), + // ]) + // ); +}); + /** * [Par] ───────────────────────────▶ [Par] * │ │ From 1359611b6e021f0093246d2779f4393bfaeec4ea Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 15:36:05 +0100 Subject: [PATCH 039/112] wip --- .../transactionPlanner.ts | 2 +- .../transactionPlanner.test.ts | 39 +++++++------------ 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 9e4b3c4..13d9f4a 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -136,7 +136,7 @@ async function traverse( case 'single': return await traverseSingle(instructionPlan, context); case 'iterable': - throw await traverseIterable(instructionPlan, context); + return await traverseIterable(instructionPlan, context); default: instructionPlan satisfies never; throw new Error( diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index a49201a..e3139b2 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -15,7 +15,6 @@ import { sequentialTransactionPlan, singleTransactionPlanFactory, } from './_transactionPlanHelpers'; -import { createTransactionMessage, IInstruction } from '@solana/kit'; function defaultFactories() { return { @@ -996,31 +995,19 @@ test('it plans non-divisible sequentials plans with divisible sequential childre // TODO test.skip('it iterate over iterable instruction plans', async (t) => { - const { txPercent, iterator } = defaultFactories(); - // const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - // const planner = createBaseTransactionPlanner({ version: 0 }); - - const instructionAx = iterator(txPercent(200)); - - const all = instructionAx.getAll(); - const foo = instructionAx.getIterator(); - const ixs: IInstruction[] = []; - while (foo.hasNext()) { - console.log('HAS_NEXT'); - const ix = foo.next(createTransactionMessage({ version: 0 })); - if (ix) ixs.push(ix); - } - console.log({ all, ixs }); - - await Promise.resolve(null); - t.pass(); - // t.deepEqual( - // await planner(instructionAx), - // sequentialTransactionPlan([ - // singleTransactionPlan([instruction(txPercent(100))]), - // singleTransactionPlan([instruction(txPercent(100))]), - // ]) - // ); + const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const iteratorIx = iterator(txPercent(200)); + + t.deepEqual( + await planner(iteratorIx), + sequentialTransactionPlan([ + singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]), + singleTransactionPlan([iteratorIx.get(txPercent(100), 1)]), + singleTransactionPlan([iteratorIx.get(70 + 35, 2)]), + ]) + ); }); /** From 47604e32ddc97b8a217170d475476ad3986de3cd Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 15:42:40 +0100 Subject: [PATCH 040/112] wip --- clients/js/src/instructionPlansDraft/instructionPlan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 9f7f367..4c81b7c 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -80,7 +80,7 @@ export function getLinearIterableInstructionPlan({ const length = Math.min(totalBytes - offset, maxLength); const instruction = getInstruction(offset, length); - offset += maxLength; + offset += length; return instruction; }, }; From 1ecf66f5d6b45612e6c7283ceacb35055a867c6a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 15:50:00 +0100 Subject: [PATCH 041/112] wip --- .../_instructionPlanHelpers.ts | 45 ++++++++++++++----- .../transactionPlanner.test.ts | 4 +- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index fabff5c..f8d4f34 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -1,5 +1,6 @@ import { Address, + appendTransactionMessageInstruction, BaseTransactionMessage, fixEncoderSize, getAddressDecoder, @@ -7,13 +8,13 @@ import { IInstruction, } from '@solana/kit'; import { - getLinearIterableInstructionPlan, getTransactionSize, InstructionPlan, IterableInstructionPlan, ParallelInstructionPlan, SequentialInstructionPlan, SingleInstructionPlan, + TRANSACTION_SIZE_LIMIT, } from '../../src'; const MINIMUM_INSTRUCTION_SIZE = 35; @@ -53,18 +54,42 @@ export function instructionIteratorFactory() { ): IterableInstructionPlan & { get: (bytes: number, index: number) => IInstruction; } => { - const get = instructionFactory(baseCounter + iteratorCounter); + const getInstruction = instructionFactory(baseCounter + iteratorCounter); iteratorCounter += iteratorIncrement; + const baseInstruction = getInstruction(MINIMUM_INSTRUCTION_SIZE, 0); - const iterator = getLinearIterableInstructionPlan({ - totalBytes, - getInstruction: (offset, length) => { - console.log({ offset, length }); - return get(length + MINIMUM_INSTRUCTION_SIZE); - }, - }); + return { + get: getInstruction, + kind: 'iterable', + getAll: () => [getInstruction(totalBytes, 0)], + getIterator: () => { + let offset = 0; + return { + hasNext: () => offset < totalBytes, + next: (tx: BaseTransactionMessage) => { + const baseTransactionSize = getTransactionSize( + appendTransactionMessageInstruction(baseInstruction, tx) + ); + const maxLength = + TRANSACTION_SIZE_LIMIT - + baseTransactionSize - + 2; /* Leeway for shortU16 numbers in transaction headers. */ + + if (maxLength <= 0) { + return null; + } - return { ...iterator, get }; + const length = + Math.min(totalBytes - offset, maxLength) + + MINIMUM_INSTRUCTION_SIZE; + + const instruction = getInstruction(length); + offset += length; + return instruction; + }, + }; + }, + }; }; } diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index e3139b2..92c8c78 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -998,14 +998,14 @@ test.skip('it iterate over iterable instruction plans', async (t) => { const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); - const iteratorIx = iterator(txPercent(200)); + const iteratorIx = iterator(txPercent(250)); t.deepEqual( await planner(iteratorIx), sequentialTransactionPlan([ singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]), singleTransactionPlan([iteratorIx.get(txPercent(100), 1)]), - singleTransactionPlan([iteratorIx.get(70 + 35, 2)]), + singleTransactionPlan([iteratorIx.get(txPercent(50), 2)]), ]) ); }); From d573f9c58e5e6dea9c1a50ec905810beb1b4ed62 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 16:07:28 +0100 Subject: [PATCH 042/112] wip --- .../instructionPlansDraft/_instructionPlanHelpers.ts | 7 ++++--- .../instructionPlansDraft/transactionPlanner.test.ts | 10 ++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index f8d4f34..8415358 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -79,9 +79,10 @@ export function instructionIteratorFactory() { return null; } - const length = - Math.min(totalBytes - offset, maxLength) + - MINIMUM_INSTRUCTION_SIZE; + const length = Math.min( + totalBytes - offset, + maxLength + MINIMUM_INSTRUCTION_SIZE + ); const instruction = getInstruction(length); offset += length; diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 92c8c78..9dd0c2d 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -993,8 +993,14 @@ test('it plans non-divisible sequentials plans with divisible sequential childre ); }); -// TODO -test.skip('it iterate over iterable instruction plans', async (t) => { +/** + * [A(x, 250%)] ─────────────▶ [Seq] + * │ + * ├── [Tx: A(1, 100%)] + * ├── [Tx: A(2, 100%)] + * └── [Tx: A(3, 50%)] + */ +test('it iterate over iterable instruction plans', async (t) => { const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); From 5482df88e5a61f3f307d057f9b9e89fc5f004941 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 16:14:02 +0100 Subject: [PATCH 043/112] wip --- .../transactionPlanner.test.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 9dd0c2d..0872aed 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1016,6 +1016,52 @@ test('it iterate over iterable instruction plans', async (t) => { ); }); +/** + * [Par] ────────────────────▶ [Par] + * │ │ + * └── [A(x, 250%)] ├── [Tx: A(1, 100%)] + * ├── [Tx: A(2, 100%)] + * └── [Tx: A(3, 50%)] + */ +test('it can handle parallel iterable instruction plans', async (t) => { + const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const iteratorIx = iterator(txPercent(250)); + + t.deepEqual( + await planner(parallelInstructionPlan([iteratorIx])), + parallelTransactionPlan([ + singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]), + singleTransactionPlan([iteratorIx.get(txPercent(100), 1)]), + singleTransactionPlan([iteratorIx.get(txPercent(50), 2)]), + ]) + ); +}); + +/** + * [NonDivSeq] ──────────────▶ [NonDivSeq] + * │ │ + * └── [A(x, 250%)] ├── [Tx: A(1, 100%)] + * ├── [Tx: A(2, 100%)] + * └── [Tx: A(3, 50%)] + */ +test('it can handle non-divisible sequential iterable instruction plans', async (t) => { + const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const iteratorIx = iterator(txPercent(250)); + + t.deepEqual( + await planner(nonDivisibleSequentialInstructionPlan([iteratorIx])), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]), + singleTransactionPlan([iteratorIx.get(txPercent(100), 1)]), + singleTransactionPlan([iteratorIx.get(txPercent(50), 2)]), + ]) + ); +}); + /** * [Par] ───────────────────────────▶ [Par] * │ │ From 8ded5ee2bf613942ab18eaed4f6f22ee209c58de Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 16:16:20 +0100 Subject: [PATCH 044/112] wip --- .../transactionPlanner.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 0872aed..7aac194 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1062,6 +1062,21 @@ 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 { txPercent, iterator, singleTransactionPlan } = defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const iteratorIx = iterator(txPercent(100)); + + t.deepEqual( + await planner(iteratorIx), + singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]) + ); +}); + /** * [Par] ───────────────────────────▶ [Par] * │ │ From bcbdc38937b4f6e4e0f0ef14af0bb413e9a68a60 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 16:25:24 +0100 Subject: [PATCH 045/112] wip --- .../transactionPlanner.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 7aac194..314ffa4 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1077,6 +1077,38 @@ test('it simplifies iterable instruction plans that fit in a single transaction' ); }); +/** + * TODO + */ +test.skip('it uses iterable instruction plans to fill gaps in parallel candidates', async (t) => { + const { txPercent, instruction, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const iteratorIx = iterator(txPercent(100)); + const instructionA = instruction(txPercent(60)); + const instructionB = instruction(txPercent(50)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + iteratorIx, + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA, iteratorIx.get(txPercent(40), 0)]), + singleTransactionPlan([instructionB, iteratorIx.get(txPercent(50), 1)]), + singleTransactionPlan([iteratorIx.get(txPercent(10), 2)]), + ]) + ); +}); + +// TODO: regardless of the order. +// TODO: with sequential plans. +// TODO: with non-divisible sequential plans. + /** * [Par] ───────────────────────────▶ [Par] * │ │ From b941c82c7c3ef9509731c456ed601dc5451140b6 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 16:57:27 +0100 Subject: [PATCH 046/112] wip --- clients/js/src/instructionPlansDraft/instructionPlan.ts | 2 +- .../js/test/instructionPlansDraft/_instructionPlanHelpers.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 4c81b7c..99f62c9 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -72,7 +72,7 @@ export function getLinearIterableInstructionPlan({ const maxLength = TRANSACTION_SIZE_LIMIT - baseTransactionSize - - 2; /* Leeway for shortU16 numbers in transaction headers. */ + 1; /* Leeway for shortU16 numbers in transaction headers. */ if (maxLength <= 0) { return null; diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index 8415358..c24f471 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -19,7 +19,7 @@ import { const MINIMUM_INSTRUCTION_SIZE = 35; const MINIMUM_TRANSACTION_SIZE = 136; -const MAXIMUM_TRANSACTION_SIZE = 1230; // 1280 - 48 (for header) - 2 (for shortU16) +const MAXIMUM_TRANSACTION_SIZE = 1231; // 1280 - 48 (for header) - 1 (for shortU16) export function parallelInstructionPlan( plans: InstructionPlan[] @@ -73,7 +73,7 @@ export function instructionIteratorFactory() { const maxLength = TRANSACTION_SIZE_LIMIT - baseTransactionSize - - 2; /* Leeway for shortU16 numbers in transaction headers. */ + 1; /* Leeway for shortU16 numbers in transaction headers. */ if (maxLength <= 0) { return null; From 471e06cdf6020de42780ec2f107939c69150af16 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 17:02:04 +0100 Subject: [PATCH 047/112] wip --- .../transactionPlanner.test.ts | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 314ffa4..73c8d74 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1016,6 +1016,31 @@ test('it iterate over iterable instruction plans', async (t) => { ); }); +/** + * [Seq] ───────────────────▶ [Tx: A + B(1, 50%)] + * │ + * ├── [A: 50%] + * └── [B(x, 50%)] + */ +test('it combines single instruction plans with iterable instruction plans', async (t) => { + const { txPercent, iterator, instruction, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(50)); + const iteratorB = iterator(txPercent(50)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + iteratorB, + ]) + ), + singleTransactionPlan([instructionA, iteratorB.get(txPercent(50), 0)]) + ); +}); + /** * [Par] ────────────────────▶ [Par] * │ │ @@ -1027,14 +1052,14 @@ test('it can handle parallel iterable instruction plans', async (t) => { const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); - const iteratorIx = iterator(txPercent(250)); + const iteratorA = iterator(txPercent(250)); t.deepEqual( - await planner(parallelInstructionPlan([iteratorIx])), + await planner(parallelInstructionPlan([iteratorA])), parallelTransactionPlan([ - singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]), - singleTransactionPlan([iteratorIx.get(txPercent(100), 1)]), - singleTransactionPlan([iteratorIx.get(txPercent(50), 2)]), + singleTransactionPlan([iteratorA.get(txPercent(100), 0)]), + singleTransactionPlan([iteratorA.get(txPercent(100), 1)]), + singleTransactionPlan([iteratorA.get(txPercent(50), 2)]), ]) ); }); @@ -1050,14 +1075,14 @@ test('it can handle non-divisible sequential iterable instruction plans', async const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); - const iteratorIx = iterator(txPercent(250)); + const iteratorA = iterator(txPercent(250)); t.deepEqual( - await planner(nonDivisibleSequentialInstructionPlan([iteratorIx])), + await planner(nonDivisibleSequentialInstructionPlan([iteratorA])), nonDivisibleSequentialTransactionPlan([ - singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]), - singleTransactionPlan([iteratorIx.get(txPercent(100), 1)]), - singleTransactionPlan([iteratorIx.get(txPercent(50), 2)]), + singleTransactionPlan([iteratorA.get(txPercent(100), 0)]), + singleTransactionPlan([iteratorA.get(txPercent(100), 1)]), + singleTransactionPlan([iteratorA.get(txPercent(50), 2)]), ]) ); }); @@ -1069,11 +1094,11 @@ test('it simplifies iterable instruction plans that fit in a single transaction' const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); - const iteratorIx = iterator(txPercent(100)); + const iteratorA = iterator(txPercent(100)); t.deepEqual( - await planner(iteratorIx), - singleTransactionPlan([iteratorIx.get(txPercent(100), 0)]) + await planner(iteratorA), + singleTransactionPlan([iteratorA.get(txPercent(100), 0)]) ); }); @@ -1085,22 +1110,22 @@ test.skip('it uses iterable instruction plans to fill gaps in parallel candidate defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); - const iteratorIx = iterator(txPercent(100)); - const instructionA = instruction(txPercent(60)); - const instructionB = instruction(txPercent(50)); + const iteratorA = iterator(txPercent(100)); + const instructionB = instruction(txPercent(60)); + const instructionC = instruction(txPercent(50)); t.deepEqual( await planner( parallelInstructionPlan([ - singleInstructionPlan(instructionA), singleInstructionPlan(instructionB), - iteratorIx, + singleInstructionPlan(instructionC), + iteratorA, ]) ), parallelTransactionPlan([ - singleTransactionPlan([instructionA, iteratorIx.get(txPercent(40), 0)]), - singleTransactionPlan([instructionB, iteratorIx.get(txPercent(50), 1)]), - singleTransactionPlan([iteratorIx.get(txPercent(10), 2)]), + singleTransactionPlan([instructionB, iteratorA.get(txPercent(40), 0)]), + singleTransactionPlan([instructionC, iteratorA.get(txPercent(50), 1)]), + singleTransactionPlan([iteratorA.get(txPercent(10), 2)]), ]) ); }); From 1d74601b2fc04648bcdcb01bc575eb3974ceb1bb Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sat, 5 Apr 2025 17:08:56 +0100 Subject: [PATCH 048/112] wip --- .../test/instructionPlansDraft/transactionPlanner.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 73c8d74..5d87203 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1110,8 +1110,8 @@ test.skip('it uses iterable instruction plans to fill gaps in parallel candidate defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); - const iteratorA = iterator(txPercent(100)); - const instructionB = instruction(txPercent(60)); + const iteratorA = iterator(txPercent(125)); + const instructionB = instruction(txPercent(75)); const instructionC = instruction(txPercent(50)); t.deepEqual( @@ -1123,9 +1123,9 @@ test.skip('it uses iterable instruction plans to fill gaps in parallel candidate ]) ), parallelTransactionPlan([ - singleTransactionPlan([instructionB, iteratorA.get(txPercent(40), 0)]), + singleTransactionPlan([instructionB, iteratorA.get(txPercent(25), 0)]), singleTransactionPlan([instructionC, iteratorA.get(txPercent(50), 1)]), - singleTransactionPlan([iteratorA.get(txPercent(10), 2)]), + singleTransactionPlan([iteratorA.get(txPercent(50), 2)]), ]) ); }); From 290000e044a9687e602b3d98e2690a68fdc87d8f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 20:04:20 +0100 Subject: [PATCH 049/112] wip --- .../transactionPlanner.test.ts | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 5d87203..2f73ec5 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1103,14 +1103,50 @@ test('it simplifies iterable instruction plans that fit in a single transaction' }); /** - * TODO + * [Par] ───────────────────────────▶ [Par] + * │ │ + * ├── [A: 75%] ├── [Tx: A + C(1, 25%)] + * ├── [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) => { + const { txPercent, instruction, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(75)); + const instructionB = instruction(txPercent(50)); + const iteratorC = iterator(txPercent(25) + txPercent(50) + txPercent(50)); // 125% + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + iteratorC, + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA, iteratorC.get(txPercent(25), 0)]), + singleTransactionPlan([instructionB, iteratorC.get(txPercent(50), 1)]), + singleTransactionPlan([iteratorC.get(txPercent(50), 2)]), + ]) + ); +}); + +/** + * [Par] ───────────────────────────▶ [Par] + * │ │ + * ├── [A(x, 125%)] ├── [Tx: B + A(1, 25%)] + * ├── [C: 50%] ├── [Tx: C + A(2, 50%)] + * └── [B: 75%] └── [Tx: A(3, 50%)] */ -test.skip('it uses iterable instruction plans to fill gaps in parallel candidates', async (t) => { +test.skip('it uses iterable instruction plans to fill gaps in parallel candidates regardless of the order', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); - const iteratorA = iterator(txPercent(125)); + const iteratorA = iterator(txPercent(25) + txPercent(50) + txPercent(50)); // 125% const instructionB = instruction(txPercent(75)); const instructionC = instruction(txPercent(50)); @@ -1130,7 +1166,6 @@ test.skip('it uses iterable instruction plans to fill gaps in parallel candidate ); }); -// TODO: regardless of the order. // TODO: with sequential plans. // TODO: with non-divisible sequential plans. From 56d5cdab6f571f39c75d3c36f9ed7f45d0089095 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 20:12:27 +0100 Subject: [PATCH 050/112] wip --- .../js/src/instructionPlansDraft/transactionPlanner.ts | 9 ++++++++- .../instructionPlansDraft/transactionPlanner.test.ts | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 13d9f4a..8e379c2 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -206,7 +206,14 @@ async function traverseParallel( ): Promise { const candidates: SingleTransactionPlan[] = [...context.parentCandidates]; const transactionPlans: TransactionPlan[] = []; - for (const plan of instructionPlan.plans) { + + // Reorder children so iterable plans are last. + const sortedChildren = [ + ...instructionPlan.plans.filter((plan) => plan.kind !== 'iterable'), + ...instructionPlan.plans.filter((plan) => plan.kind === 'iterable'), + ]; + + for (const plan of sortedChildren) { const transactionPlan = await traverse(plan, { ...context, parent: instructionPlan, diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 2f73ec5..2a2a74e 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1141,7 +1141,7 @@ 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.skip('it uses iterable instruction plans to fill gaps in parallel candidates regardless of the order', async (t) => { +test('it uses iterable instruction plans to fill gaps in parallel candidates regardless of the order', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); @@ -1153,9 +1153,9 @@ test.skip('it uses iterable instruction plans to fill gaps in parallel candidate t.deepEqual( await planner( parallelInstructionPlan([ + iteratorA, singleInstructionPlan(instructionB), singleInstructionPlan(instructionC), - iteratorA, ]) ), parallelTransactionPlan([ From fd2868d4600def57be72e905536f8e2fe7872489 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 20:20:08 +0100 Subject: [PATCH 051/112] wip --- .../transactionPlanner.test.ts | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 2a2a74e..021180b 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1103,11 +1103,11 @@ test('it simplifies iterable instruction plans that fit in a single transaction' }); /** - * [Par] ───────────────────────────▶ [Par] - * │ │ - * ├── [A: 75%] ├── [Tx: A + C(1, 25%)] - * ├── [B: 50%] ├── [Tx: B + C(2, 50%)] - * └── [C(x, 125%)] └── [Tx: C(3, 50%)] + * [Par] ─────────────────────▶ [Par] + * │ │ + * ├── [A: 75%] ├── [Tx: A + C(1, 25%)] + * ├── [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) => { const { txPercent, instruction, iterator, singleTransactionPlan } = @@ -1135,13 +1135,13 @@ test('it uses iterable instruction plans to fill gaps in parallel candidates', a }); /** - * [Par] ───────────────────────────▶ [Par] - * │ │ - * ├── [A(x, 125%)] ├── [Tx: B + A(1, 25%)] - * ├── [C: 50%] ├── [Tx: C + A(2, 50%)] - * └── [B: 75%] └── [Tx: A(3, 50%)] + * [Par] ─────────────────────▶ [Par] + * │ │ + * ├── [A(x, 125%)] ├── [Tx: B + A(1, 25%)] + * ├── [C: 50%] ├── [Tx: C + A(2, 50%)] + * └── [B: 75%] └── [Tx: A(3, 50%)] */ -test('it uses iterable instruction plans to fill gaps in parallel candidates regardless of the order', async (t) => { +test('it handles parallel iterable instruction plans last to fill gaps in previous parallel candidates', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); const planner = createBaseTransactionPlanner({ version: 0 }); @@ -1166,9 +1166,42 @@ test('it uses iterable instruction plans to fill gaps in parallel candidates reg ); }); -// TODO: with sequential plans. +/** + * [Seq] ─────────────────────▶ [Seq] + * │ │ + * ├── [A: 75%] ├── [Tx: A + B(1, 25%)] + * ├── [B(x, 75%)] └── [Tx: B(2, 50%) + C] + * └── [C: 50%] + */ +test('it uses iterable instruction plans to fill gaps in sequential candidates', async (t) => { + const { txPercent, instruction, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(75)); + const instructionB = instruction(txPercent(50)); + const iteratorC = iterator(txPercent(25) + txPercent(50) + txPercent(50)); // 125% + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + singleInstructionPlan(instructionB), + iteratorC, + ]) + ), + parallelTransactionPlan([ + singleTransactionPlan([instructionA, iteratorC.get(txPercent(25), 0)]), + singleTransactionPlan([instructionB, iteratorC.get(txPercent(50), 1)]), + singleTransactionPlan([iteratorC.get(txPercent(50), 2)]), + ]) + ); +}); + // TODO: with non-divisible sequential plans. +// TODO: [Seq] -> [Par] -> [Iter] filling [Seq] candidate. + /** * [Par] ───────────────────────────▶ [Par] * │ │ From d4d638941963f4b309f442b5817cf075e8517431 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 20:24:50 +0100 Subject: [PATCH 052/112] wip --- .../transactionPlanner.test.ts | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 021180b..3908d15 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1179,26 +1179,54 @@ test('it uses iterable instruction plans to fill gaps in sequential candidates', const planner = createBaseTransactionPlanner({ version: 0 }); const instructionA = instruction(txPercent(75)); - const instructionB = instruction(txPercent(50)); - const iteratorC = iterator(txPercent(25) + txPercent(50) + txPercent(50)); // 125% + const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% + const instructionC = instruction(txPercent(50)); t.deepEqual( await planner( - parallelInstructionPlan([ + sequentialInstructionPlan([ singleInstructionPlan(instructionA), - singleInstructionPlan(instructionB), - iteratorC, + iteratorB, + singleInstructionPlan(instructionC), ]) ), - parallelTransactionPlan([ - singleTransactionPlan([instructionA, iteratorC.get(txPercent(25), 0)]), - singleTransactionPlan([instructionB, iteratorC.get(txPercent(50), 1)]), - singleTransactionPlan([iteratorC.get(txPercent(50), 2)]), + sequentialTransactionPlan([ + singleTransactionPlan([instructionA, iteratorB.get(txPercent(25), 0)]), + singleTransactionPlan([iteratorB.get(txPercent(50), 1), instructionC]), ]) ); }); -// TODO: with non-divisible sequential plans. +/** + * [NonDivSeq] ───────────────▶ [NonDivSeq] + * │ │ + * ├── [A: 75%] ├── [Tx: A + B(1, 25%)] + * ├── [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) => { + const { txPercent, instruction, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(75)); + const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% + const instructionC = instruction(txPercent(50)); + + t.deepEqual( + await planner( + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionA), + iteratorB, + singleInstructionPlan(instructionC), + ]) + ), + nonDivisibleSequentialTransactionPlan([ + singleTransactionPlan([instructionA, iteratorB.get(txPercent(25), 0)]), + singleTransactionPlan([iteratorB.get(txPercent(50), 1), instructionC]), + ]) + ); +}); // TODO: [Seq] -> [Par] -> [Iter] filling [Seq] candidate. From a74a4114ccd8b24502c0c5307abf881fb3bfcc63 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 20:34:25 +0100 Subject: [PATCH 053/112] wip --- .../transactionPlanner.test.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 3908d15..45e71ec 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1228,7 +1228,38 @@ test('it uses iterable instruction plans to fill gaps in non-divisible sequentia ); }); -// TODO: [Seq] -> [Par] -> [Iter] filling [Seq] candidate. +/** + * [Seq] ───────────────────────▶ [Seq] + * │ │ + * ├── [A: 75%] ├── [Tx: A + B(1, 25%)] + * └── [Par] └── [Tx: C + B(2, 50%)] + * ├── [B(x, 75%)] + * └── [C: 50%] + */ +test('it uses parallel iterable instruction plans to fill gaps in sequential candidates', async (t) => { + const { txPercent, instruction, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(75)); + const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% + const instructionC = instruction(txPercent(50)); + + const result = await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + parallelInstructionPlan([iteratorB, singleInstructionPlan(instructionC)]), + ]) + ); + + t.deepEqual( + result, + sequentialTransactionPlan([ + singleTransactionPlan([instructionA, iteratorB.get(txPercent(25), 0)]), + singleTransactionPlan([instructionC, iteratorB.get(txPercent(50), 1)]), + ]) + ); +}); /** * [Par] ───────────────────────────▶ [Par] From 85689821c2dcb35d1db4b6962b12b674e84435a7 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 20:44:43 +0100 Subject: [PATCH 054/112] wip --- .../transactionPlanner.test.ts | 87 +++++++++++++++++-- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 45e71ec..5e85007 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1245,15 +1245,16 @@ test('it uses parallel iterable instruction plans to fill gaps in sequential can const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% const instructionC = instruction(txPercent(50)); - const result = await planner( - sequentialInstructionPlan([ - singleInstructionPlan(instructionA), - parallelInstructionPlan([iteratorB, singleInstructionPlan(instructionC)]), - ]) - ); - t.deepEqual( - result, + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + parallelInstructionPlan([ + iteratorB, + singleInstructionPlan(instructionC), + ]), + ]) + ), sequentialTransactionPlan([ singleTransactionPlan([instructionA, iteratorB.get(txPercent(25), 0)]), singleTransactionPlan([instructionC, iteratorB.get(txPercent(50), 1)]), @@ -1261,6 +1262,76 @@ test('it uses parallel iterable instruction plans to fill gaps in sequential can ); }); +/** + * [Par] ─────────────────────────▶ [Tx: A + B(1, 50%) + C] + * │ + * ├── [A: 25%] + * └── [Seq] + * ├── [B(x, 50%)] + * └── [C: 25%] + */ +test('it uses the whole sequential iterable instruction plan when it fits in the parent parallel candidate', async (t) => { + const { txPercent, instruction, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(25)); + const iteratorB = iterator(txPercent(50)); + const instructionC = instruction(txPercent(25)); + + t.deepEqual( + await planner( + parallelInstructionPlan([ + singleInstructionPlan(instructionA), + sequentialInstructionPlan([ + iteratorB, + singleInstructionPlan(instructionC), + ]), + ]) + ), + singleTransactionPlan([ + instructionA, + iteratorB.get(txPercent(50), 0), + instructionC, + ]) + ); +}); + +/** + * [Seq] ─────────────────────────▶ [Tx: A + B(1, 50%) + C] + * │ + * ├── [A: 25%] + * └── [NonDivSeq] + * ├── [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) => { + const { txPercent, instruction, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(25)); + const iteratorB = iterator(txPercent(50)); + const instructionC = instruction(txPercent(25)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + nonDivisibleSequentialInstructionPlan([ + iteratorB, + singleInstructionPlan(instructionC), + ]), + ]) + ), + singleTransactionPlan([ + instructionA, + iteratorB.get(txPercent(50), 0), + instructionC, + ]) + ); +}); + /** * [Par] ───────────────────────────▶ [Par] * │ │ From 251788b1875ac58547941b72d37de8b10f916d2f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 21:05:53 +0100 Subject: [PATCH 055/112] wip --- .../transactionPlanner.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 5e85007..fbde514 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1393,3 +1393,62 @@ test('complex example 1', async (t) => { ]) ); }); + +/** + * [Seq] ─────────────────────────────────▶ [Seq] + * │ │ + * ├── [A: 20%] ├── [Tx: A + B + C + E(1, 40%)] + * ├── [NonDivSeq] ├── [Par] + * │ ├── [B: 20%] │ ├── [Tx: D + E(2, 50%)] + * │ └── [C: 20%] │ ├── [Tx: E(3, 100%)] + * ├── [Par] │ └── [Tx: E(4, 60%)] + * │ ├── [D: 50%] └── [Tx: F + G] + * │ └── [E(x, 250%)] + * ├── [F: 50%] + * └── [G: 50%] + */ +test('complex example 2', async (t) => { + const { instruction, iterator, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createBaseTransactionPlanner({ version: 0 }); + + const instructionA = instruction(txPercent(20)); + const instructionB = instruction(txPercent(20)); + const instructionC = instruction(txPercent(20)); + const instructionD = instruction(txPercent(50)); + const iteratorE = iterator(txPercent(250)); + const instructionF = instruction(txPercent(50)); + const instructionG = instruction(txPercent(50)); + + t.deepEqual( + await planner( + sequentialInstructionPlan([ + singleInstructionPlan(instructionA), + nonDivisibleSequentialInstructionPlan([ + singleInstructionPlan(instructionB), + singleInstructionPlan(instructionC), + ]), + parallelInstructionPlan([ + singleInstructionPlan(instructionD), + iteratorE, + ]), + singleInstructionPlan(instructionF), + singleInstructionPlan(instructionG), + ]) + ), + sequentialTransactionPlan([ + singleTransactionPlan([ + instructionA, + instructionB, + instructionC, + iteratorE.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([instructionF, instructionG]), + ]) + ); +}); From b4f0da5d0162e4c7ef07173afbe0c131bcf3677c Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 21:13:22 +0100 Subject: [PATCH 056/112] wip --- clients/js/src/instructionPlansDraft/transactionPlanner.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 8e379c2..0496b0b 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -256,7 +256,7 @@ async function traverseIterable( ): Promise { const iterator = instructionPlan.getIterator(); const transactionPlans: SingleTransactionPlan[] = []; - const candidates = [...context.parentCandidates]; // TODO: Use some caching mechanism to avoid trying filled candidates. + const candidates = [...context.parentCandidates]; while (iterator.hasNext()) { const candidateResult = selectCandidateForIterator(candidates, iterator); @@ -273,6 +273,9 @@ async function traverseIterable( } await context.addInstructionsToSingleTransactionPlan(newPlan, [ix]); 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); } } From 2a3ee60255035741601f529d9ffaa3b5dc642b9b Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Sun, 6 Apr 2025 21:33:19 +0100 Subject: [PATCH 057/112] wip --- .../transactionPlanner.ts | 7 -- .../transactionPlannerDecorators.ts | 97 +++++++++++++++++++ 2 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 0496b0b..fb241bb 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -53,13 +53,6 @@ export type TransactionPlanner = ( config?: TransactionPlannerConfig ) => Promise; -// TODO: Implement -// - Ask for additional instructions for each message. Maybe `getDefaultMessage` or `messageModifier` functions? -// - Add Compute Unit instructions. -// - Split instruction by sizes. -// - Provide remaining bytes to dynamic instructions. -// - Pack transaction messages as much as possible. -// - simulate CU. export function createBaseTransactionPlanner({ version, }: { diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts new file mode 100644 index 0000000..a73b82e --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts @@ -0,0 +1,97 @@ +import { + Address, + appendTransactionMessageInstructions, + BaseTransactionMessage, + IInstruction, + ITransactionMessageWithFeePayer, + ITransactionMessageWithFeePayerSigner, + pipe, + prependTransactionMessageInstructions, + setTransactionMessageFeePayer, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + TransactionMessageWithBlockhashLifetime, + TransactionSigner, +} from '@solana/kit'; +import { TransactionPlanner } from './transactionPlanner'; + +export function transformTransactionPlannerMessage( + transformer: ( + transactionMessage: TTransactionMessage + ) => TTransactionMessage, + planner: TransactionPlanner +): TransactionPlanner { + return async (instructionPlan, config) => { + return await planner(instructionPlan, { + ...config, + newTransactionTransformer: (transactionMessage) => + pipe(transactionMessage, transformer, (tx) => + config?.newTransactionTransformer + ? config.newTransactionTransformer(tx) + : Promise.resolve(tx) + ), + }); + }; +} + +export function prependTransactionPlannerInstructions( + instructions: IInstruction[], + planner: TransactionPlanner +): TransactionPlanner { + return transformTransactionPlannerMessage( + (tx) => prependTransactionMessageInstructions(instructions, tx), + planner + ); +} + +export function appendTransactionPlannerInstructions( + instructions: IInstruction[], + planner: TransactionPlanner +): TransactionPlanner { + return transformTransactionPlannerMessage( + (tx) => appendTransactionMessageInstructions(instructions, tx), + planner + ); +} + +export function setTransactionPlannerFeePayer( + feePayer: Address, + planner: TransactionPlanner +): TransactionPlanner { + return transformTransactionPlannerMessage( + ( + tx: TTransactionMessage + ) => + setTransactionMessageFeePayer(feePayer, tx) as TTransactionMessage & + ITransactionMessageWithFeePayer, + planner + ); +} + +export function setTransactionPlannerFeePayerSigner( + feePayerSigner: TransactionSigner, + planner: TransactionPlanner +): TransactionPlanner { + return transformTransactionPlannerMessage( + ( + tx: TTransactionMessage + ) => + setTransactionMessageFeePayerSigner( + feePayerSigner, + tx + ) as TTransactionMessage & ITransactionMessageWithFeePayerSigner, + planner + ); +} + +export function setTransactionPlannerLifetimeUsingBlockhash( + latestBlockhash: TransactionMessageWithBlockhashLifetime['lifetimeConstraint'], + planner: TransactionPlanner +): TransactionPlanner { + return transformTransactionPlannerMessage( + (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), + planner + ); +} + +// TODO: estimateAndSetComputeUnitLimitForTransactionPlanner From 1228c8bbb609166e69f615cdd47b05c7c9bb2a14 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 7 Apr 2025 13:04:21 +0100 Subject: [PATCH 058/112] wip --- .../transactionPlanner.ts | 4 +- .../transactionPlannerDecorators.ts | 81 +++++++++++++------ 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index fb241bb..01889be 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -25,7 +25,7 @@ import { } from './instructionPlan'; import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; -// TODO: This would need to be a first-class citizen of @solana/kit. +// TODO: This would need to be a first-class citizen of @solana/transactions. export const TRANSACTION_PACKET_SIZE = 1280; export const TRANSACTION_PACKET_HEADER = 40 /* 40 bytes is the size of the IPv6 header. */ + @@ -378,7 +378,7 @@ export function getRemainingTransactionSize(message: BaseTransactionMessage) { return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); } -// TODO: This would need to be a first-class citizen of @solana/kit. +// TODO: This would need to be a first-class citizen of @solana/transactions. // It should accepts both `Transaction` and `BaseTransactionMessage` instances. // Over time, efforts should be made to improve the performance of this function. // E.g. maybe we don't need to compile the transaction message to get the size. diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts index a73b82e..dc4f98e 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts @@ -2,34 +2,35 @@ import { Address, appendTransactionMessageInstructions, BaseTransactionMessage, + GetLatestBlockhashApi, IInstruction, ITransactionMessageWithFeePayer, ITransactionMessageWithFeePayerSigner, - pipe, prependTransactionMessageInstructions, + Rpc, setTransactionMessageFeePayer, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, - TransactionMessageWithBlockhashLifetime, TransactionSigner, } from '@solana/kit'; -import { TransactionPlanner } from './transactionPlanner'; +import { + TransactionPlanner, + TransactionPlannerConfig, +} from './transactionPlanner'; export function transformTransactionPlannerMessage( - transformer: ( - transactionMessage: TTransactionMessage - ) => TTransactionMessage, + transformer: Required['newTransactionTransformer'], planner: TransactionPlanner ): TransactionPlanner { return async (instructionPlan, config) => { return await planner(instructionPlan, { ...config, - newTransactionTransformer: (transactionMessage) => - pipe(transactionMessage, transformer, (tx) => - config?.newTransactionTransformer - ? config.newTransactionTransformer(tx) - : Promise.resolve(tx) - ), + newTransactionTransformer: async (tx) => { + const transformedTx = await transformer(tx); + return config?.newTransactionTransformer + ? await config.newTransactionTransformer(transformedTx) + : transformedTx; + }, }); }; } @@ -39,7 +40,8 @@ export function prependTransactionPlannerInstructions( planner: TransactionPlanner ): TransactionPlanner { return transformTransactionPlannerMessage( - (tx) => prependTransactionMessageInstructions(instructions, tx), + (tx) => + Promise.resolve(prependTransactionMessageInstructions(instructions, tx)), planner ); } @@ -49,7 +51,8 @@ export function appendTransactionPlannerInstructions( planner: TransactionPlanner ): TransactionPlanner { return transformTransactionPlannerMessage( - (tx) => appendTransactionMessageInstructions(instructions, tx), + (tx) => + Promise.resolve(appendTransactionMessageInstructions(instructions, tx)), planner ); } @@ -62,8 +65,10 @@ export function setTransactionPlannerFeePayer( ( tx: TTransactionMessage ) => - setTransactionMessageFeePayer(feePayer, tx) as TTransactionMessage & - ITransactionMessageWithFeePayer, + Promise.resolve( + setTransactionMessageFeePayer(feePayer, tx) as TTransactionMessage & + ITransactionMessageWithFeePayer + ), planner ); } @@ -76,22 +81,52 @@ export function setTransactionPlannerFeePayerSigner( ( tx: TTransactionMessage ) => - setTransactionMessageFeePayerSigner( - feePayerSigner, - tx - ) as TTransactionMessage & ITransactionMessageWithFeePayerSigner, + Promise.resolve( + setTransactionMessageFeePayerSigner( + feePayerSigner, + tx + ) as TTransactionMessage & ITransactionMessageWithFeePayerSigner + ), planner ); } -export function setTransactionPlannerLifetimeUsingBlockhash( - latestBlockhash: TransactionMessageWithBlockhashLifetime['lifetimeConstraint'], +export function setTransactionPlannerLifetimeUsingLatestBlockhash( + rpc: Rpc, planner: TransactionPlanner ): TransactionPlanner { + // Cache the latest blockhash for 60 seconds. + const getBlockhash = getTimedCacheFunction(async () => { + const { value } = await rpc.getLatestBlockhash().send(); + return value; + }, 60_000); + return transformTransactionPlannerMessage( - (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), + async (tx) => + setTransactionMessageLifetimeUsingBlockhash(await getBlockhash(), tx), planner ); } // TODO: estimateAndSetComputeUnitLimitForTransactionPlanner + +function getTimedCacheFunction( + fn: () => Promise, + timeoutInMilliseconds: number +): () => Promise { + let cache: T | null = null; + let lastFetchTime = 0; + return async () => { + const currentTime = Date.now(); + + // Cache hit. + if (cache && currentTime - lastFetchTime < timeoutInMilliseconds) { + return cache; + } + + // Cache miss. + cache = await fn(); + lastFetchTime = currentTime; + return cache; + }; +} From 2993186f08cd235b35487ff81da6273974d1b3de Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 7 Apr 2025 13:19:31 +0100 Subject: [PATCH 059/112] wip --- .../transactionPlannerDecorators.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts index dc4f98e..381843b 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts @@ -2,6 +2,7 @@ import { Address, appendTransactionMessageInstructions, BaseTransactionMessage, + getComputeUnitEstimateForTransactionMessageFactory, GetLatestBlockhashApi, IInstruction, ITransactionMessageWithFeePayer, @@ -11,12 +12,19 @@ import { setTransactionMessageFeePayer, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, + SimulateTransactionApi, TransactionSigner, } from '@solana/kit'; import { TransactionPlanner, TransactionPlannerConfig, } from './transactionPlanner'; +import { + COMPUTE_BUDGET_PROGRAM_ADDRESS, + ComputeBudgetInstruction, + getSetComputeUnitLimitInstruction, + identifyComputeBudgetInstruction, +} from '@solana-program/compute-budget'; export function transformTransactionPlannerMessage( transformer: Required['newTransactionTransformer'], @@ -108,7 +116,39 @@ export function setTransactionPlannerLifetimeUsingLatestBlockhash( ); } -// TODO: estimateAndSetComputeUnitLimitForTransactionPlanner +const MAX_COMPUTE_UNIT_LIMIT = 1_400_000; + +// TODO: This will need decoupling from `@solana-program/compute-budget` +// when added to `@solana/instruction-plans`. Also, the function +// `getComputeUnitEstimateForTransactionMessageFactory` will need to +// move in a granular package so `instruction-plans` can use it. +export function estimateAndSetComputeUnitLimitForTransactionPlanner( + rpc: Rpc, + planner: TransactionPlanner +): TransactionPlanner { + const estimate = getComputeUnitEstimateForTransactionMessageFactory({ rpc }); + + return transformTransactionPlannerMessage((tx) => { + const hasComputeBudgetLimit = tx.instructions.some((ix) => { + return ( + ix.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS && + identifyComputeBudgetInstruction(ix.data as Uint8Array) === + ComputeBudgetInstruction.SetComputeUnitLimit + ); + }); + + if (hasComputeBudgetLimit) { + return Promise.resolve(tx); + } + + return Promise.resolve( + prependTransactionMessageInstructions( + [getSetComputeUnitLimitInstruction({ units: MAX_COMPUTE_UNIT_LIMIT })], + tx + ) + ); + }, planner); +} function getTimedCacheFunction( fn: () => Promise, From f1de283b830c1974a274062c4d8d1296e33ae851 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 7 Apr 2025 13:47:05 +0100 Subject: [PATCH 060/112] wip --- .../transactionPlannerDecorators.ts | 135 ++++++++++++++---- 1 file changed, 110 insertions(+), 25 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts index 381843b..6301f1d 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts @@ -2,11 +2,13 @@ import { Address, appendTransactionMessageInstructions, BaseTransactionMessage, + CompilableTransactionMessage, getComputeUnitEstimateForTransactionMessageFactory, GetLatestBlockhashApi, IInstruction, ITransactionMessageWithFeePayer, ITransactionMessageWithFeePayerSigner, + prependTransactionMessageInstruction, prependTransactionMessageInstructions, Rpc, setTransactionMessageFeePayer, @@ -25,8 +27,11 @@ import { getSetComputeUnitLimitInstruction, identifyComputeBudgetInstruction, } from '@solana-program/compute-budget'; +import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; -export function transformTransactionPlannerMessage( +type Mutable = { -readonly [P in keyof T]: T[P] }; + +export function transformNewTransactionPlannerMessage( transformer: Required['newTransactionTransformer'], planner: TransactionPlanner ): TransactionPlanner { @@ -43,11 +48,20 @@ export function transformTransactionPlannerMessage( }; } +export function transformTransactionPlan( + transformer: (transactionPlan: TransactionPlan) => Promise, + planner: TransactionPlanner +): TransactionPlanner { + return async (instructionPlan, config) => { + return await transformer(await planner(instructionPlan, config)); + }; +} + export function prependTransactionPlannerInstructions( instructions: IInstruction[], planner: TransactionPlanner ): TransactionPlanner { - return transformTransactionPlannerMessage( + return transformNewTransactionPlannerMessage( (tx) => Promise.resolve(prependTransactionMessageInstructions(instructions, tx)), planner @@ -58,7 +72,7 @@ export function appendTransactionPlannerInstructions( instructions: IInstruction[], planner: TransactionPlanner ): TransactionPlanner { - return transformTransactionPlannerMessage( + return transformNewTransactionPlannerMessage( (tx) => Promise.resolve(appendTransactionMessageInstructions(instructions, tx)), planner @@ -69,7 +83,7 @@ export function setTransactionPlannerFeePayer( feePayer: Address, planner: TransactionPlanner ): TransactionPlanner { - return transformTransactionPlannerMessage( + return transformNewTransactionPlannerMessage( ( tx: TTransactionMessage ) => @@ -85,7 +99,7 @@ export function setTransactionPlannerFeePayerSigner( feePayerSigner: TransactionSigner, planner: TransactionPlanner ): TransactionPlanner { - return transformTransactionPlannerMessage( + return transformNewTransactionPlannerMessage( ( tx: TTransactionMessage ) => @@ -109,7 +123,7 @@ export function setTransactionPlannerLifetimeUsingLatestBlockhash( return value; }, 60_000); - return transformTransactionPlannerMessage( + return transformNewTransactionPlannerMessage( async (tx) => setTransactionMessageLifetimeUsingBlockhash(await getBlockhash(), tx), planner @@ -124,30 +138,80 @@ const MAX_COMPUTE_UNIT_LIMIT = 1_400_000; // move in a granular package so `instruction-plans` can use it. export function estimateAndSetComputeUnitLimitForTransactionPlanner( rpc: Rpc, - planner: TransactionPlanner + planner: TransactionPlanner, + chunkSize: number | null = 10 ): TransactionPlanner { - const estimate = getComputeUnitEstimateForTransactionMessageFactory({ rpc }); - - return transformTransactionPlannerMessage((tx) => { - const hasComputeBudgetLimit = tx.instructions.some((ix) => { - return ( - ix.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS && - identifyComputeBudgetInstruction(ix.data as Uint8Array) === - ComputeBudgetInstruction.SetComputeUnitLimit + // Create a function to estimate the compute unit limit for a transaction. + const estimateComputeUnitLimit = + getComputeUnitEstimateForTransactionMessageFactory({ rpc }); + + // Add a compute unit limit instruction to the transaction if it doesn't exist. + const plannerWithComputeBudgetLimits = transformNewTransactionPlannerMessage( + (tx) => { + if (getComputeUnitLimitInstructionIndex(tx) >= 0) { + return Promise.resolve(tx); + } + + return Promise.resolve( + prependTransactionMessageInstruction( + getSetComputeUnitLimitInstruction({ units: MAX_COMPUTE_UNIT_LIMIT }), + tx + ) ); - }); + }, + planner + ); + + // Transform the final transaction plan to set the correct compute unit limit. + return transformTransactionPlan(async (plan) => { + const promises = getAllSingleTransactionPlans(plan).map( + async (singlePlan) => { + const computeUnitsEstimate = await estimateComputeUnitLimit( + singlePlan.message as CompilableTransactionMessage + ); + const instructionIndex = getComputeUnitLimitInstructionIndex( + singlePlan.message + ); + const newMessage = + instructionIndex === -1 + ? prependTransactionMessageInstruction( + getSetComputeUnitLimitInstruction({ + units: computeUnitsEstimate, + }), + singlePlan.message + ) + : { + ...singlePlan.message, + instructions: [ + ...singlePlan.message.instructions.slice(0, instructionIndex), + getSetComputeUnitLimitInstruction({ + units: computeUnitsEstimate, + }), + ...singlePlan.message.instructions.slice( + instructionIndex + 1 + ), + ], + }; + (singlePlan as Mutable).message = newMessage; + } + ); - if (hasComputeBudgetLimit) { - return Promise.resolve(tx); + // Chunk promises to avoid rate limiting. + const chunkedPromises = []; + if (!chunkSize) { + chunkedPromises.push(promises); + } else { + for (let i = 0; i < promises.length; i += chunkSize) { + const chunk = promises.slice(i, i + chunkSize); + chunkedPromises.push(chunk); + } + } + for (const chunk of chunkedPromises) { + await Promise.all(chunk); } - return Promise.resolve( - prependTransactionMessageInstructions( - [getSetComputeUnitLimitInstruction({ units: MAX_COMPUTE_UNIT_LIMIT })], - tx - ) - ); - }, planner); + return plan; + }, plannerWithComputeBudgetLimits); } function getTimedCacheFunction( @@ -170,3 +234,24 @@ function getTimedCacheFunction( return cache; }; } + +function getComputeUnitLimitInstructionIndex( + transactionMessage: BaseTransactionMessage +) { + return transactionMessage.instructions.findIndex((ix) => { + return ( + ix.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS && + identifyComputeBudgetInstruction(ix.data as Uint8Array) === + ComputeBudgetInstruction.SetComputeUnitLimit + ); + }); +} + +function getAllSingleTransactionPlans( + transactionPlan: TransactionPlan +): SingleTransactionPlan[] { + if (transactionPlan.kind === 'single') { + return [transactionPlan]; + } + return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); +} From 4a034d1424c4ac21c0ba7298b3dbcd021be81d9f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 7 Apr 2025 14:00:21 +0100 Subject: [PATCH 061/112] wip --- .../transactionPlanExecutor.ts | 71 +--------------- .../transactionPlanExecutorDecorators.ts | 81 +++++++++++++++++++ 2 files changed, 84 insertions(+), 68 deletions(-) create mode 100644 clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index 42a2377..19987a7 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -1,8 +1,5 @@ import { - Blockhash, GetLatestBlockhashApi, - isSolanaError, - pipe, Rpc, RpcSubscriptions, Signature, @@ -15,12 +12,10 @@ export type TransactionPlanExecutor = ( transactionPlan: TransactionPlan ) => Promise>; -export function getDefaultTransactionPlanExecutor(options: { +export function createBaseTransactionPlanExecutor(options: { rpc: Rpc; - rpcSubscriptions: RpcSubscriptions; // TODO: narrow + rpcSubscriptions: RpcSubscriptions; }): TransactionPlanExecutor { - const { rpc } = options; - // TODO: implement // - Refetch blockhash if it's expired // - Retry on failure @@ -38,65 +33,5 @@ export function getDefaultTransactionPlanExecutor(options: { }; }; - return pipe( - executor, - (ex) => refreshBlockheightTransactionPlanExecutor(rpc, ex), - (ex) => retryTransactionPlanExecutor(5, ex) - ); -} - -export function refreshBlockheightTransactionPlanExecutor( - rpc: Rpc, - executor: TransactionPlanExecutor -): TransactionPlanExecutor { - let latestBlockhash: { - blockhash: Blockhash; - lastValidBlockHeight: bigint; - } | null = null; - return async (transactionPlan) => { - if (transactionPlan.kind !== 'single') { - return await executor(transactionPlan); - } - - if (latestBlockhash) { - // Replace the blockhash in the message - } - try { - return await executor(transactionPlan); - } catch (error) { - if (isSolanaError(error)) { - // TODO: Retry on blockhash expired error - const result = await rpc.getLatestBlockhash().send(); - latestBlockhash = result.value; - return await executor(transactionPlan); - } else { - throw error; - } - } - }; -} - -export function retryTransactionPlanExecutor( - maxRetries: number, - executor: TransactionPlanExecutor -): TransactionPlanExecutor { - return async (transactionPlan) => { - if (transactionPlan.kind !== 'single') { - return await executor(transactionPlan); - } - - let retries = 0; - let lastError: Error | null = null; - - while (retries < maxRetries) { - try { - return await executor(transactionPlan); - } catch (error) { - retries++; - lastError = error as Error; - } - } - - throw lastError; - }; + return executor; } diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts new file mode 100644 index 0000000..9de8c0c --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -0,0 +1,81 @@ +import { + Blockhash, + GetLatestBlockhashApi, + isSolanaError, + Rpc, + setTransactionMessageLifetimeUsingBlockhash, + SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND, +} from '@solana/kit'; +import { SingleTransactionPlan } from './transactionPlan'; +import { TransactionPlanExecutor } from './transactionPlanExecutor'; + +type Mutable = { -readonly [P in keyof T]: T[P] }; + +export function refreshBlockheightTransactionPlanExecutor( + rpc: Rpc, + executor: TransactionPlanExecutor +): TransactionPlanExecutor { + let latestBlockhash: { + blockhash: Blockhash; + lastValidBlockHeight: bigint; + } | null = null; + return async function traverse(transactionPlan) { + if (transactionPlan.kind !== 'single') { + return await executor(transactionPlan); + } + + // Replace the blockhash in the message, if a new one is available. + if (latestBlockhash) { + (transactionPlan as Mutable).message = + setTransactionMessageLifetimeUsingBlockhash( + latestBlockhash, + transactionPlan.message + ); + } + + try { + return await executor(transactionPlan); + } catch (error) { + if ( + isSolanaError( + error, + // TODO: Retry on blockhash expired error. + SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND + ) + ) { + const result = await rpc.getLatestBlockhash().send(); + latestBlockhash = result.value; + return await traverse(transactionPlan); + } else { + throw error; + } + } + }; +} + +export function retryTransactionPlanExecutor( + maxRetries: number, + executor: TransactionPlanExecutor +): TransactionPlanExecutor { + return async (transactionPlan) => { + if (transactionPlan.kind !== 'single') { + return await executor(transactionPlan); + } + + let retries = 0; + let lastError: Error | null = null; + + // x retries means x+1 attempts. + while (retries < maxRetries + 1) { + try { + return await executor(transactionPlan); + } catch (error) { + // TODO: Should we not retry on certain error codes? + retries++; + lastError = error as Error; + } + } + + throw lastError; + }; +} From 4f6185512a88b88121252ac8305b9629e33b03b1 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 7 Apr 2025 14:43:59 +0100 Subject: [PATCH 062/112] wip --- .../transactionPlanExecutor.ts | 106 +++++++++++++----- .../transactionPlanExecutorDecorators.ts | 5 + 2 files changed, 85 insertions(+), 26 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index 19987a7..10f9f5c 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -1,37 +1,91 @@ import { - GetLatestBlockhashApi, - Rpc, - RpcSubscriptions, - Signature, - SolanaRpcSubscriptionsApi, + BaseTransactionMessage, + getSignatureFromTransaction, + Transaction, } from '@solana/kit'; -import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; +import { + ParallelTransactionPlan, + SequentialTransactionPlan, + SingleTransactionPlan, + TransactionPlan, +} from './transactionPlan'; import { TransactionPlanResult } from './transactionPlanResult'; export type TransactionPlanExecutor = ( transactionPlan: TransactionPlan ) => Promise>; -export function createBaseTransactionPlanExecutor(options: { - rpc: Rpc; - rpcSubscriptions: RpcSubscriptions; -}): TransactionPlanExecutor { - // TODO: implement - // - Refetch blockhash if it's expired - // - Retry on failure - // - Chunk parallel transactions - // - Handle cancellation (i.e. don't continue past a failing sequential plan) - - const executor: TransactionPlanExecutor = async (plan) => { - await new Promise((resolve) => setTimeout(resolve, 500)); - return { - context: null, - kind: 'single', - message: (plan as SingleTransactionPlan).message, - signature: 'signature' as Signature, - status: { kind: 'success' }, - }; +export function createBaseTransactionPlanExecutor( + sendAndConfirm: ( + transactionMessage: TTransactionMessage + ) => Promise +): TransactionPlanExecutor { + return async (plan): Promise => { + const context: TraverseContext = { sendAndConfirm }; + return await traverse(plan, context); }; +} + +type TraverseContext = { + sendAndConfirm: ( + transactionMessage: TTransactionMessage + ) => Promise; +}; + +async function traverse( + transactionPlan: TransactionPlan, + context: TraverseContext +): Promise { + switch (transactionPlan.kind) { + case 'sequential': + return await traverseSequential(transactionPlan, context); + case 'parallel': + return await traverseParallel(transactionPlan, context); + case 'single': + return await traverseSingle(transactionPlan, context); + default: + transactionPlan satisfies never; + throw new Error( + `Unknown instruction plan kind: ${(transactionPlan as { kind: string }).kind}` + ); + } +} + +async function traverseSequential( + transactionPlan: SequentialTransactionPlan, + context: TraverseContext +): Promise { + const results: TransactionPlanResult[] = []; + for (const subPlan of transactionPlan.plans) { + const result = await traverse(subPlan, context); + results.push(result); + } + return { kind: 'sequential', plans: results }; +} + +async function traverseParallel( + transactionPlan: ParallelTransactionPlan, + context: TraverseContext +): Promise { + const results = await Promise.all( + transactionPlan.plans.map((subPlan) => traverse(subPlan, context)) + ); + return { kind: 'parallel', plans: results }; +} - return executor; +async function traverseSingle( + transactionPlan: SingleTransactionPlan, + context: TraverseContext +): Promise { + const transaction = await context.sendAndConfirm(transactionPlan.message); + + // TODO: Handle error. + + return { + kind: 'single', + context: null, + message: transactionPlan.message, + signature: getSignatureFromTransaction(transaction), + status: { kind: 'success' }, + }; } diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts index 9de8c0c..980d0c2 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -11,6 +11,11 @@ import { TransactionPlanExecutor } from './transactionPlanExecutor'; type Mutable = { -readonly [P in keyof T]: T[P] }; +// TODO: implement +// - Chunk parallel transactions +// - Add support for curstom +// - Handle cancellation (i.e. don't continue past a failing sequential plan) + export function refreshBlockheightTransactionPlanExecutor( rpc: Rpc, executor: TransactionPlanExecutor From d962dd52a34ff988cdf9088aa88b6defe7dc72cb Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 7 Apr 2025 14:52:56 +0100 Subject: [PATCH 063/112] wip --- .../js/src/instructionPlansDraft/internal.ts | 22 +++++++++++++++++ .../transactionPlanExecutorDecorators.ts | 5 ++-- .../transactionPlanner.ts | 3 +-- .../transactionPlannerDecorators.ts | 24 +------------------ 4 files changed, 26 insertions(+), 28 deletions(-) create mode 100644 clients/js/src/instructionPlansDraft/internal.ts diff --git a/clients/js/src/instructionPlansDraft/internal.ts b/clients/js/src/instructionPlansDraft/internal.ts new file mode 100644 index 0000000..924a5b9 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/internal.ts @@ -0,0 +1,22 @@ +export type Mutable = { -readonly [P in keyof T]: T[P] }; + +export function getTimedCacheFunction( + fn: () => Promise, + timeoutInMilliseconds: number +): () => Promise { + let cache: T | null = null; + let lastFetchTime = 0; + return async () => { + const currentTime = Date.now(); + + // Cache hit. + if (cache && currentTime - lastFetchTime < timeoutInMilliseconds) { + return cache; + } + + // Cache miss. + cache = await fn(); + lastFetchTime = currentTime; + return cache; + }; +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts index 980d0c2..08c3cdf 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -8,15 +8,14 @@ import { } from '@solana/kit'; import { SingleTransactionPlan } from './transactionPlan'; import { TransactionPlanExecutor } from './transactionPlanExecutor'; - -type Mutable = { -readonly [P in keyof T]: T[P] }; +import { Mutable } from './internal'; // TODO: implement // - Chunk parallel transactions // - Add support for curstom // - Handle cancellation (i.e. don't continue past a failing sequential plan) -export function refreshBlockheightTransactionPlanExecutor( +export function refreshBlockhashForTransactionPlanExecutor( rpc: Rpc, executor: TransactionPlanExecutor ): TransactionPlanExecutor { diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 01889be..54c157b 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -24,6 +24,7 @@ import { SingleInstructionPlan, } from './instructionPlan'; import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; +import { Mutable } from './internal'; // TODO: This would need to be a first-class citizen of @solana/transactions. export const TRANSACTION_PACKET_SIZE = 1280; @@ -33,8 +34,6 @@ export const TRANSACTION_PACKET_HEADER = export const TRANSACTION_SIZE_LIMIT = TRANSACTION_PACKET_SIZE - TRANSACTION_PACKET_HEADER; -type Mutable = { -readonly [P in keyof T]: T[P] }; - export type TransactionPlannerConfig = { newTransactionTransformer?: < TTransactionMessage extends BaseTransactionMessage, diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts index 6301f1d..69c4363 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts @@ -28,8 +28,7 @@ import { identifyComputeBudgetInstruction, } from '@solana-program/compute-budget'; import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; - -type Mutable = { -readonly [P in keyof T]: T[P] }; +import { getTimedCacheFunction, Mutable } from './internal'; export function transformNewTransactionPlannerMessage( transformer: Required['newTransactionTransformer'], @@ -214,27 +213,6 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( }, plannerWithComputeBudgetLimits); } -function getTimedCacheFunction( - fn: () => Promise, - timeoutInMilliseconds: number -): () => Promise { - let cache: T | null = null; - let lastFetchTime = 0; - return async () => { - const currentTime = Date.now(); - - // Cache hit. - if (cache && currentTime - lastFetchTime < timeoutInMilliseconds) { - return cache; - } - - // Cache miss. - cache = await fn(); - lastFetchTime = currentTime; - return cache; - }; -} - function getComputeUnitLimitInstructionIndex( transactionMessage: BaseTransactionMessage ) { From b3a40f050898cc2bc41c0d805caa52ee74d4fc83 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 7 Apr 2025 14:56:59 +0100 Subject: [PATCH 064/112] wip --- .../transactionPlanExecutor.ts | 2 + .../transactionPlanExecutorDecorators.ts | 52 ++++++------------- 2 files changed, 17 insertions(+), 37 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index 10f9f5c..90b9e10 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -59,6 +59,8 @@ async function traverseSequential( for (const subPlan of transactionPlan.plans) { const result = await traverse(subPlan, context); results.push(result); + + // TODO: Handle cancellations. } return { kind: 'sequential', plans: results }; } diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts index 08c3cdf..39f8f26 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -1,59 +1,37 @@ import { - Blockhash, GetLatestBlockhashApi, - isSolanaError, Rpc, setTransactionMessageLifetimeUsingBlockhash, - SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND, } from '@solana/kit'; +import { getTimedCacheFunction, Mutable } from './internal'; import { SingleTransactionPlan } from './transactionPlan'; import { TransactionPlanExecutor } from './transactionPlanExecutor'; -import { Mutable } from './internal'; // TODO: implement -// - Chunk parallel transactions +// - Chunk parallel transactions (Needs special transformer) // - Add support for curstom -// - Handle cancellation (i.e. don't continue past a failing sequential plan) export function refreshBlockhashForTransactionPlanExecutor( rpc: Rpc, executor: TransactionPlanExecutor ): TransactionPlanExecutor { - let latestBlockhash: { - blockhash: Blockhash; - lastValidBlockHeight: bigint; - } | null = null; - return async function traverse(transactionPlan) { + // Cache the latest blockhash for 60 seconds. + const getBlockhash = getTimedCacheFunction(async () => { + const { value } = await rpc.getLatestBlockhash().send(); + return value; + }, 60_000); + + return async (transactionPlan) => { if (transactionPlan.kind !== 'single') { return await executor(transactionPlan); } - // Replace the blockhash in the message, if a new one is available. - if (latestBlockhash) { - (transactionPlan as Mutable).message = - setTransactionMessageLifetimeUsingBlockhash( - latestBlockhash, - transactionPlan.message - ); - } - - try { - return await executor(transactionPlan); - } catch (error) { - if ( - isSolanaError( - error, - // TODO: Retry on blockhash expired error. - SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND - ) - ) { - const result = await rpc.getLatestBlockhash().send(); - latestBlockhash = result.value; - return await traverse(transactionPlan); - } else { - throw error; - } - } + (transactionPlan as Mutable).message = + setTransactionMessageLifetimeUsingBlockhash( + await getBlockhash(), + transactionPlan.message + ); + return await executor(transactionPlan); }; } From 82271b6f03b32a59285ac8b66e5d673dc41ef07d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 8 Apr 2025 08:37:26 +0100 Subject: [PATCH 065/112] wip --- .../js/src/instructionPlansDraft/transactionPlanExecutor.ts | 3 +++ .../instructionPlansDraft/transactionPlanExecutorDecorators.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index 90b9e10..70c7b4a 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -72,6 +72,9 @@ async function traverseParallel( const results = await Promise.all( transactionPlan.plans.map((subPlan) => traverse(subPlan, context)) ); + + // TODO: Handle chunking via decorators. + return { kind: 'parallel', plans: results }; } diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts index 39f8f26..a052f33 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -9,7 +9,7 @@ import { TransactionPlanExecutor } from './transactionPlanExecutor'; // TODO: implement // - Chunk parallel transactions (Needs special transformer) -// - Add support for curstom +// - Add support for custom export function refreshBlockhashForTransactionPlanExecutor( rpc: Rpc, From 09323a3d9daec5470a55ae9bdecc8fdc6ecd941f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 8 Apr 2025 08:57:46 +0100 Subject: [PATCH 066/112] wip --- clients/js/src/instructionPlansDraft/index.ts | 3 + .../instructionPlansDraft/instructionPlan.ts | 2 +- .../transactionHelpers.ts | 55 +++ .../instructionPlansDraft/transactionPlan.ts | 9 + .../transactionPlanner.ts | 408 +----------------- .../transactionPlannerFactory.ts | 359 +++++++++++++++ ...=> transactionPlannerFactoryDecorators.ts} | 97 ++--- .../_instructionPlanHelpers.ts | 2 +- .../transactionPlanner.test.ts | 88 ++-- 9 files changed, 522 insertions(+), 501 deletions(-) create mode 100644 clients/js/src/instructionPlansDraft/transactionHelpers.ts create mode 100644 clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts rename clients/js/src/instructionPlansDraft/{transactionPlannerDecorators.ts => transactionPlannerFactoryDecorators.ts} (78%) diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts index 797a0b8..12a9d76 100644 --- a/clients/js/src/instructionPlansDraft/index.ts +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -1,3 +1,6 @@ export * from './instructionPlan'; +export * from './transactionHelpers'; export * from './transactionPlan'; export * from './transactionPlanner'; +export * from './transactionPlannerFactory'; +export * from './transactionPlannerFactoryDecorators'; diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 99f62c9..c423bab 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -6,7 +6,7 @@ import { import { getTransactionSize, TRANSACTION_SIZE_LIMIT, -} from './transactionPlanner'; +} from './transactionHelpers'; export type InstructionPlan = | SequentialInstructionPlan diff --git a/clients/js/src/instructionPlansDraft/transactionHelpers.ts b/clients/js/src/instructionPlansDraft/transactionHelpers.ts new file mode 100644 index 0000000..d6d1ae3 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionHelpers.ts @@ -0,0 +1,55 @@ +/** + * TODO: The helpers in this file would need to be a first-class citizen of @solana/transactions. + */ + +import { + Address, + BaseTransactionMessage, + Blockhash, + CompilableTransactionMessage, + compileTransaction, + getTransactionEncoder, + ITransactionMessageWithFeePayer, + pipe, + setTransactionMessageFeePayer, + setTransactionMessageLifetimeUsingBlockhash, + TransactionMessageWithBlockhashLifetime, +} from '@solana/kit'; + +export const TRANSACTION_PACKET_SIZE = 1280; + +export const TRANSACTION_PACKET_HEADER = + 40 /* 40 bytes is the size of the IPv6 header. */ + + 8; /* 8 bytes is the size of the fragment header. */ + +export const TRANSACTION_SIZE_LIMIT = + TRANSACTION_PACKET_SIZE - TRANSACTION_PACKET_HEADER; + +// It should accepts both `Transaction` and `BaseTransactionMessage` instances. +// Over time, efforts should be made to improve the performance of this function. +// E.g. maybe we don't need to compile the transaction message to get the size. +export function getTransactionSize( + message: BaseTransactionMessage & Partial +): number { + const mockFeePayer = + 'Gm1uVH3JxiLgafByNNmnoxLncB7ytpyWNqX3kRM9tSxN' as Address; + const mockBlockhash = { + blockhash: '2WCjwT4P5tJF7tjMtTVEnN6o53bcZ8MhszcfXMERtU3z' as Blockhash, + lastValidBlockHeight: 0n, + }; + const transaction = pipe( + message, + (tx) => { + return tx.feePayer + ? (tx as typeof tx & ITransactionMessageWithFeePayer) + : setTransactionMessageFeePayer(mockFeePayer, tx); + }, + (tx) => { + return tx.lifetimeConstraint + ? (tx as typeof tx & TransactionMessageWithBlockhashLifetime) + : setTransactionMessageLifetimeUsingBlockhash(mockBlockhash, tx); + }, + (tx) => compileTransaction(tx) + ); + return getTransactionEncoder().getSizeFromValue(transaction); +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlan.ts b/clients/js/src/instructionPlansDraft/transactionPlan.ts index a2e1ebb..6a08b0d 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlan.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlan.ts @@ -22,3 +22,12 @@ export type SingleTransactionPlan< kind: 'single'; message: TTransactionMessage; }>; + +export function getAllSingleTransactionPlans( + transactionPlan: TransactionPlan +): SingleTransactionPlan[] { + if (transactionPlan.kind === 'single') { + return [transactionPlan]; + } + return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 54c157b..44a4dd4 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -1,408 +1,6 @@ -import { - Address, - appendTransactionMessageInstructions, - BaseTransactionMessage, - Blockhash, - CompilableTransactionMessage, - compileTransaction, - createTransactionMessage, - getTransactionEncoder, - IInstruction, - ITransactionMessageWithFeePayer, - pipe, - setTransactionMessageFeePayer, - setTransactionMessageLifetimeUsingBlockhash, - TransactionMessageWithBlockhashLifetime, - TransactionVersion, -} from '@solana/kit'; -import { - InstructionIterator, - InstructionPlan, - IterableInstructionPlan, - ParallelInstructionPlan, - SequentialInstructionPlan, - SingleInstructionPlan, -} from './instructionPlan'; -import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; -import { Mutable } from './internal'; - -// TODO: This would need to be a first-class citizen of @solana/transactions. -export const TRANSACTION_PACKET_SIZE = 1280; -export const TRANSACTION_PACKET_HEADER = - 40 /* 40 bytes is the size of the IPv6 header. */ + - 8; /* 8 bytes is the size of the fragment header. */ -export const TRANSACTION_SIZE_LIMIT = - TRANSACTION_PACKET_SIZE - TRANSACTION_PACKET_HEADER; - -export type TransactionPlannerConfig = { - newTransactionTransformer?: < - TTransactionMessage extends BaseTransactionMessage, - >( - transactionMessage: TTransactionMessage - ) => Promise; - newInstructionsTransformer?: < - TTransactionMessage extends BaseTransactionMessage, - >( - transactionMessage: TTransactionMessage - ) => Promise; -}; +import { InstructionPlan } from './instructionPlan'; +import { TransactionPlan } from './transactionPlan'; export type TransactionPlanner = ( - instructionPlan: InstructionPlan, - config?: TransactionPlannerConfig -) => Promise; - -export function createBaseTransactionPlanner({ - version, -}: { - version: TransactionVersion; -}): TransactionPlanner { - return async (originalInstructionPlan, config): Promise => { - const createSingleTransactionPlan = async ( - instructions: IInstruction[] = [] - ): Promise => { - const plan: SingleTransactionPlan = { - kind: 'single', - message: createTransactionMessage({ version }), - }; - if (config?.newTransactionTransformer) { - (plan as Mutable).message = - await config.newTransactionTransformer(plan.message); - } - if (instructions.length > 0) { - await addInstructionsToSingleTransactionPlan(plan, instructions); - } - return plan; - }; - - const addInstructionsToSingleTransactionPlan = async ( - plan: SingleTransactionPlan, - instructions: IInstruction[] - ): Promise => { - let message = appendTransactionMessageInstructions( - instructions, - plan.message - ); - if (config?.newInstructionsTransformer) { - message = await config.newInstructionsTransformer(plan.message); - } - (plan as Mutable).message = message; - }; - - const plan = await traverse(originalInstructionPlan, { - parent: null, - parentCandidates: [], - createSingleTransactionPlan, - addInstructionsToSingleTransactionPlan, - }); - - if (!plan) { - throw new Error('No instructions were found in the instruction plan.'); - } - - return plan; - }; -} - -type TraverseContext = { - parent: InstructionPlan | null; - parentCandidates: SingleTransactionPlan[]; - createSingleTransactionPlan: ( - instructions?: IInstruction[] - ) => Promise; - addInstructionsToSingleTransactionPlan: ( - plan: SingleTransactionPlan, - instructions: IInstruction[] - ) => Promise; -}; - -async function traverse( - instructionPlan: InstructionPlan, - context: TraverseContext -): Promise { - switch (instructionPlan.kind) { - case 'sequential': - return await traverseSequential(instructionPlan, context); - case 'parallel': - return await traverseParallel(instructionPlan, context); - case 'single': - return await traverseSingle(instructionPlan, context); - case 'iterable': - return await traverseIterable(instructionPlan, context); - default: - instructionPlan satisfies never; - throw new Error( - `Unknown instruction plan kind: ${(instructionPlan as { kind: string }).kind}` - ); - } -} - -async function traverseSequential( - instructionPlan: SequentialInstructionPlan, - context: TraverseContext -): Promise { - let candidate: SingleTransactionPlan | null = null; - const mustEntirelyFitInCandidate = - context.parent && - (context.parent.kind === 'parallel' || !instructionPlan.divisible); - if (mustEntirelyFitInCandidate) { - const allInstructions = getAllInstructions(instructionPlan); - candidate = allInstructions - ? selectCandidate(context.parentCandidates, allInstructions) - : null; - if (candidate && allInstructions) { - await context.addInstructionsToSingleTransactionPlan( - candidate, - allInstructions - ); - return null; - } - } else { - candidate = - context.parentCandidates.length > 0 ? context.parentCandidates[0] : null; - } - - const transactionPlans: TransactionPlan[] = []; - for (const plan of instructionPlan.plans) { - const transactionPlan = await traverse(plan, { - ...context, - parent: instructionPlan, - parentCandidates: candidate ? [candidate] : [], - }); - if (transactionPlan) { - candidate = getSequentialCandidate(transactionPlan); - const newPlans = - transactionPlan.kind === 'sequential' && - (transactionPlan.divisible || !instructionPlan.divisible) - ? transactionPlan.plans - : [transactionPlan]; - transactionPlans.push(...newPlans); - } - } - if (transactionPlans.length === 1) { - return transactionPlans[0]; - } - if (transactionPlans.length === 0) { - return null; - } - return { - kind: 'sequential', - divisible: instructionPlan.divisible, - plans: transactionPlans, - }; -} - -async function traverseParallel( - instructionPlan: ParallelInstructionPlan, - context: TraverseContext -): Promise { - const candidates: SingleTransactionPlan[] = [...context.parentCandidates]; - const transactionPlans: TransactionPlan[] = []; - - // Reorder children so iterable plans are last. - const sortedChildren = [ - ...instructionPlan.plans.filter((plan) => plan.kind !== 'iterable'), - ...instructionPlan.plans.filter((plan) => plan.kind === 'iterable'), - ]; - - for (const plan of sortedChildren) { - const transactionPlan = await traverse(plan, { - ...context, - parent: instructionPlan, - parentCandidates: candidates, - }); - if (transactionPlan) { - candidates.push(...getParallelCandidates(transactionPlan)); - const newPlans = - transactionPlan.kind === 'parallel' - ? transactionPlan.plans - : [transactionPlan]; - transactionPlans.push(...newPlans); - } - } - if (transactionPlans.length === 1) { - return transactionPlans[0]; - } - if (transactionPlans.length === 0) { - return null; - } - return { kind: 'parallel', plans: transactionPlans }; -} - -async function traverseSingle( - instructionPlan: SingleInstructionPlan, - context: TraverseContext -): Promise { - const ix = instructionPlan.instruction; - const candidate = selectCandidate(context.parentCandidates, [ix]); - if (candidate) { - await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); - return null; - } - return await context.createSingleTransactionPlan([ix]); -} - -async function traverseIterable( - instructionPlan: IterableInstructionPlan, - context: TraverseContext -): Promise { - const iterator = instructionPlan.getIterator(); - const transactionPlans: SingleTransactionPlan[] = []; - const candidates = [...context.parentCandidates]; - - while (iterator.hasNext()) { - const candidateResult = selectCandidateForIterator(candidates, iterator); - if (candidateResult) { - const [candidate, ix] = candidateResult; - await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); - } else { - const newPlan = await context.createSingleTransactionPlan(); - const ix = iterator.next(newPlan.message); - if (!ix) { - throw new Error( - 'Could not fit `InterableInstructionPlan` into a transaction' - ); - } - await context.addInstructionsToSingleTransactionPlan(newPlan, [ix]); - 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); - } - } - - if (transactionPlans.length === 1) { - return transactionPlans[0]; - } - if (transactionPlans.length === 0) { - return null; - } - if (context.parent?.kind === 'parallel') { - return { kind: 'parallel', plans: transactionPlans }; - } - return { - kind: 'sequential', - divisible: - context.parent?.kind === 'sequential' ? context.parent.divisible : true, - plans: transactionPlans, - }; -} - -function getSequentialCandidate( - latestPlan: TransactionPlan -): SingleTransactionPlan | null { - if (latestPlan.kind === 'single') { - return latestPlan; - } - if (latestPlan.kind === 'sequential' && latestPlan.plans.length > 0) { - return getSequentialCandidate( - latestPlan.plans[latestPlan.plans.length - 1] - ); - } - return null; -} - -function getParallelCandidates( - latestPlan: TransactionPlan -): SingleTransactionPlan[] { - return getAllSingleTransactionPlans(latestPlan); -} - -function getAllSingleTransactionPlans( - transactionPlan: TransactionPlan -): SingleTransactionPlan[] { - if (transactionPlan.kind === 'single') { - return [transactionPlan]; - } - return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); -} - -function getAllInstructions( instructionPlan: InstructionPlan -): IInstruction[] | null { - if (instructionPlan.kind === 'single') { - return [instructionPlan.instruction]; - } - if (instructionPlan.kind === 'iterable') { - return instructionPlan.getAll(); - } - return instructionPlan.plans.reduce( - (acc, plan) => { - if (acc === null) return null; - const instructions = getAllInstructions(plan); - if (instructions === null) return null; - acc.push(...instructions); - return acc; - }, - [] as IInstruction[] | null - ); -} - -function selectCandidateForIterator( - candidates: SingleTransactionPlan[], - iterator: InstructionIterator -): [SingleTransactionPlan, IInstruction] | null { - for (const candidate of candidates) { - const ix = iterator.next(candidate.message); - if (ix) { - return [candidate, ix]; - } - } - return null; -} - -function selectCandidate( - candidates: SingleTransactionPlan[], - instructions: IInstruction[] -): SingleTransactionPlan | null { - const firstValidCandidate = candidates.find((candidate) => - isValidCandidate(candidate, instructions) - ); - return firstValidCandidate ?? null; -} - -function isValidCandidate( - candidate: SingleTransactionPlan, - instructions: IInstruction[] -): boolean { - const message = appendTransactionMessageInstructions( - instructions, - candidate.message - ); - return getRemainingTransactionSize(message) >= 0; -} - -export function getRemainingTransactionSize(message: BaseTransactionMessage) { - return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); -} - -// TODO: This would need to be a first-class citizen of @solana/transactions. -// It should accepts both `Transaction` and `BaseTransactionMessage` instances. -// Over time, efforts should be made to improve the performance of this function. -// E.g. maybe we don't need to compile the transaction message to get the size. -export function getTransactionSize( - message: BaseTransactionMessage & Partial -): number { - const mockFeePayer = - 'Gm1uVH3JxiLgafByNNmnoxLncB7ytpyWNqX3kRM9tSxN' as Address; - const mockBlockhash = { - blockhash: '2WCjwT4P5tJF7tjMtTVEnN6o53bcZ8MhszcfXMERtU3z' as Blockhash, - lastValidBlockHeight: 0n, - }; - const transaction = pipe( - message, - (tx) => { - return tx.feePayer - ? (tx as typeof tx & ITransactionMessageWithFeePayer) - : setTransactionMessageFeePayer(mockFeePayer, tx); - }, - (tx) => { - return tx.lifetimeConstraint - ? (tx as typeof tx & TransactionMessageWithBlockhashLifetime) - : setTransactionMessageLifetimeUsingBlockhash(mockBlockhash, tx); - }, - (tx) => compileTransaction(tx) - ); - return getTransactionEncoder().getSizeFromValue(transaction); -} +) => Promise; diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts new file mode 100644 index 0000000..533c877 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts @@ -0,0 +1,359 @@ +import { + appendTransactionMessageInstructions, + BaseTransactionMessage, + createTransactionMessage, + IInstruction, + TransactionVersion, +} from '@solana/kit'; +import { + InstructionIterator, + InstructionPlan, + IterableInstructionPlan, + ParallelInstructionPlan, + SequentialInstructionPlan, + SingleInstructionPlan, +} from './instructionPlan'; +import { Mutable } from './internal'; +import { + getTransactionSize, + TRANSACTION_SIZE_LIMIT, +} from './transactionHelpers'; +import { + getAllSingleTransactionPlans, + SingleTransactionPlan, + TransactionPlan, +} from './transactionPlan'; +import { TransactionPlanner } from './transactionPlanner'; + +export type TransactionPlannerFactory = ( + configs?: TransactionPlannerFactoryConfig +) => TransactionPlanner; + +type TransactionMessageTransformer = < + TTransactionMessage extends BaseTransactionMessage, +>( + transactionMessage: TTransactionMessage +) => Promise; + +export type TransactionPlannerFactoryConfig = { + newTransactionTransformer?: TransactionMessageTransformer; + newInstructionsTransformer?: TransactionMessageTransformer; +}; + +export function createBaseTransactionPlannerFactory({ + version, +}: { + version: TransactionVersion; +}): TransactionPlannerFactory { + return (config) => { + const createSingleTransactionPlan = async ( + instructions: IInstruction[] = [] + ): Promise => { + const plan: SingleTransactionPlan = { + kind: 'single', + message: createTransactionMessage({ version }), + }; + if (config?.newTransactionTransformer) { + (plan as Mutable).message = + await config.newTransactionTransformer(plan.message); + } + if (instructions.length > 0) { + await addInstructionsToSingleTransactionPlan(plan, instructions); + } + return plan; + }; + + const addInstructionsToSingleTransactionPlan = async ( + plan: SingleTransactionPlan, + instructions: IInstruction[] + ): Promise => { + let message = appendTransactionMessageInstructions( + instructions, + plan.message + ); + if (config?.newInstructionsTransformer) { + message = await config.newInstructionsTransformer(plan.message); + } + (plan as Mutable).message = message; + }; + + return async (originalInstructionPlan): Promise => { + const plan = await traverse(originalInstructionPlan, { + parent: null, + parentCandidates: [], + createSingleTransactionPlan, + addInstructionsToSingleTransactionPlan, + }); + + if (!plan) { + throw new Error('No instructions were found in the instruction plan.'); + } + + return plan; + }; + }; +} + +type TraverseContext = { + parent: InstructionPlan | null; + parentCandidates: SingleTransactionPlan[]; + createSingleTransactionPlan: ( + instructions?: IInstruction[] + ) => Promise; + addInstructionsToSingleTransactionPlan: ( + plan: SingleTransactionPlan, + instructions: IInstruction[] + ) => Promise; +}; + +async function traverse( + instructionPlan: InstructionPlan, + context: TraverseContext +): Promise { + switch (instructionPlan.kind) { + case 'sequential': + return await traverseSequential(instructionPlan, context); + case 'parallel': + return await traverseParallel(instructionPlan, context); + case 'single': + return await traverseSingle(instructionPlan, context); + case 'iterable': + return await traverseIterable(instructionPlan, context); + default: + instructionPlan satisfies never; + throw new Error( + `Unknown instruction plan kind: ${(instructionPlan as { kind: string }).kind}` + ); + } +} + +async function traverseSequential( + instructionPlan: SequentialInstructionPlan, + context: TraverseContext +): Promise { + let candidate: SingleTransactionPlan | null = null; + const mustEntirelyFitInCandidate = + context.parent && + (context.parent.kind === 'parallel' || !instructionPlan.divisible); + if (mustEntirelyFitInCandidate) { + const allInstructions = getAllInstructions(instructionPlan); + candidate = allInstructions + ? selectCandidate(context.parentCandidates, allInstructions) + : null; + if (candidate && allInstructions) { + await context.addInstructionsToSingleTransactionPlan( + candidate, + allInstructions + ); + return null; + } + } else { + candidate = + context.parentCandidates.length > 0 ? context.parentCandidates[0] : null; + } + + const transactionPlans: TransactionPlan[] = []; + for (const plan of instructionPlan.plans) { + const transactionPlan = await traverse(plan, { + ...context, + parent: instructionPlan, + parentCandidates: candidate ? [candidate] : [], + }); + if (transactionPlan) { + candidate = getSequentialCandidate(transactionPlan); + const newPlans = + transactionPlan.kind === 'sequential' && + (transactionPlan.divisible || !instructionPlan.divisible) + ? transactionPlan.plans + : [transactionPlan]; + transactionPlans.push(...newPlans); + } + } + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length === 0) { + return null; + } + return { + kind: 'sequential', + divisible: instructionPlan.divisible, + plans: transactionPlans, + }; +} + +async function traverseParallel( + instructionPlan: ParallelInstructionPlan, + context: TraverseContext +): Promise { + const candidates: SingleTransactionPlan[] = [...context.parentCandidates]; + const transactionPlans: TransactionPlan[] = []; + + // Reorder children so iterable plans are last. + const sortedChildren = [ + ...instructionPlan.plans.filter((plan) => plan.kind !== 'iterable'), + ...instructionPlan.plans.filter((plan) => plan.kind === 'iterable'), + ]; + + for (const plan of sortedChildren) { + const transactionPlan = await traverse(plan, { + ...context, + parent: instructionPlan, + parentCandidates: candidates, + }); + if (transactionPlan) { + candidates.push(...getParallelCandidates(transactionPlan)); + const newPlans = + transactionPlan.kind === 'parallel' + ? transactionPlan.plans + : [transactionPlan]; + transactionPlans.push(...newPlans); + } + } + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length === 0) { + return null; + } + return { kind: 'parallel', plans: transactionPlans }; +} + +async function traverseSingle( + instructionPlan: SingleInstructionPlan, + context: TraverseContext +): Promise { + const ix = instructionPlan.instruction; + const candidate = selectCandidate(context.parentCandidates, [ix]); + if (candidate) { + await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); + return null; + } + return await context.createSingleTransactionPlan([ix]); +} + +async function traverseIterable( + instructionPlan: IterableInstructionPlan, + context: TraverseContext +): Promise { + const iterator = instructionPlan.getIterator(); + const transactionPlans: SingleTransactionPlan[] = []; + const candidates = [...context.parentCandidates]; + + while (iterator.hasNext()) { + const candidateResult = selectCandidateForIterator(candidates, iterator); + if (candidateResult) { + const [candidate, ix] = candidateResult; + await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); + } else { + const newPlan = await context.createSingleTransactionPlan(); + const ix = iterator.next(newPlan.message); + if (!ix) { + throw new Error( + 'Could not fit `InterableInstructionPlan` into a transaction' + ); + } + await context.addInstructionsToSingleTransactionPlan(newPlan, [ix]); + 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); + } + } + + if (transactionPlans.length === 1) { + return transactionPlans[0]; + } + if (transactionPlans.length === 0) { + return null; + } + if (context.parent?.kind === 'parallel') { + return { kind: 'parallel', plans: transactionPlans }; + } + return { + kind: 'sequential', + divisible: + context.parent?.kind === 'sequential' ? context.parent.divisible : true, + plans: transactionPlans, + }; +} + +function getSequentialCandidate( + latestPlan: TransactionPlan +): SingleTransactionPlan | null { + if (latestPlan.kind === 'single') { + return latestPlan; + } + if (latestPlan.kind === 'sequential' && latestPlan.plans.length > 0) { + return getSequentialCandidate( + latestPlan.plans[latestPlan.plans.length - 1] + ); + } + return null; +} + +function getParallelCandidates( + latestPlan: TransactionPlan +): SingleTransactionPlan[] { + return getAllSingleTransactionPlans(latestPlan); +} + +function getAllInstructions( + instructionPlan: InstructionPlan +): IInstruction[] | null { + if (instructionPlan.kind === 'single') { + return [instructionPlan.instruction]; + } + if (instructionPlan.kind === 'iterable') { + return instructionPlan.getAll(); + } + return instructionPlan.plans.reduce( + (acc, plan) => { + if (acc === null) return null; + const instructions = getAllInstructions(plan); + if (instructions === null) return null; + acc.push(...instructions); + return acc; + }, + [] as IInstruction[] | null + ); +} + +function selectCandidateForIterator( + candidates: SingleTransactionPlan[], + iterator: InstructionIterator +): [SingleTransactionPlan, IInstruction] | null { + for (const candidate of candidates) { + const ix = iterator.next(candidate.message); + if (ix) { + return [candidate, ix]; + } + } + return null; +} + +function selectCandidate( + candidates: SingleTransactionPlan[], + instructions: IInstruction[] +): SingleTransactionPlan | null { + const firstValidCandidate = candidates.find((candidate) => + isValidCandidate(candidate, instructions) + ); + return firstValidCandidate ?? null; +} + +function isValidCandidate( + candidate: SingleTransactionPlan, + instructions: IInstruction[] +): boolean { + const message = appendTransactionMessageInstructions( + instructions, + candidate.message + ); + return getRemainingTransactionSize(message) >= 0; +} + +export function getRemainingTransactionSize(message: BaseTransactionMessage) { + return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts similarity index 78% rename from clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts rename to clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts index 69c4363..73485b2 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts @@ -17,25 +17,29 @@ import { SimulateTransactionApi, TransactionSigner, } from '@solana/kit'; -import { - TransactionPlanner, - TransactionPlannerConfig, -} from './transactionPlanner'; import { COMPUTE_BUDGET_PROGRAM_ADDRESS, ComputeBudgetInstruction, getSetComputeUnitLimitInstruction, identifyComputeBudgetInstruction, } from '@solana-program/compute-budget'; -import { SingleTransactionPlan, TransactionPlan } from './transactionPlan'; +import { + getAllSingleTransactionPlans, + SingleTransactionPlan, + TransactionPlan, +} from './transactionPlan'; import { getTimedCacheFunction, Mutable } from './internal'; - -export function transformNewTransactionPlannerMessage( - transformer: Required['newTransactionTransformer'], - planner: TransactionPlanner -): TransactionPlanner { - return async (instructionPlan, config) => { - return await planner(instructionPlan, { +import { + TransactionPlannerFactory, + TransactionPlannerFactoryConfig, +} from './transactionPlannerFactory'; + +function transformTransactionPlannerNewMessage( + transformer: Required['newTransactionTransformer'], + plannerFactory: TransactionPlannerFactory +): TransactionPlannerFactory { + return (config) => { + return plannerFactory({ ...config, newTransactionTransformer: async (tx) => { const transformedTx = await transformer(tx); @@ -47,42 +51,44 @@ export function transformNewTransactionPlannerMessage( }; } -export function transformTransactionPlan( +function transformTransactionPlan( transformer: (transactionPlan: TransactionPlan) => Promise, - planner: TransactionPlanner -): TransactionPlanner { - return async (instructionPlan, config) => { - return await transformer(await planner(instructionPlan, config)); + plannerFactory: TransactionPlannerFactory +): TransactionPlannerFactory { + return (config) => { + const planner = plannerFactory(config); + return async (instructionPlan) => + await transformer(await planner(instructionPlan)); }; } export function prependTransactionPlannerInstructions( instructions: IInstruction[], - planner: TransactionPlanner -): TransactionPlanner { - return transformNewTransactionPlannerMessage( + plannerFactory: TransactionPlannerFactory +): TransactionPlannerFactory { + return transformTransactionPlannerNewMessage( (tx) => Promise.resolve(prependTransactionMessageInstructions(instructions, tx)), - planner + plannerFactory ); } export function appendTransactionPlannerInstructions( instructions: IInstruction[], - planner: TransactionPlanner -): TransactionPlanner { - return transformNewTransactionPlannerMessage( + plannerFactory: TransactionPlannerFactory +): TransactionPlannerFactory { + return transformTransactionPlannerNewMessage( (tx) => Promise.resolve(appendTransactionMessageInstructions(instructions, tx)), - planner + plannerFactory ); } export function setTransactionPlannerFeePayer( feePayer: Address, - planner: TransactionPlanner -): TransactionPlanner { - return transformNewTransactionPlannerMessage( + plannerFactory: TransactionPlannerFactory +): TransactionPlannerFactory { + return transformTransactionPlannerNewMessage( ( tx: TTransactionMessage ) => @@ -90,15 +96,15 @@ export function setTransactionPlannerFeePayer( setTransactionMessageFeePayer(feePayer, tx) as TTransactionMessage & ITransactionMessageWithFeePayer ), - planner + plannerFactory ); } export function setTransactionPlannerFeePayerSigner( feePayerSigner: TransactionSigner, - planner: TransactionPlanner -): TransactionPlanner { - return transformNewTransactionPlannerMessage( + plannerFactory: TransactionPlannerFactory +): TransactionPlannerFactory { + return transformTransactionPlannerNewMessage( ( tx: TTransactionMessage ) => @@ -108,24 +114,24 @@ export function setTransactionPlannerFeePayerSigner( tx ) as TTransactionMessage & ITransactionMessageWithFeePayerSigner ), - planner + plannerFactory ); } export function setTransactionPlannerLifetimeUsingLatestBlockhash( rpc: Rpc, - planner: TransactionPlanner -): TransactionPlanner { + plannerFactory: TransactionPlannerFactory +): TransactionPlannerFactory { // Cache the latest blockhash for 60 seconds. const getBlockhash = getTimedCacheFunction(async () => { const { value } = await rpc.getLatestBlockhash().send(); return value; }, 60_000); - return transformNewTransactionPlannerMessage( + return transformTransactionPlannerNewMessage( async (tx) => setTransactionMessageLifetimeUsingBlockhash(await getBlockhash(), tx), - planner + plannerFactory ); } @@ -137,15 +143,15 @@ const MAX_COMPUTE_UNIT_LIMIT = 1_400_000; // move in a granular package so `instruction-plans` can use it. export function estimateAndSetComputeUnitLimitForTransactionPlanner( rpc: Rpc, - planner: TransactionPlanner, + plannerFactory: TransactionPlannerFactory, chunkSize: number | null = 10 -): TransactionPlanner { +): TransactionPlannerFactory { // Create a function to estimate the compute unit limit for a transaction. const estimateComputeUnitLimit = getComputeUnitEstimateForTransactionMessageFactory({ rpc }); // Add a compute unit limit instruction to the transaction if it doesn't exist. - const plannerWithComputeBudgetLimits = transformNewTransactionPlannerMessage( + const plannerWithComputeBudgetLimits = transformTransactionPlannerNewMessage( (tx) => { if (getComputeUnitLimitInstructionIndex(tx) >= 0) { return Promise.resolve(tx); @@ -158,7 +164,7 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( ) ); }, - planner + plannerFactory ); // Transform the final transaction plan to set the correct compute unit limit. @@ -224,12 +230,3 @@ function getComputeUnitLimitInstructionIndex( ); }); } - -function getAllSingleTransactionPlans( - transactionPlan: TransactionPlan -): SingleTransactionPlan[] { - if (transactionPlan.kind === 'single') { - return [transactionPlan]; - } - return transactionPlan.plans.flatMap(getAllSingleTransactionPlans); -} diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index c24f471..67feb08 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -19,7 +19,7 @@ import { const MINIMUM_INSTRUCTION_SIZE = 35; const MINIMUM_TRANSACTION_SIZE = 136; -const MAXIMUM_TRANSACTION_SIZE = 1231; // 1280 - 48 (for header) - 1 (for shortU16) +const MAXIMUM_TRANSACTION_SIZE = TRANSACTION_SIZE_LIMIT - 1; // (for shortU16) export function parallelInstructionPlan( plans: InstructionPlan[] diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index fbde514..0e22b99 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1,5 +1,4 @@ import test from 'ava'; -import { createBaseTransactionPlanner } from '../../src'; import { instructionFactory, instructionIteratorFactory, @@ -15,6 +14,7 @@ import { sequentialTransactionPlan, singleTransactionPlanFactory, } from './_transactionPlanHelpers'; +import { createBaseTransactionPlannerFactory } from '../../src'; function defaultFactories() { return { @@ -30,7 +30,7 @@ function defaultFactories() { */ test('it plans a single instruction', async (t) => { const { instruction, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(42); @@ -48,7 +48,7 @@ test('it plans a single instruction', async (t) => { */ test('it plans a sequential plan with instructions that all fit in a single transaction', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -73,7 +73,7 @@ test('it plans a sequential plan with instructions that all fit in a single tran */ test('it plans a sequential plan with instructions that must be split accross multiple transactions (v1)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -103,7 +103,7 @@ test('it plans a sequential plan with instructions that must be split accross mu */ test('it plans a sequential plan with instructions that must be split accross multiple transactions (v2)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. const instructionB = instruction(txPercent(50)); @@ -134,7 +134,7 @@ test('it plans a sequential plan with instructions that must be split accross mu */ test('it simplifies sequential plans with one child or less', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -161,7 +161,7 @@ test('it simplifies sequential plans with one child or less', async (t) => { */ test('it simplifies nested sequential plans', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -193,7 +193,7 @@ test('it simplifies nested sequential plans', async (t) => { */ test('it plans a parallel plan with instructions that all fit in a single transaction', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -218,7 +218,7 @@ test('it plans a parallel plan with instructions that all fit in a single transa */ test('it plans a parallel plan with instructions that must be split accross multiple transactions (v1)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -248,7 +248,7 @@ test('it plans a parallel plan with instructions that must be split accross mult */ test('it plans a parallel plan with instructions that must be split accross multiple transactions (v2)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. const instructionB = instruction(txPercent(50)); @@ -279,7 +279,7 @@ test('it plans a parallel plan with instructions that must be split accross mult */ test('it simplifies parallel plans with one child or less', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -306,7 +306,7 @@ test('it simplifies parallel plans with one child or less', async (t) => { */ test('it simplifies nested parallel plans', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -341,7 +341,7 @@ test('it simplifies nested parallel plans', async (t) => { */ test('it re-uses previous parallel transactions if there is space', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(25)); @@ -378,7 +378,7 @@ test('it re-uses previous parallel transactions if there is space', async (t) => */ test('it can merge sequential plans in a parallel plan if the whole sequential plan fits', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(25)); const instructionB = instruction(txPercent(25)); @@ -419,7 +419,7 @@ test('it can merge sequential plans in a parallel plan if the whole sequential p */ test('it does not split a sequential plan on a parallel parent', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -458,7 +458,7 @@ test('it does not split a sequential plan on a parallel parent', async (t) => { */ test('it can split parallel plans inside sequential plans as long as they follow the sequence', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -500,7 +500,7 @@ test('it can split parallel plans inside sequential plans as long as they follow */ test('it cannnot split a parallel plan in a sequential plan if that would break the sequence', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -542,7 +542,7 @@ test('it cannnot split a parallel plan in a sequential plan if that would break */ test('it plans an non-divisible sequential plan with instructions that all fit in a single transaction', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -567,7 +567,7 @@ test('it plans an non-divisible sequential plan with instructions that all fit i */ test('it plans a non-divisible sequential plan with instructions that must be split accross multiple transactions (v1)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -597,7 +597,7 @@ test('it plans a non-divisible sequential plan with instructions that must be sp */ test('it plans a non-divisible sequential plan with instructions that must be split accross multiple transactions (v2)', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. const instructionB = instruction(txPercent(50)); @@ -628,7 +628,7 @@ test('it plans a non-divisible sequential plan with instructions that must be sp */ test('it simplifies non-divisible sequential plans with one child or less', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -657,7 +657,7 @@ test('it simplifies non-divisible sequential plans with one child or less', asyn */ test('it simplifies nested non-divisible sequential plans', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -691,7 +691,7 @@ test('it simplifies nested non-divisible sequential plans', async (t) => { */ test('it simplifies divisible sequential plans inside non-divisible sequential plans', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -725,7 +725,7 @@ test('it simplifies divisible sequential plans inside non-divisible sequential p */ test('it does not simplify non-divisible sequential plans inside divisible sequential plans', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -763,7 +763,7 @@ test('it does not simplify non-divisible sequential plans inside divisible seque */ test('it can merge non-divisible sequential plans in a parallel plan if the whole sequential plan fits', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(25)); const instructionB = instruction(txPercent(25)); @@ -804,7 +804,7 @@ test('it can merge non-divisible sequential plans in a parallel plan if the whol */ test('it does not split a non-divisible sequential plan on a parallel parent', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -843,7 +843,7 @@ test('it does not split a non-divisible sequential plan on a parallel parent', a */ test('it can merge non-divisible sequential plans in a sequential plan if the whole plan fits', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(25)); const instructionB = instruction(txPercent(25)); @@ -884,7 +884,7 @@ test('it can merge non-divisible sequential plans in a sequential plan if the wh */ test('it does not split a non-divisible sequential plan on a sequential parent', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -923,7 +923,7 @@ test('it does not split a non-divisible sequential plan on a sequential parent', */ test('it plans non-divisible sequentials plans with parallel children', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -965,7 +965,7 @@ test('it plans non-divisible sequentials plans with parallel children', async (t */ test('it plans non-divisible sequentials plans with divisible sequential children', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -1002,7 +1002,7 @@ test('it plans non-divisible sequentials plans with divisible sequential childre */ test('it iterate over iterable instruction plans', async (t) => { const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const iteratorIx = iterator(txPercent(250)); @@ -1025,7 +1025,7 @@ test('it iterate over iterable instruction plans', async (t) => { test('it combines single instruction plans with iterable instruction plans', async (t) => { const { txPercent, iterator, instruction, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(50)); const iteratorB = iterator(txPercent(50)); @@ -1050,7 +1050,7 @@ test('it combines single instruction plans with iterable instruction plans', asy */ test('it can handle parallel iterable instruction plans', async (t) => { const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const iteratorA = iterator(txPercent(250)); @@ -1073,7 +1073,7 @@ test('it can handle parallel iterable instruction plans', async (t) => { */ test('it can handle non-divisible sequential iterable instruction plans', async (t) => { const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const iteratorA = iterator(txPercent(250)); @@ -1092,7 +1092,7 @@ test('it can handle non-divisible sequential iterable instruction plans', async */ test('it simplifies iterable instruction plans that fit in a single transaction', async (t) => { const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const iteratorA = iterator(txPercent(100)); @@ -1112,7 +1112,7 @@ test('it simplifies iterable instruction plans that fit in a single transaction' test('it uses iterable instruction plans to fill gaps in parallel candidates', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(75)); const instructionB = instruction(txPercent(50)); @@ -1144,7 +1144,7 @@ test('it uses iterable instruction plans to fill gaps in parallel candidates', a test('it handles parallel iterable instruction plans last to fill gaps in previous parallel candidates', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const iteratorA = iterator(txPercent(25) + txPercent(50) + txPercent(50)); // 125% const instructionB = instruction(txPercent(75)); @@ -1176,7 +1176,7 @@ test('it handles parallel iterable instruction plans last to fill gaps in previo test('it uses iterable instruction plans to fill gaps in sequential candidates', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(75)); const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% @@ -1207,7 +1207,7 @@ test('it uses iterable instruction plans to fill gaps in sequential candidates', test('it uses iterable instruction plans to fill gaps in non-divisible sequential candidates', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(75)); const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% @@ -1239,7 +1239,7 @@ test('it uses iterable instruction plans to fill gaps in non-divisible sequentia test('it uses parallel iterable instruction plans to fill gaps in sequential candidates', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(75)); const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% @@ -1273,7 +1273,7 @@ test('it uses parallel iterable instruction plans to fill gaps in sequential can test('it uses the whole sequential iterable instruction plan when it fits in the parent parallel candidate', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(25)); const iteratorB = iterator(txPercent(50)); @@ -1308,7 +1308,7 @@ test('it uses the whole sequential iterable instruction plan when it fits in the test('it uses the whole non-divisible sequential iterable instruction plan when it fits in the parent sequential candidate', async (t) => { const { txPercent, instruction, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(25)); const iteratorB = iterator(txPercent(50)); @@ -1349,7 +1349,7 @@ test('it uses the whole non-divisible sequential iterable instruction plan when */ test('complex example 1', async (t) => { const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(40)); const instructionB = instruction(txPercent(40)); @@ -1410,7 +1410,7 @@ test('complex example 1', async (t) => { test('complex example 2', async (t) => { const { instruction, iterator, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlanner({ version: 0 }); + const planner = createBaseTransactionPlannerFactory({ version: 0 })(); const instructionA = instruction(txPercent(20)); const instructionB = instruction(txPercent(20)); From 192eede6daed6a8f5911cb16fc9dcd22161c41f1 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 8 Apr 2025 09:07:09 +0100 Subject: [PATCH 067/112] wip --- .../transactionPlanExecutor.ts | 92 +------------ .../transactionPlanExecutorFactory.ts | 121 ++++++++++++++++++ ...ansactionPlanExecutorFactoryDecorators.ts} | 0 3 files changed, 122 insertions(+), 91 deletions(-) create mode 100644 clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts rename clients/js/src/instructionPlansDraft/{transactionPlanExecutorDecorators.ts => transactionPlanExecutorFactoryDecorators.ts} (100%) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index 70c7b4a..ab0aaff 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -1,96 +1,6 @@ -import { - BaseTransactionMessage, - getSignatureFromTransaction, - Transaction, -} from '@solana/kit'; -import { - ParallelTransactionPlan, - SequentialTransactionPlan, - SingleTransactionPlan, - TransactionPlan, -} from './transactionPlan'; +import { TransactionPlan } from './transactionPlan'; import { TransactionPlanResult } from './transactionPlanResult'; export type TransactionPlanExecutor = ( transactionPlan: TransactionPlan ) => Promise>; - -export function createBaseTransactionPlanExecutor( - sendAndConfirm: ( - transactionMessage: TTransactionMessage - ) => Promise -): TransactionPlanExecutor { - return async (plan): Promise => { - const context: TraverseContext = { sendAndConfirm }; - return await traverse(plan, context); - }; -} - -type TraverseContext = { - sendAndConfirm: ( - transactionMessage: TTransactionMessage - ) => Promise; -}; - -async function traverse( - transactionPlan: TransactionPlan, - context: TraverseContext -): Promise { - switch (transactionPlan.kind) { - case 'sequential': - return await traverseSequential(transactionPlan, context); - case 'parallel': - return await traverseParallel(transactionPlan, context); - case 'single': - return await traverseSingle(transactionPlan, context); - default: - transactionPlan satisfies never; - throw new Error( - `Unknown instruction plan kind: ${(transactionPlan as { kind: string }).kind}` - ); - } -} - -async function traverseSequential( - transactionPlan: SequentialTransactionPlan, - context: TraverseContext -): Promise { - const results: TransactionPlanResult[] = []; - for (const subPlan of transactionPlan.plans) { - const result = await traverse(subPlan, context); - results.push(result); - - // TODO: Handle cancellations. - } - return { kind: 'sequential', plans: results }; -} - -async function traverseParallel( - transactionPlan: ParallelTransactionPlan, - context: TraverseContext -): Promise { - const results = await Promise.all( - transactionPlan.plans.map((subPlan) => traverse(subPlan, context)) - ); - - // TODO: Handle chunking via decorators. - - return { kind: 'parallel', plans: results }; -} - -async function traverseSingle( - transactionPlan: SingleTransactionPlan, - context: TraverseContext -): Promise { - const transaction = await context.sendAndConfirm(transactionPlan.message); - - // TODO: Handle error. - - return { - kind: 'single', - context: null, - message: transactionPlan.message, - signature: getSignatureFromTransaction(transaction), - status: { kind: 'success' }, - }; -} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts new file mode 100644 index 0000000..360fb8d --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts @@ -0,0 +1,121 @@ +import { + BaseTransactionMessage, + getSignatureFromTransaction, + Transaction, +} from '@solana/kit'; +import { + ParallelTransactionPlan, + SequentialTransactionPlan, + SingleTransactionPlan, + TransactionPlan, +} from './transactionPlan'; +import { TransactionPlanResult } from './transactionPlanResult'; +import { TransactionPlanExecutor } from './transactionPlanExecutor'; + +type TransactionPlanExecutorFactoryTransformer = ( + transactionPlan: TransactionPlan, + next: (transactionPlan: TransactionPlan) => Promise +) => Promise; + +export type TransactionPlanExecutorFactoryConfig = { + transformer?: TransactionPlanExecutorFactoryTransformer; +}; + +export type TransactionPlanExecutorFactory< + TContext extends object | null = null, +> = ( + config?: TransactionPlanExecutorFactoryConfig +) => TransactionPlanExecutor; + +export function createBaseTransactionPlanExecutorFactory( + sendAndConfirm: ( + transactionMessage: TTransactionMessage + ) => Promise +): TransactionPlanExecutorFactory { + return (config) => { + const wrapInTransformer: TransactionPlanExecutorFactoryTransformer = ( + transactionPlan, + next + ) => { + if (config?.transformer) { + return config.transformer(transactionPlan, next); + } + return next(transactionPlan); + }; + + return async (plan): Promise => { + const context: TraverseContext = { sendAndConfirm, wrapInTransformer }; + return await traverse(plan, context); + }; + }; +} + +type TraverseContext = { + sendAndConfirm: ( + transactionMessage: TTransactionMessage + ) => Promise; + wrapInTransformer: TransactionPlanExecutorFactoryTransformer; +}; + +async function traverse( + transactionPlan: TransactionPlan, + context: TraverseContext +): Promise { + switch (transactionPlan.kind) { + case 'sequential': + return await traverseSequential(transactionPlan, context); + case 'parallel': + return await traverseParallel(transactionPlan, context); + case 'single': + return await traverseSingle(transactionPlan, context); + default: + transactionPlan satisfies never; + throw new Error( + `Unknown instruction plan kind: ${(transactionPlan as { kind: string }).kind}` + ); + } +} + +async function traverseSequential( + transactionPlan: SequentialTransactionPlan, + context: TraverseContext +): Promise { + const results: TransactionPlanResult[] = []; + for (const subPlan of transactionPlan.plans) { + const result = await traverse(subPlan, context); + results.push(result); + + // TODO: Handle cancellations. + } + return { kind: 'sequential', plans: results }; +} + +async function traverseParallel( + transactionPlan: ParallelTransactionPlan, + context: TraverseContext +): Promise { + const results = await Promise.all( + transactionPlan.plans.map((subPlan) => traverse(subPlan, context)) + ); + + // TODO: Handle chunking via decorators. + + return { kind: 'parallel', plans: results }; +} + +async function traverseSingle( + transactionPlan: SingleTransactionPlan, + context: TraverseContext +): Promise { + const transaction = await context.sendAndConfirm(transactionPlan.message); + + // TODO: Handle error. + + return { + kind: 'single', + context: null, + message: transactionPlan.message, + signature: getSignatureFromTransaction(transaction), + status: { kind: 'success' }, + }; +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts rename to clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts From 94af55219ae5a15ac9926b2be736f37cb2dfdedd Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 8 Apr 2025 15:57:45 +0100 Subject: [PATCH 068/112] wip --- ...ransactionPlanExecutorFactoryDecorators.ts | 100 +++++++++++------- 1 file changed, 64 insertions(+), 36 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts index a052f33..aba1cf0 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts @@ -5,59 +5,87 @@ import { } from '@solana/kit'; import { getTimedCacheFunction, Mutable } from './internal'; import { SingleTransactionPlan } from './transactionPlan'; -import { TransactionPlanExecutor } from './transactionPlanExecutor'; +import { + TransactionPlanExecutorFactory, + TransactionPlanExecutorFactoryConfig, +} from './transactionPlanExecutorFactory'; // TODO: implement // - Chunk parallel transactions (Needs special transformer) // - Add support for custom +export function transformTransactionPlanExecutorFactory( + transformer: Required['transformer'], + executorFactory: TransactionPlanExecutorFactory +): TransactionPlanExecutorFactory { + return (config) => { + return executorFactory({ + ...config, + transformer: (transactionPlan, next) => { + const newNext: typeof next = (plan) => transformer(plan, next); + return config?.transformer + ? config.transformer(transactionPlan, newNext) + : newNext(transactionPlan); + }, + }); + }; +} + export function refreshBlockhashForTransactionPlanExecutor( rpc: Rpc, - executor: TransactionPlanExecutor -): TransactionPlanExecutor { + executorFactory: TransactionPlanExecutorFactory +): TransactionPlanExecutorFactory { // Cache the latest blockhash for 60 seconds. const getBlockhash = getTimedCacheFunction(async () => { const { value } = await rpc.getLatestBlockhash().send(); return value; }, 60_000); - return async (transactionPlan) => { - if (transactionPlan.kind !== 'single') { - return await executor(transactionPlan); - } - - (transactionPlan as Mutable).message = - setTransactionMessageLifetimeUsingBlockhash( - await getBlockhash(), - transactionPlan.message - ); - return await executor(transactionPlan); - }; + return transformTransactionPlanExecutorFactory( + async (transactionPlan, next) => { + if (transactionPlan.kind !== 'single') { + return await next(transactionPlan); + } + + (transactionPlan as Mutable).message = + setTransactionMessageLifetimeUsingBlockhash( + await getBlockhash(), + transactionPlan.message + ); + return await next(transactionPlan); + }, + executorFactory + ); } export function retryTransactionPlanExecutor( maxRetries: number, - executor: TransactionPlanExecutor -): TransactionPlanExecutor { - return async (transactionPlan) => { - if (transactionPlan.kind !== 'single') { - return await executor(transactionPlan); - } - - let retries = 0; - let lastError: Error | null = null; - - // x retries means x+1 attempts. - while (retries < maxRetries + 1) { - try { - return await executor(transactionPlan); - } catch (error) { - // TODO: Should we not retry on certain error codes? - retries++; - lastError = error as Error; + executorFactory: TransactionPlanExecutorFactory +): TransactionPlanExecutorFactory { + return transformTransactionPlanExecutorFactory( + async (transactionPlan, next) => { + if (transactionPlan.kind !== 'single') { + return await next(transactionPlan); } - } - throw lastError; - }; + let retries = 0; + let lastError: Error | null = null; + + // x retries means x+1 attempts. + while (retries < maxRetries + 1) { + try { + return await next(transactionPlan); + } catch (error) { + // TODO: Should we not retry on certain error codes? + retries++; + lastError = error as Error; + } + } + + throw lastError; + // TODO: Catch and return failed results instead of failing. + // TODO: Have another decorator that fails whilst returning the result in the context at the very end. + }, + executorFactory + ); } From aa5bc8bda741e365e6bd045a7c34e63ca7f1c477 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 09:51:54 +0100 Subject: [PATCH 069/112] wip --- .../instructionPlansDraft/transactionPlan.ts | 5 +- .../transactionPlannerFactory.ts | 36 +-- .../transactionPlannerFactoryDecorators.ts | 23 +- .../_transactionPlanHelpers.ts | 32 +- .../transactionPlanner.test.ts | 288 ++++++++++++------ 5 files changed, 241 insertions(+), 143 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlan.ts b/clients/js/src/instructionPlansDraft/transactionPlan.ts index 6a08b0d..66b4411 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlan.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlan.ts @@ -1,4 +1,4 @@ -import { BaseTransactionMessage } from '@solana/kit'; +import { CompilableTransactionMessage } from '@solana/kit'; export type TransactionPlan = | SequentialTransactionPlan @@ -17,7 +17,8 @@ export type ParallelTransactionPlan = Readonly<{ }>; export type SingleTransactionPlan< - TTransactionMessage extends BaseTransactionMessage = BaseTransactionMessage, + TTransactionMessage extends + CompilableTransactionMessage = CompilableTransactionMessage, > = Readonly<{ kind: 'single'; message: TTransactionMessage; diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts index 533c877..f256b11 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts @@ -1,9 +1,7 @@ import { appendTransactionMessageInstructions, - BaseTransactionMessage, - createTransactionMessage, + CompilableTransactionMessage, IInstruction, - TransactionVersion, } from '@solana/kit'; import { InstructionIterator, @@ -26,37 +24,27 @@ import { import { TransactionPlanner } from './transactionPlanner'; export type TransactionPlannerFactory = ( - configs?: TransactionPlannerFactoryConfig + configs: TransactionPlannerFactoryConfig ) => TransactionPlanner; -type TransactionMessageTransformer = < - TTransactionMessage extends BaseTransactionMessage, ->( - transactionMessage: TTransactionMessage -) => Promise; - export type TransactionPlannerFactoryConfig = { - newTransactionTransformer?: TransactionMessageTransformer; - newInstructionsTransformer?: TransactionMessageTransformer; + createTransactionMessage: () => Promise; + newInstructionsTransformer?: < + TTransactionMessage extends CompilableTransactionMessage, + >( + transactionMessage: TTransactionMessage + ) => Promise; }; -export function createBaseTransactionPlannerFactory({ - version, -}: { - version: TransactionVersion; -}): TransactionPlannerFactory { +export function createBaseTransactionPlannerFactory(): TransactionPlannerFactory { return (config) => { const createSingleTransactionPlan = async ( instructions: IInstruction[] = [] ): Promise => { const plan: SingleTransactionPlan = { kind: 'single', - message: createTransactionMessage({ version }), + message: await config.createTransactionMessage(), }; - if (config?.newTransactionTransformer) { - (plan as Mutable).message = - await config.newTransactionTransformer(plan.message); - } if (instructions.length > 0) { await addInstructionsToSingleTransactionPlan(plan, instructions); } @@ -354,6 +342,8 @@ function isValidCandidate( return getRemainingTransactionSize(message) >= 0; } -export function getRemainingTransactionSize(message: BaseTransactionMessage) { +export function getRemainingTransactionSize( + message: CompilableTransactionMessage +) { return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); } diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts index 73485b2..26b8533 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts @@ -29,23 +29,20 @@ import { TransactionPlan, } from './transactionPlan'; import { getTimedCacheFunction, Mutable } from './internal'; -import { - TransactionPlannerFactory, - TransactionPlannerFactoryConfig, -} from './transactionPlannerFactory'; +import { TransactionPlannerFactory } from './transactionPlannerFactory'; function transformTransactionPlannerNewMessage( - transformer: Required['newTransactionTransformer'], + transformer: ( + transactionMessage: TTransactionMessage + ) => Promise, plannerFactory: TransactionPlannerFactory ): TransactionPlannerFactory { return (config) => { return plannerFactory({ ...config, - newTransactionTransformer: async (tx) => { - const transformedTx = await transformer(tx); - return config?.newTransactionTransformer - ? await config.newTransactionTransformer(transformedTx) - : transformedTx; + createTransactionMessage: async () => { + const tx = await config.createTransactionMessage(); + return await transformer(tx); }, }); }; @@ -177,7 +174,7 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( const instructionIndex = getComputeUnitLimitInstructionIndex( singlePlan.message ); - const newMessage = + const newMessage: CompilableTransactionMessage = instructionIndex === -1 ? prependTransactionMessageInstruction( getSetComputeUnitLimitInstruction({ @@ -185,7 +182,7 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( }), singlePlan.message ) - : { + : ({ ...singlePlan.message, instructions: [ ...singlePlan.message.instructions.slice(0, instructionIndex), @@ -196,7 +193,7 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( instructionIndex + 1 ), ], - }; + } as CompilableTransactionMessage); (singlePlan as Mutable).message = newMessage; } ); diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts index af24dc7..c8421a4 100644 --- a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts @@ -1,14 +1,19 @@ import { + Address, appendTransactionMessageInstructions, - BaseTransactionMessage, - createTransactionMessage, + Blockhash, + CompilableTransactionMessage, IInstruction, + createTransactionMessage as kitCreateTransactionMessage, + pipe, + setTransactionMessageFeePayer, + setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit'; import { - TransactionPlan, ParallelTransactionPlan, SequentialTransactionPlan, SingleTransactionPlan, + TransactionPlan, } from '../../src'; export function parallelTransactionPlan( @@ -29,17 +34,30 @@ export function nonDivisibleSequentialTransactionPlan( return { kind: 'sequential', divisible: false, plans }; } +const MOCK_FEE_PAYER = + 'Gm1uVH3JxiLgafByNNmnoxLncB7ytpyWNqX3kRM9tSxN' as Address; +const MOCK_BLOCKHASH = { + blockhash: '11111111111111111111111111111111' as Blockhash, + lastValidBlockHeight: 0n, +} as const; + +export const getMockCreateTransactionMessage = () => { + return pipe( + kitCreateTransactionMessage({ version: 0 }), + (tx) => setTransactionMessageLifetimeUsingBlockhash(MOCK_BLOCKHASH, tx), + (tx) => setTransactionMessageFeePayer(MOCK_FEE_PAYER, tx) + ); +}; + export function singleTransactionPlanFactory( - defaultMessage?: () => BaseTransactionMessage + createTransactionMessage?: () => CompilableTransactionMessage ) { - const defaultMessageFn = - defaultMessage ?? (() => createTransactionMessage({ version: 0 })); return (instructions: IInstruction[]): SingleTransactionPlan => { return { kind: 'single', message: appendTransactionMessageInstructions( instructions, - defaultMessageFn() + (createTransactionMessage ?? getMockCreateTransactionMessage)() ), }; }; diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 0e22b99..468a3a2 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -9,19 +9,32 @@ import { transactionPercentFactory, } from './_instructionPlanHelpers'; import { + getMockCreateTransactionMessage, nonDivisibleSequentialTransactionPlan, parallelTransactionPlan, sequentialTransactionPlan, singleTransactionPlanFactory, } from './_transactionPlanHelpers'; import { createBaseTransactionPlannerFactory } from '../../src'; +import { CompilableTransactionMessage } from '@solana/kit'; -function defaultFactories() { +function defaultFactories( + createTransactionMessage?: () => CompilableTransactionMessage +) { + const effectiveCreateTransactionMessage = + createTransactionMessage ?? getMockCreateTransactionMessage; return { + createPlanner: () => + createBaseTransactionPlannerFactory()({ + createTransactionMessage: () => + Promise.resolve(effectiveCreateTransactionMessage()), + }), instruction: instructionFactory(), iterator: instructionIteratorFactory(), - txPercent: transactionPercentFactory(), - singleTransactionPlan: singleTransactionPlanFactory(), + txPercent: transactionPercentFactory(effectiveCreateTransactionMessage), + singleTransactionPlan: singleTransactionPlanFactory( + effectiveCreateTransactionMessage + ), }; } @@ -29,8 +42,9 @@ function defaultFactories() { * [A: 42] ───────────────────▶ [Tx: A] */ test('it plans a single instruction', async (t) => { - const { instruction, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(42); @@ -47,8 +61,9 @@ test('it plans a single instruction', async (t) => { * └── [B: 50%] */ test('it plans a sequential plan with instructions that all fit in a single transaction', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -72,8 +87,9 @@ test('it plans a sequential plan with instructions that all fit in a single tran * └── [C: 50%] */ test('it plans a sequential plan with instructions that must be split accross multiple transactions (v1)', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -102,8 +118,9 @@ test('it plans a sequential plan with instructions that must be split accross mu * └── [C: 50%] */ test('it plans a sequential plan with instructions that must be split accross multiple transactions (v2)', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. const instructionB = instruction(txPercent(50)); @@ -133,8 +150,9 @@ test('it plans a sequential plan with instructions that must be split accross mu * └── [B: 50%] */ test('it simplifies sequential plans with one child or less', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -160,8 +178,9 @@ test('it simplifies sequential plans with one child or less', async (t) => { * └── [C: 100%] */ test('it simplifies nested sequential plans', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -192,8 +211,9 @@ test('it simplifies nested sequential plans', async (t) => { * └── [B: 50%] */ test('it plans a parallel plan with instructions that all fit in a single transaction', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -217,8 +237,9 @@ test('it plans a parallel plan with instructions that all fit in a single transa * └── [C: 50%] */ test('it plans a parallel plan with instructions that must be split accross multiple transactions (v1)', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -247,8 +268,9 @@ test('it plans a parallel plan with instructions that must be split accross mult * └── [C: 50%] */ test('it plans a parallel plan with instructions that must be split accross multiple transactions (v2)', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. const instructionB = instruction(txPercent(50)); @@ -278,8 +300,9 @@ test('it plans a parallel plan with instructions that must be split accross mult * └── [B: 50%] */ test('it simplifies parallel plans with one child or less', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -305,8 +328,9 @@ test('it simplifies parallel plans with one child or less', async (t) => { * └── [C: 100%] */ test('it simplifies nested parallel plans', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -340,8 +364,9 @@ test('it simplifies nested parallel plans', async (t) => { * └── [D: 25%] */ test('it re-uses previous parallel transactions if there is space', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(25)); @@ -377,8 +402,9 @@ test('it re-uses previous parallel transactions if there is space', async (t) => * └── [D: 25%] */ test('it can merge sequential plans in a parallel plan if the whole sequential plan fits', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(25)); const instructionB = instruction(txPercent(25)); @@ -418,8 +444,9 @@ test('it can merge sequential plans in a parallel plan if the whole sequential p * └── [D: 33%] */ test('it does not split a sequential plan on a parallel parent', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -457,8 +484,9 @@ test('it does not split a sequential plan on a parallel parent', async (t) => { * └── [D: 33%] */ test('it can split parallel plans inside sequential plans as long as they follow the sequence', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -499,8 +527,9 @@ test('it can split parallel plans inside sequential plans as long as they follow * └── [F: 33%] */ test('it cannnot split a parallel plan in a sequential plan if that would break the sequence', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -541,8 +570,9 @@ test('it cannnot split a parallel plan in a sequential plan if that would break * └── [B: 50%] */ test('it plans an non-divisible sequential plan with instructions that all fit in a single transaction', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -566,8 +596,9 @@ test('it plans an non-divisible sequential plan with instructions that all fit i * └── [C: 50%] */ test('it plans a non-divisible sequential plan with instructions that must be split accross multiple transactions (v1)', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -596,8 +627,9 @@ test('it plans a non-divisible sequential plan with instructions that must be sp * └── [C: 50%] */ test('it plans a non-divisible sequential plan with instructions that must be split accross multiple transactions (v2)', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(60)); // Tx A cannot have Ix B. const instructionB = instruction(txPercent(50)); @@ -627,8 +659,9 @@ test('it plans a non-divisible sequential plan with instructions that must be sp * └── [B: 50%] */ test('it simplifies non-divisible sequential plans with one child or less', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -656,8 +689,9 @@ test('it simplifies non-divisible sequential plans with one child or less', asyn * └── [C: 100%] */ test('it simplifies nested non-divisible sequential plans', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -690,8 +724,9 @@ test('it simplifies nested non-divisible sequential plans', async (t) => { * └── [C: 100%] */ test('it simplifies divisible sequential plans inside non-divisible sequential plans', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -724,8 +759,9 @@ test('it simplifies divisible sequential plans inside non-divisible sequential p * └── [C: 100%] └── [Tx: C] */ test('it does not simplify non-divisible sequential plans inside divisible sequential plans', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(100)); const instructionB = instruction(txPercent(100)); @@ -762,8 +798,9 @@ test('it does not simplify non-divisible sequential plans inside divisible seque * └── [D: 25%] */ test('it can merge non-divisible sequential plans in a parallel plan if the whole sequential plan fits', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(25)); const instructionB = instruction(txPercent(25)); @@ -803,8 +840,9 @@ test('it can merge non-divisible sequential plans in a parallel plan if the whol * └── [D: 33%] */ test('it does not split a non-divisible sequential plan on a parallel parent', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -842,8 +880,9 @@ test('it does not split a non-divisible sequential plan on a parallel parent', a * └── [D: 25%] */ test('it can merge non-divisible sequential plans in a sequential plan if the whole plan fits', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(25)); const instructionB = instruction(txPercent(25)); @@ -883,8 +922,9 @@ test('it can merge non-divisible sequential plans in a sequential plan if the wh * └── [D: 33%] */ test('it does not split a non-divisible sequential plan on a sequential parent', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(33)); const instructionB = instruction(txPercent(33)); @@ -922,8 +962,9 @@ test('it does not split a non-divisible sequential plan on a sequential parent', * └── [D: 100%] */ test('it plans non-divisible sequentials plans with parallel children', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -964,8 +1005,9 @@ test('it plans non-divisible sequentials plans with parallel children', async (t * └── [D: 100%] */ test('it plans non-divisible sequentials plans with divisible sequential children', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const instructionB = instruction(txPercent(50)); @@ -1001,8 +1043,9 @@ test('it plans non-divisible sequentials plans with divisible sequential childre * └── [Tx: A(3, 50%)] */ test('it iterate over iterable instruction plans', async (t) => { - const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, txPercent, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const iteratorIx = iterator(txPercent(250)); @@ -1023,9 +1066,14 @@ test('it iterate over iterable instruction plans', async (t) => { * └── [B(x, 50%)] */ test('it combines single instruction plans with iterable instruction plans', async (t) => { - const { txPercent, iterator, instruction, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + txPercent, + iterator, + instruction, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(50)); const iteratorB = iterator(txPercent(50)); @@ -1049,8 +1097,9 @@ test('it combines single instruction plans with iterable instruction plans', asy * └── [Tx: A(3, 50%)] */ test('it can handle parallel iterable instruction plans', async (t) => { - const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, txPercent, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const iteratorA = iterator(txPercent(250)); @@ -1072,8 +1121,9 @@ test('it can handle parallel iterable instruction plans', async (t) => { * └── [Tx: A(3, 50%)] */ test('it can handle non-divisible sequential iterable instruction plans', async (t) => { - const { txPercent, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, txPercent, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const iteratorA = iterator(txPercent(250)); @@ -1091,8 +1141,9 @@ 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 { txPercent, iterator, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, txPercent, iterator, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const iteratorA = iterator(txPercent(100)); @@ -1110,9 +1161,14 @@ test('it simplifies iterable instruction plans that fit in a single transaction' * └── [C(x, 125%)] └── [Tx: C(3, 50%)] */ test('it uses iterable instruction plans to fill gaps in parallel candidates', async (t) => { - const { txPercent, instruction, iterator, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + txPercent, + instruction, + iterator, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(75)); const instructionB = instruction(txPercent(50)); @@ -1142,9 +1198,14 @@ test('it uses iterable instruction plans to fill gaps in parallel candidates', a * └── [B: 75%] └── [Tx: A(3, 50%)] */ test('it handles parallel iterable instruction plans last to fill gaps in previous parallel candidates', async (t) => { - const { txPercent, instruction, iterator, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + txPercent, + instruction, + iterator, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const iteratorA = iterator(txPercent(25) + txPercent(50) + txPercent(50)); // 125% const instructionB = instruction(txPercent(75)); @@ -1174,9 +1235,14 @@ test('it handles parallel iterable instruction plans last to fill gaps in previo * └── [C: 50%] */ test('it uses iterable instruction plans to fill gaps in sequential candidates', async (t) => { - const { txPercent, instruction, iterator, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + txPercent, + instruction, + iterator, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(75)); const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% @@ -1205,9 +1271,14 @@ test('it uses iterable instruction plans to fill gaps in sequential candidates', * └── [C: 50%] */ test('it uses iterable instruction plans to fill gaps in non-divisible sequential candidates', async (t) => { - const { txPercent, instruction, iterator, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + txPercent, + instruction, + iterator, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(75)); const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% @@ -1237,9 +1308,14 @@ test('it uses iterable instruction plans to fill gaps in non-divisible sequentia * └── [C: 50%] */ test('it uses parallel iterable instruction plans to fill gaps in sequential candidates', async (t) => { - const { txPercent, instruction, iterator, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + txPercent, + instruction, + iterator, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(75)); const iteratorB = iterator(txPercent(25) + txPercent(50)); // 75% @@ -1271,9 +1347,14 @@ test('it uses parallel iterable instruction plans to fill gaps in sequential can * └── [C: 25%] */ test('it uses the whole sequential iterable instruction plan when it fits in the parent parallel candidate', async (t) => { - const { txPercent, instruction, iterator, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + txPercent, + instruction, + iterator, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(25)); const iteratorB = iterator(txPercent(50)); @@ -1306,9 +1387,14 @@ test('it uses the whole sequential iterable instruction plan when it fits in the * └── [C: 25%] */ test('it uses the whole non-divisible sequential iterable instruction plan when it fits in the parent sequential candidate', async (t) => { - const { txPercent, instruction, iterator, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + txPercent, + instruction, + iterator, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(25)); const iteratorB = iterator(txPercent(50)); @@ -1348,8 +1434,9 @@ test('it uses the whole non-divisible sequential iterable instruction plan when * └── [G: 25%] */ test('complex example 1', async (t) => { - const { instruction, txPercent, singleTransactionPlan } = defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { createPlanner, instruction, txPercent, singleTransactionPlan } = + defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(40)); const instructionB = instruction(txPercent(40)); @@ -1408,9 +1495,14 @@ test('complex example 1', async (t) => { * └── [G: 50%] */ test('complex example 2', async (t) => { - const { instruction, iterator, txPercent, singleTransactionPlan } = - defaultFactories(); - const planner = createBaseTransactionPlannerFactory({ version: 0 })(); + const { + createPlanner, + instruction, + iterator, + txPercent, + singleTransactionPlan, + } = defaultFactories(); + const planner = createPlanner(); const instructionA = instruction(txPercent(20)); const instructionB = instruction(txPercent(20)); From ba39a091a3eff8d81144a408efacc5bd54735b95 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 09:55:11 +0100 Subject: [PATCH 070/112] wip --- .../instructionPlansDraft/instructionPlan.ts | 10 +++--- .../transactionHelpers.ts | 33 ++----------------- .../_instructionPlanHelpers.ts | 10 +++--- 3 files changed, 14 insertions(+), 39 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index c423bab..f2ff508 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -1,6 +1,6 @@ import { appendTransactionMessageInstruction, - BaseTransactionMessage, + CompilableTransactionMessage, IInstruction, } from '@solana/kit'; import { @@ -48,7 +48,9 @@ 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: BaseTransactionMessage) => TInstruction | null; + next: ( + transactionMessage: CompilableTransactionMessage + ) => TInstruction | null; }>; export function getLinearIterableInstructionPlan({ @@ -65,7 +67,7 @@ export function getLinearIterableInstructionPlan({ let offset = 0; return { hasNext: () => offset < totalBytes, - next: (tx: BaseTransactionMessage) => { + next: (tx: CompilableTransactionMessage) => { const baseTransactionSize = getTransactionSize( appendTransactionMessageInstruction(getInstruction(offset, 0), tx) ); @@ -98,7 +100,7 @@ export function getIterableInstructionPlanFromInstructions< let instructionIndex = 0; return { hasNext: () => instructionIndex < instructions.length, - next: (tx: BaseTransactionMessage) => { + next: (tx: CompilableTransactionMessage) => { if (instructionIndex >= instructions.length) { return null; } diff --git a/clients/js/src/instructionPlansDraft/transactionHelpers.ts b/clients/js/src/instructionPlansDraft/transactionHelpers.ts index d6d1ae3..fb1bf08 100644 --- a/clients/js/src/instructionPlansDraft/transactionHelpers.ts +++ b/clients/js/src/instructionPlansDraft/transactionHelpers.ts @@ -3,17 +3,9 @@ */ import { - Address, - BaseTransactionMessage, - Blockhash, CompilableTransactionMessage, compileTransaction, getTransactionEncoder, - ITransactionMessageWithFeePayer, - pipe, - setTransactionMessageFeePayer, - setTransactionMessageLifetimeUsingBlockhash, - TransactionMessageWithBlockhashLifetime, } from '@solana/kit'; export const TRANSACTION_PACKET_SIZE = 1280; @@ -25,31 +17,12 @@ export const TRANSACTION_PACKET_HEADER = export const TRANSACTION_SIZE_LIMIT = TRANSACTION_PACKET_SIZE - TRANSACTION_PACKET_HEADER; -// It should accepts both `Transaction` and `BaseTransactionMessage` instances. +// It should accepts both `Transaction` and `CompilableTransactionMessage` instances. // Over time, efforts should be made to improve the performance of this function. // E.g. maybe we don't need to compile the transaction message to get the size. export function getTransactionSize( - message: BaseTransactionMessage & Partial + message: CompilableTransactionMessage ): number { - const mockFeePayer = - 'Gm1uVH3JxiLgafByNNmnoxLncB7ytpyWNqX3kRM9tSxN' as Address; - const mockBlockhash = { - blockhash: '2WCjwT4P5tJF7tjMtTVEnN6o53bcZ8MhszcfXMERtU3z' as Blockhash, - lastValidBlockHeight: 0n, - }; - const transaction = pipe( - message, - (tx) => { - return tx.feePayer - ? (tx as typeof tx & ITransactionMessageWithFeePayer) - : setTransactionMessageFeePayer(mockFeePayer, tx); - }, - (tx) => { - return tx.lifetimeConstraint - ? (tx as typeof tx & TransactionMessageWithBlockhashLifetime) - : setTransactionMessageLifetimeUsingBlockhash(mockBlockhash, tx); - }, - (tx) => compileTransaction(tx) - ); + const transaction = compileTransaction(message); return getTransactionEncoder().getSizeFromValue(transaction); } diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index 67feb08..8661c48 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -1,7 +1,7 @@ import { Address, appendTransactionMessageInstruction, - BaseTransactionMessage, + CompilableTransactionMessage, fixEncoderSize, getAddressDecoder, getU64Encoder, @@ -66,7 +66,7 @@ export function instructionIteratorFactory() { let offset = 0; return { hasNext: () => offset < totalBytes, - next: (tx: BaseTransactionMessage) => { + next: (tx) => { const baseTransactionSize = getTransactionSize( appendTransactionMessageInstruction(baseInstruction, tx) ); @@ -121,10 +121,10 @@ export function instructionFactory(baseCounter: bigint = 0n) { } export function transactionPercentFactory( - defaultMessage?: () => BaseTransactionMessage + createTransactionMessage?: () => CompilableTransactionMessage ) { - const minimumTransactionSize = defaultMessage - ? getTransactionSize(defaultMessage()) + const minimumTransactionSize = createTransactionMessage + ? getTransactionSize(createTransactionMessage()) : MINIMUM_TRANSACTION_SIZE; return (percent: number) => { return Math.floor( From 6b7252429ca8e4ea7c248dfaa2d9fe51fd922099 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 09:58:31 +0100 Subject: [PATCH 071/112] wip --- .../instructionPlansDraft/transactionPlanExecutorDefault.ts | 5 +++++ .../src/instructionPlansDraft/transactionPlannerDefault.ts | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts create mode 100644 clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts new file mode 100644 index 0000000..de5933f --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts @@ -0,0 +1,5 @@ +import { TransactionPlanExecutor } from './transactionPlanExecutor'; + +export function createDefaultTransactionPlanExecutor(): TransactionPlanExecutor { + throw new Error('Not implemented'); +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts new file mode 100644 index 0000000..2ca9e1e --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts @@ -0,0 +1,5 @@ +import { TransactionPlanner } from './transactionPlanner'; + +export function createDefaultTransactionPlanner(): TransactionPlanner { + throw new Error('Not implemented'); +} From 48f381426fe50ae3964cbc2079a71dbbcec7996c Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 10:05:57 +0100 Subject: [PATCH 072/112] wip --- .../transactionPlannerFactory.ts | 12 ++-- .../transactionPlannerFactoryDecorators.ts | 59 ++++++++----------- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts index f256b11..7c52d71 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts @@ -28,12 +28,14 @@ export type TransactionPlannerFactory = ( ) => TransactionPlanner; export type TransactionPlannerFactoryConfig = { - createTransactionMessage: () => Promise; + createTransactionMessage: () => + | Promise + | CompilableTransactionMessage; newInstructionsTransformer?: < TTransactionMessage extends CompilableTransactionMessage, >( transactionMessage: TTransactionMessage - ) => Promise; + ) => Promise | TTransactionMessage; }; export function createBaseTransactionPlannerFactory(): TransactionPlannerFactory { @@ -43,7 +45,7 @@ export function createBaseTransactionPlannerFactory(): TransactionPlannerFactory ): Promise => { const plan: SingleTransactionPlan = { kind: 'single', - message: await config.createTransactionMessage(), + message: await Promise.resolve(config.createTransactionMessage()), }; if (instructions.length > 0) { await addInstructionsToSingleTransactionPlan(plan, instructions); @@ -60,7 +62,9 @@ export function createBaseTransactionPlannerFactory(): TransactionPlannerFactory plan.message ); if (config?.newInstructionsTransformer) { - message = await config.newInstructionsTransformer(plan.message); + message = await Promise.resolve( + config.newInstructionsTransformer(plan.message) + ); } (plan as Mutable).message = message; }; diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts index 26b8533..4ab9d04 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts @@ -1,7 +1,12 @@ +import { + COMPUTE_BUDGET_PROGRAM_ADDRESS, + ComputeBudgetInstruction, + getSetComputeUnitLimitInstruction, + identifyComputeBudgetInstruction, +} from '@solana-program/compute-budget'; import { Address, appendTransactionMessageInstructions, - BaseTransactionMessage, CompilableTransactionMessage, getComputeUnitEstimateForTransactionMessageFactory, GetLatestBlockhashApi, @@ -17,32 +22,26 @@ import { SimulateTransactionApi, TransactionSigner, } from '@solana/kit'; -import { - COMPUTE_BUDGET_PROGRAM_ADDRESS, - ComputeBudgetInstruction, - getSetComputeUnitLimitInstruction, - identifyComputeBudgetInstruction, -} from '@solana-program/compute-budget'; +import { getTimedCacheFunction, Mutable } from './internal'; import { getAllSingleTransactionPlans, SingleTransactionPlan, TransactionPlan, } from './transactionPlan'; -import { getTimedCacheFunction, Mutable } from './internal'; import { TransactionPlannerFactory } from './transactionPlannerFactory'; function transformTransactionPlannerNewMessage( transformer: ( transactionMessage: TTransactionMessage - ) => Promise, + ) => Promise | TTransactionMessage, plannerFactory: TransactionPlannerFactory ): TransactionPlannerFactory { return (config) => { return plannerFactory({ ...config, createTransactionMessage: async () => { - const tx = await config.createTransactionMessage(); - return await transformer(tx); + const tx = await Promise.resolve(config.createTransactionMessage()); + return await Promise.resolve(transformer(tx)); }, }); }; @@ -64,8 +63,7 @@ export function prependTransactionPlannerInstructions( plannerFactory: TransactionPlannerFactory ): TransactionPlannerFactory { return transformTransactionPlannerNewMessage( - (tx) => - Promise.resolve(prependTransactionMessageInstructions(instructions, tx)), + (tx) => prependTransactionMessageInstructions(instructions, tx), plannerFactory ); } @@ -75,8 +73,7 @@ export function appendTransactionPlannerInstructions( plannerFactory: TransactionPlannerFactory ): TransactionPlannerFactory { return transformTransactionPlannerNewMessage( - (tx) => - Promise.resolve(appendTransactionMessageInstructions(instructions, tx)), + (tx) => appendTransactionMessageInstructions(instructions, tx), plannerFactory ); } @@ -86,13 +83,11 @@ export function setTransactionPlannerFeePayer( plannerFactory: TransactionPlannerFactory ): TransactionPlannerFactory { return transformTransactionPlannerNewMessage( - ( + ( tx: TTransactionMessage ) => - Promise.resolve( - setTransactionMessageFeePayer(feePayer, tx) as TTransactionMessage & - ITransactionMessageWithFeePayer - ), + setTransactionMessageFeePayer(feePayer, tx) as TTransactionMessage & + ITransactionMessageWithFeePayer, plannerFactory ); } @@ -102,15 +97,13 @@ export function setTransactionPlannerFeePayerSigner( plannerFactory: TransactionPlannerFactory ): TransactionPlannerFactory { return transformTransactionPlannerNewMessage( - ( + ( tx: TTransactionMessage ) => - Promise.resolve( - setTransactionMessageFeePayerSigner( - feePayerSigner, - tx - ) as TTransactionMessage & ITransactionMessageWithFeePayerSigner - ), + setTransactionMessageFeePayerSigner( + feePayerSigner, + tx + ) as TTransactionMessage & ITransactionMessageWithFeePayerSigner, plannerFactory ); } @@ -151,14 +144,12 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( const plannerWithComputeBudgetLimits = transformTransactionPlannerNewMessage( (tx) => { if (getComputeUnitLimitInstructionIndex(tx) >= 0) { - return Promise.resolve(tx); + return tx; } - return Promise.resolve( - prependTransactionMessageInstruction( - getSetComputeUnitLimitInstruction({ units: MAX_COMPUTE_UNIT_LIMIT }), - tx - ) + return prependTransactionMessageInstruction( + getSetComputeUnitLimitInstruction({ units: MAX_COMPUTE_UNIT_LIMIT }), + tx ); }, plannerFactory @@ -217,7 +208,7 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( } function getComputeUnitLimitInstructionIndex( - transactionMessage: BaseTransactionMessage + transactionMessage: CompilableTransactionMessage ) { return transactionMessage.instructions.findIndex((ix) => { return ( From 681a5d3d330a66494a3a881832eb8d0d2597fcb0 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 10:31:07 +0100 Subject: [PATCH 073/112] wip --- .../transactionHelpers.ts | 22 +++++++++++++++++++ .../_transactionPlanHelpers.ts | 9 ++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionHelpers.ts b/clients/js/src/instructionPlansDraft/transactionHelpers.ts index fb1bf08..96376d4 100644 --- a/clients/js/src/instructionPlansDraft/transactionHelpers.ts +++ b/clients/js/src/instructionPlansDraft/transactionHelpers.ts @@ -3,9 +3,13 @@ */ import { + BaseTransactionMessage, + Blockhash, CompilableTransactionMessage, compileTransaction, getTransactionEncoder, + setTransactionMessageLifetimeUsingBlockhash, + TransactionMessageWithBlockhashLifetime, } from '@solana/kit'; export const TRANSACTION_PACKET_SIZE = 1280; @@ -26,3 +30,21 @@ export function getTransactionSize( const transaction = compileTransaction(message); return getTransactionEncoder().getSizeFromValue(transaction); } + +const PROVISORY_BLOCKHASH_LIFETIME_CONSTRAINT: TransactionMessageWithBlockhashLifetime['lifetimeConstraint'] = + { + blockhash: '11111111111111111111111111111111' as Blockhash, + lastValidBlockHeight: 0n, + }; + +export function setTransactionMessageLifetimeUsingProvisoryBlockhash< + TTransactionMessage extends BaseTransactionMessage, +>(transactionMessage: TTransactionMessage) { + return setTransactionMessageLifetimeUsingBlockhash( + PROVISORY_BLOCKHASH_LIFETIME_CONSTRAINT, + transactionMessage + ); +} + +// Setting it to zero ensures the transaction fails unless it is properly estimated. +export const PROVISORY_COMPUTE_UNIT_LIMIT = 0n; diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts index c8421a4..2f16712 100644 --- a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts @@ -1,17 +1,16 @@ import { Address, appendTransactionMessageInstructions, - Blockhash, CompilableTransactionMessage, IInstruction, createTransactionMessage as kitCreateTransactionMessage, pipe, setTransactionMessageFeePayer, - setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit'; import { ParallelTransactionPlan, SequentialTransactionPlan, + setTransactionMessageLifetimeUsingProvisoryBlockhash, SingleTransactionPlan, TransactionPlan, } from '../../src'; @@ -36,15 +35,11 @@ export function nonDivisibleSequentialTransactionPlan( const MOCK_FEE_PAYER = 'Gm1uVH3JxiLgafByNNmnoxLncB7ytpyWNqX3kRM9tSxN' as Address; -const MOCK_BLOCKHASH = { - blockhash: '11111111111111111111111111111111' as Blockhash, - lastValidBlockHeight: 0n, -} as const; export const getMockCreateTransactionMessage = () => { return pipe( kitCreateTransactionMessage({ version: 0 }), - (tx) => setTransactionMessageLifetimeUsingBlockhash(MOCK_BLOCKHASH, tx), + setTransactionMessageLifetimeUsingProvisoryBlockhash, (tx) => setTransactionMessageFeePayer(MOCK_FEE_PAYER, tx) ); }; From 201df481637cd9eaaba52480a0d044efd119b44d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 10:44:16 +0100 Subject: [PATCH 074/112] wip --- .../computeBudgetHelpers.ts | 62 +++++++++++++++++++ clients/js/src/instructionPlansDraft/index.ts | 1 + .../transactionHelpers.ts | 3 - .../transactionPlannerFactoryDecorators.ts | 52 +++------------- 4 files changed, 73 insertions(+), 45 deletions(-) create mode 100644 clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts diff --git a/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts b/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts new file mode 100644 index 0000000..8ed4f96 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts @@ -0,0 +1,62 @@ +// TODO: This will need decoupling from `@solana-program/compute-budget` +// when added to `@solana/instruction-plans`. Also, the function +// `getComputeUnitEstimateForTransactionMessageFactory` will need to +// move in a granular package so `instruction-plans` can use it. + +import { + COMPUTE_BUDGET_PROGRAM_ADDRESS, + ComputeBudgetInstruction, + getSetComputeUnitLimitInstruction, + identifyComputeBudgetInstruction, +} from '@solana-program/compute-budget'; +import { + BaseTransactionMessage, + prependTransactionMessageInstruction, +} from '@solana/kit'; + +// Setting it to zero ensures the transaction fails unless it is properly estimated. +export const PROVISORY_COMPUTE_UNIT_LIMIT = 0; + +export function updateOrPrependProvisorySetComputeUnitLimitInstruction< + TTransactionMessage extends BaseTransactionMessage, +>(transactionMessage: TTransactionMessage) { + return updateOrPrependSetComputeUnitLimitInstruction( + PROVISORY_COMPUTE_UNIT_LIMIT, + transactionMessage + ); +} + +export function updateOrPrependSetComputeUnitLimitInstruction< + TTransactionMessage extends BaseTransactionMessage, +>(units: number, transactionMessage: TTransactionMessage): TTransactionMessage { + const instructionIndex = + getComputeUnitLimitInstructionIndex(transactionMessage); + + if (instructionIndex === -1) { + return prependTransactionMessageInstruction( + getSetComputeUnitLimitInstruction({ units }), + transactionMessage + ); + } + + return { + ...transactionMessage, + instructions: [ + ...transactionMessage.instructions.slice(0, instructionIndex), + getSetComputeUnitLimitInstruction({ units }), + ...transactionMessage.instructions.slice(instructionIndex + 1), + ], + }; +} + +export function getComputeUnitLimitInstructionIndex( + transactionMessage: BaseTransactionMessage +) { + return transactionMessage.instructions.findIndex((ix) => { + return ( + ix.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS && + identifyComputeBudgetInstruction(ix.data as Uint8Array) === + ComputeBudgetInstruction.SetComputeUnitLimit + ); + }); +} diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts index 12a9d76..02e72d7 100644 --- a/clients/js/src/instructionPlansDraft/index.ts +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -1,3 +1,4 @@ +export * from './computeBudgetHelpers'; export * from './instructionPlan'; export * from './transactionHelpers'; export * from './transactionPlan'; diff --git a/clients/js/src/instructionPlansDraft/transactionHelpers.ts b/clients/js/src/instructionPlansDraft/transactionHelpers.ts index 96376d4..602b8a8 100644 --- a/clients/js/src/instructionPlansDraft/transactionHelpers.ts +++ b/clients/js/src/instructionPlansDraft/transactionHelpers.ts @@ -45,6 +45,3 @@ export function setTransactionMessageLifetimeUsingProvisoryBlockhash< transactionMessage ); } - -// Setting it to zero ensures the transaction fails unless it is properly estimated. -export const PROVISORY_COMPUTE_UNIT_LIMIT = 0n; diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts index 4ab9d04..e2c4d03 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts @@ -1,9 +1,4 @@ -import { - COMPUTE_BUDGET_PROGRAM_ADDRESS, - ComputeBudgetInstruction, - getSetComputeUnitLimitInstruction, - identifyComputeBudgetInstruction, -} from '@solana-program/compute-budget'; +import { getSetComputeUnitLimitInstruction } from '@solana-program/compute-budget'; import { Address, appendTransactionMessageInstructions, @@ -22,6 +17,10 @@ import { SimulateTransactionApi, TransactionSigner, } from '@solana/kit'; +import { + getComputeUnitLimitInstructionIndex, + updateOrPrependSetComputeUnitLimitInstruction, +} from './computeBudgetHelpers'; import { getTimedCacheFunction, Mutable } from './internal'; import { getAllSingleTransactionPlans, @@ -160,32 +159,13 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( const promises = getAllSingleTransactionPlans(plan).map( async (singlePlan) => { const computeUnitsEstimate = await estimateComputeUnitLimit( - singlePlan.message as CompilableTransactionMessage - ); - const instructionIndex = getComputeUnitLimitInstructionIndex( singlePlan.message ); - const newMessage: CompilableTransactionMessage = - instructionIndex === -1 - ? prependTransactionMessageInstruction( - getSetComputeUnitLimitInstruction({ - units: computeUnitsEstimate, - }), - singlePlan.message - ) - : ({ - ...singlePlan.message, - instructions: [ - ...singlePlan.message.instructions.slice(0, instructionIndex), - getSetComputeUnitLimitInstruction({ - units: computeUnitsEstimate, - }), - ...singlePlan.message.instructions.slice( - instructionIndex + 1 - ), - ], - } as CompilableTransactionMessage); - (singlePlan as Mutable).message = newMessage; + (singlePlan as Mutable).message = + updateOrPrependSetComputeUnitLimitInstruction( + computeUnitsEstimate, + singlePlan.message + ); } ); @@ -206,15 +186,3 @@ export function estimateAndSetComputeUnitLimitForTransactionPlanner( return plan; }, plannerWithComputeBudgetLimits); } - -function getComputeUnitLimitInstructionIndex( - transactionMessage: CompilableTransactionMessage -) { - return transactionMessage.instructions.findIndex((ix) => { - return ( - ix.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS && - identifyComputeBudgetInstruction(ix.data as Uint8Array) === - ComputeBudgetInstruction.SetComputeUnitLimit - ); - }); -} From 8aacc6eb8532f8c8bf68748049a9a980e5176388 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 12:17:39 +0100 Subject: [PATCH 075/112] wip --- .../transactionPlanExecutorFactoryDecorators.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts index aba1cf0..3fd908a 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts @@ -3,8 +3,7 @@ import { Rpc, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit'; -import { getTimedCacheFunction, Mutable } from './internal'; -import { SingleTransactionPlan } from './transactionPlan'; +import { getTimedCacheFunction } from './internal'; import { TransactionPlanExecutorFactory, TransactionPlanExecutorFactoryConfig, @@ -47,12 +46,13 @@ export function refreshBlockhashForTransactionPlanExecutor( return await next(transactionPlan); } - (transactionPlan as Mutable).message = - setTransactionMessageLifetimeUsingBlockhash( + return await next({ + ...transactionPlan, + message: setTransactionMessageLifetimeUsingBlockhash( await getBlockhash(), transactionPlan.message - ); - return await next(transactionPlan); + ), + }); }, executorFactory ); From ece79945a01a86f215c04e9954bf1d7ff84ef4cc Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 12:21:09 +0100 Subject: [PATCH 076/112] Remove estimateAndSetComputeUnitLimitForTransactionPlanner --- .../transactionPlannerFactoryDecorators.ts | 89 +------------------ 1 file changed, 1 insertion(+), 88 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts index e2c4d03..9f82370 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts @@ -1,32 +1,19 @@ -import { getSetComputeUnitLimitInstruction } from '@solana-program/compute-budget'; import { Address, appendTransactionMessageInstructions, CompilableTransactionMessage, - getComputeUnitEstimateForTransactionMessageFactory, GetLatestBlockhashApi, IInstruction, ITransactionMessageWithFeePayer, ITransactionMessageWithFeePayerSigner, - prependTransactionMessageInstruction, prependTransactionMessageInstructions, Rpc, setTransactionMessageFeePayer, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, - SimulateTransactionApi, TransactionSigner, } from '@solana/kit'; -import { - getComputeUnitLimitInstructionIndex, - updateOrPrependSetComputeUnitLimitInstruction, -} from './computeBudgetHelpers'; -import { getTimedCacheFunction, Mutable } from './internal'; -import { - getAllSingleTransactionPlans, - SingleTransactionPlan, - TransactionPlan, -} from './transactionPlan'; +import { getTimedCacheFunction } from './internal'; import { TransactionPlannerFactory } from './transactionPlannerFactory'; function transformTransactionPlannerNewMessage( @@ -46,17 +33,6 @@ function transformTransactionPlannerNewMessage( }; } -function transformTransactionPlan( - transformer: (transactionPlan: TransactionPlan) => Promise, - plannerFactory: TransactionPlannerFactory -): TransactionPlannerFactory { - return (config) => { - const planner = plannerFactory(config); - return async (instructionPlan) => - await transformer(await planner(instructionPlan)); - }; -} - export function prependTransactionPlannerInstructions( instructions: IInstruction[], plannerFactory: TransactionPlannerFactory @@ -123,66 +99,3 @@ export function setTransactionPlannerLifetimeUsingLatestBlockhash( plannerFactory ); } - -const MAX_COMPUTE_UNIT_LIMIT = 1_400_000; - -// TODO: This will need decoupling from `@solana-program/compute-budget` -// when added to `@solana/instruction-plans`. Also, the function -// `getComputeUnitEstimateForTransactionMessageFactory` will need to -// move in a granular package so `instruction-plans` can use it. -export function estimateAndSetComputeUnitLimitForTransactionPlanner( - rpc: Rpc, - plannerFactory: TransactionPlannerFactory, - chunkSize: number | null = 10 -): TransactionPlannerFactory { - // Create a function to estimate the compute unit limit for a transaction. - const estimateComputeUnitLimit = - getComputeUnitEstimateForTransactionMessageFactory({ rpc }); - - // Add a compute unit limit instruction to the transaction if it doesn't exist. - const plannerWithComputeBudgetLimits = transformTransactionPlannerNewMessage( - (tx) => { - if (getComputeUnitLimitInstructionIndex(tx) >= 0) { - return tx; - } - - return prependTransactionMessageInstruction( - getSetComputeUnitLimitInstruction({ units: MAX_COMPUTE_UNIT_LIMIT }), - tx - ); - }, - plannerFactory - ); - - // Transform the final transaction plan to set the correct compute unit limit. - return transformTransactionPlan(async (plan) => { - const promises = getAllSingleTransactionPlans(plan).map( - async (singlePlan) => { - const computeUnitsEstimate = await estimateComputeUnitLimit( - singlePlan.message - ); - (singlePlan as Mutable).message = - updateOrPrependSetComputeUnitLimitInstruction( - computeUnitsEstimate, - singlePlan.message - ); - } - ); - - // Chunk promises to avoid rate limiting. - const chunkedPromises = []; - if (!chunkSize) { - chunkedPromises.push(promises); - } else { - for (let i = 0; i < promises.length; i += chunkSize) { - const chunk = promises.slice(i, i + chunkSize); - chunkedPromises.push(chunk); - } - } - for (const chunk of chunkedPromises) { - await Promise.all(chunk); - } - - return plan; - }, plannerWithComputeBudgetLimits); -} From c6fcd4287a6faebbf33f7ee81b2aa1481e1410a0 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 12:35:01 +0100 Subject: [PATCH 077/112] wip --- .../transactionPlannerDefault.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts index 2ca9e1e..9d90c1b 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts @@ -1,5 +1,24 @@ +import { + createTransactionMessage as kitCreateTransactionMessage, + pipe, + setTransactionMessageFeePayerSigner, + TransactionSigner, +} from '@solana/kit'; import { TransactionPlanner } from './transactionPlanner'; +import { createBaseTransactionPlannerFactory } from './transactionPlannerFactory'; +import { setTransactionMessageLifetimeUsingProvisoryBlockhash } from './transactionHelpers'; +import { updateOrPrependProvisorySetComputeUnitLimitInstruction } from './computeBudgetHelpers'; -export function createDefaultTransactionPlanner(): TransactionPlanner { - throw new Error('Not implemented'); +export function createDefaultTransactionPlanner( + feePayer: TransactionSigner +): TransactionPlanner { + return createBaseTransactionPlannerFactory()({ + createTransactionMessage: () => + pipe( + kitCreateTransactionMessage({ version: 0 }), + setTransactionMessageLifetimeUsingProvisoryBlockhash, + updateOrPrependProvisorySetComputeUnitLimitInstruction, + (tx) => setTransactionMessageFeePayerSigner(feePayer, tx) + ), + }); } From 2750a49c7218abc8970eb63284f285c9d206bfbc Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 12:51:06 +0100 Subject: [PATCH 078/112] wip --- .../computeBudgetHelpers.ts | 65 +++++++++++++------ .../transactionPlannerDefault.ts | 4 +- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts b/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts index 8ed4f96..317822e 100644 --- a/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts +++ b/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts @@ -1,6 +1,6 @@ -// TODO: This will need decoupling from `@solana-program/compute-budget` -// when added to `@solana/instruction-plans`. Also, the function -// `getComputeUnitEstimateForTransactionMessageFactory` will need to +// TODO: This will need decoupling from `@solana-program/compute-budget` when added to `@solana/instruction-plans` + +// TODO: The function `getComputeUnitEstimateForTransactionMessageFactory` will need to // move in a granular package so `instruction-plans` can use it. import { @@ -10,46 +10,60 @@ import { identifyComputeBudgetInstruction, } from '@solana-program/compute-budget'; import { + appendTransactionMessageInstruction, BaseTransactionMessage, - prependTransactionMessageInstruction, + getU32Decoder, + IInstruction, + offsetDecoder, } from '@solana/kit'; // Setting it to zero ensures the transaction fails unless it is properly estimated. export const PROVISORY_COMPUTE_UNIT_LIMIT = 0; -export function updateOrPrependProvisorySetComputeUnitLimitInstruction< +// This is the maximum compute unit limit that can be set for a transaction. +export const MAX_COMPUTE_UNIT_LIMIT = 1_400_000; + +export function fillProvisorySetComputeUnitLimitInstruction< TTransactionMessage extends BaseTransactionMessage, >(transactionMessage: TTransactionMessage) { - return updateOrPrependSetComputeUnitLimitInstruction( - PROVISORY_COMPUTE_UNIT_LIMIT, + return updateOrAppendSetComputeUnitLimitInstruction( + (previousUnits) => + previousUnits === null ? PROVISORY_COMPUTE_UNIT_LIMIT : previousUnits, transactionMessage ); } -export function updateOrPrependSetComputeUnitLimitInstruction< +export function updateOrAppendSetComputeUnitLimitInstruction< TTransactionMessage extends BaseTransactionMessage, ->(units: number, transactionMessage: TTransactionMessage): TTransactionMessage { +>( + getUnits: (previousUnits: number | null) => number, + transactionMessage: TTransactionMessage +): TTransactionMessage { const instructionIndex = - getComputeUnitLimitInstructionIndex(transactionMessage); + getSetComputeUnitLimitInstructionIndex(transactionMessage); if (instructionIndex === -1) { - return prependTransactionMessageInstruction( - getSetComputeUnitLimitInstruction({ units }), + return appendTransactionMessageInstruction( + getSetComputeUnitLimitInstruction({ units: getUnits(null) }), transactionMessage ); } - return { - ...transactionMessage, - instructions: [ - ...transactionMessage.instructions.slice(0, instructionIndex), - getSetComputeUnitLimitInstruction({ units }), - ...transactionMessage.instructions.slice(instructionIndex + 1), - ], - }; + const previousUnits = getUnitsFromSetComputeUnitLimitInstruction( + transactionMessage.instructions[instructionIndex] + ); + const units = getUnits(previousUnits); + if (units === previousUnits) { + return transactionMessage; + } + + const nextInstruction = getSetComputeUnitLimitInstruction({ units }); + const nextInstructions = [...transactionMessage.instructions]; + nextInstructions.splice(instructionIndex, 1, nextInstruction); + return { ...transactionMessage, instructions: nextInstructions }; } -export function getComputeUnitLimitInstructionIndex( +export function getSetComputeUnitLimitInstructionIndex( transactionMessage: BaseTransactionMessage ) { return transactionMessage.instructions.findIndex((ix) => { @@ -60,3 +74,12 @@ export function getComputeUnitLimitInstructionIndex( ); }); } + +export function getUnitsFromSetComputeUnitLimitInstruction( + instruction: IInstruction +) { + const unitsDecoder = offsetDecoder(getU32Decoder(), { + preOffset: ({ preOffset }) => preOffset + 1, + }); + return unitsDecoder.decode(instruction.data as Uint8Array); +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts index 9d90c1b..08393d4 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts @@ -7,7 +7,7 @@ import { import { TransactionPlanner } from './transactionPlanner'; import { createBaseTransactionPlannerFactory } from './transactionPlannerFactory'; import { setTransactionMessageLifetimeUsingProvisoryBlockhash } from './transactionHelpers'; -import { updateOrPrependProvisorySetComputeUnitLimitInstruction } from './computeBudgetHelpers'; +import { fillProvisorySetComputeUnitLimitInstruction } from './computeBudgetHelpers'; export function createDefaultTransactionPlanner( feePayer: TransactionSigner @@ -17,7 +17,7 @@ export function createDefaultTransactionPlanner( pipe( kitCreateTransactionMessage({ version: 0 }), setTransactionMessageLifetimeUsingProvisoryBlockhash, - updateOrPrependProvisorySetComputeUnitLimitInstruction, + fillProvisorySetComputeUnitLimitInstruction, (tx) => setTransactionMessageFeePayerSigner(feePayer, tx) ), }); From 7215483ec03ce24d7ccfbb7c216f7fe5e704f40a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 14:42:19 +0100 Subject: [PATCH 079/112] wip --- .../computeBudgetHelpers.ts | 72 +++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts b/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts index 317822e..28b6ec0 100644 --- a/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts +++ b/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts @@ -12,9 +12,15 @@ import { import { appendTransactionMessageInstruction, BaseTransactionMessage, + CompilableTransactionMessage, + getComputeUnitEstimateForTransactionMessageFactory, getU32Decoder, IInstruction, + ITransactionMessageWithFeePayer, offsetDecoder, + Rpc, + SimulateTransactionApi, + TransactionMessage, } from '@solana/kit'; // Setting it to zero ensures the transaction fails unless it is properly estimated. @@ -33,25 +39,54 @@ export function fillProvisorySetComputeUnitLimitInstruction< ); } -export function updateOrAppendSetComputeUnitLimitInstruction< +export async function estimateAndUpdateProvisorySetComputeUnitLimitInstruction( + rpc: Rpc, + transactionMessage: + | CompilableTransactionMessage + | (ITransactionMessageWithFeePayer & TransactionMessage) +) { + const getComputeUnitEstimateForTransactionMessage = + getComputeUnitEstimateForTransactionMessageFactory({ rpc }); + + const instructionDetails = + getSetComputeUnitLimitInstructionIndexAndUnits(transactionMessage); + + // If the transaction message already has a compute unit limit instruction + // which is set to a specific value — i.e. not 0 or the maximum limit — + // we don't need to estimate the compute unit limit. + if ( + instructionDetails && + instructionDetails.units !== PROVISORY_COMPUTE_UNIT_LIMIT && + instructionDetails.units !== MAX_COMPUTE_UNIT_LIMIT + ) { + return transactionMessage; + } + + const units = + await getComputeUnitEstimateForTransactionMessage(transactionMessage); + return updateOrAppendSetComputeUnitLimitInstruction( + () => units, + transactionMessage + ); +} + +function updateOrAppendSetComputeUnitLimitInstruction< TTransactionMessage extends BaseTransactionMessage, >( getUnits: (previousUnits: number | null) => number, transactionMessage: TTransactionMessage ): TTransactionMessage { - const instructionIndex = - getSetComputeUnitLimitInstructionIndex(transactionMessage); + const instructionDetails = + getSetComputeUnitLimitInstructionIndexAndUnits(transactionMessage); - if (instructionIndex === -1) { + if (!instructionDetails) { return appendTransactionMessageInstruction( getSetComputeUnitLimitInstruction({ units: getUnits(null) }), transactionMessage ); } - const previousUnits = getUnitsFromSetComputeUnitLimitInstruction( - transactionMessage.instructions[instructionIndex] - ); + const { index, units: previousUnits } = instructionDetails; const units = getUnits(previousUnits); if (units === previousUnits) { return transactionMessage; @@ -59,11 +94,26 @@ export function updateOrAppendSetComputeUnitLimitInstruction< const nextInstruction = getSetComputeUnitLimitInstruction({ units }); const nextInstructions = [...transactionMessage.instructions]; - nextInstructions.splice(instructionIndex, 1, nextInstruction); + nextInstructions.splice(index, 1, nextInstruction); return { ...transactionMessage, instructions: nextInstructions }; } -export function getSetComputeUnitLimitInstructionIndex( +function getSetComputeUnitLimitInstructionIndexAndUnits( + transactionMessage: BaseTransactionMessage +): { index: number; units: number } | null { + const index = getSetComputeUnitLimitInstructionIndex(transactionMessage); + if (index < 0) { + return null; + } + + const units = getSetComputeUnitLimitInstructionUnits( + transactionMessage.instructions[index] + ); + + return { index, units }; +} + +function getSetComputeUnitLimitInstructionIndex( transactionMessage: BaseTransactionMessage ) { return transactionMessage.instructions.findIndex((ix) => { @@ -75,9 +125,7 @@ export function getSetComputeUnitLimitInstructionIndex( }); } -export function getUnitsFromSetComputeUnitLimitInstruction( - instruction: IInstruction -) { +function getSetComputeUnitLimitInstructionUnits(instruction: IInstruction) { const unitsDecoder = offsetDecoder(getU32Decoder(), { preOffset: ({ preOffset }) => preOffset + 1, }); From d60e04a5f2572390200478b618df13a02029e010 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 14:45:59 +0100 Subject: [PATCH 080/112] Remove transactionPlannerFactoryDecorators --- clients/js/src/instructionPlansDraft/index.ts | 1 - .../transactionPlannerFactoryDecorators.ts | 101 ------------------ 2 files changed, 102 deletions(-) delete mode 100644 clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts index 02e72d7..81ccc73 100644 --- a/clients/js/src/instructionPlansDraft/index.ts +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -4,4 +4,3 @@ export * from './transactionHelpers'; export * from './transactionPlan'; export * from './transactionPlanner'; export * from './transactionPlannerFactory'; -export * from './transactionPlannerFactoryDecorators'; diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts deleted file mode 100644 index 9f82370..0000000 --- a/clients/js/src/instructionPlansDraft/transactionPlannerFactoryDecorators.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - Address, - appendTransactionMessageInstructions, - CompilableTransactionMessage, - GetLatestBlockhashApi, - IInstruction, - ITransactionMessageWithFeePayer, - ITransactionMessageWithFeePayerSigner, - prependTransactionMessageInstructions, - Rpc, - setTransactionMessageFeePayer, - setTransactionMessageFeePayerSigner, - setTransactionMessageLifetimeUsingBlockhash, - TransactionSigner, -} from '@solana/kit'; -import { getTimedCacheFunction } from './internal'; -import { TransactionPlannerFactory } from './transactionPlannerFactory'; - -function transformTransactionPlannerNewMessage( - transformer: ( - transactionMessage: TTransactionMessage - ) => Promise | TTransactionMessage, - plannerFactory: TransactionPlannerFactory -): TransactionPlannerFactory { - return (config) => { - return plannerFactory({ - ...config, - createTransactionMessage: async () => { - const tx = await Promise.resolve(config.createTransactionMessage()); - return await Promise.resolve(transformer(tx)); - }, - }); - }; -} - -export function prependTransactionPlannerInstructions( - instructions: IInstruction[], - plannerFactory: TransactionPlannerFactory -): TransactionPlannerFactory { - return transformTransactionPlannerNewMessage( - (tx) => prependTransactionMessageInstructions(instructions, tx), - plannerFactory - ); -} - -export function appendTransactionPlannerInstructions( - instructions: IInstruction[], - plannerFactory: TransactionPlannerFactory -): TransactionPlannerFactory { - return transformTransactionPlannerNewMessage( - (tx) => appendTransactionMessageInstructions(instructions, tx), - plannerFactory - ); -} - -export function setTransactionPlannerFeePayer( - feePayer: Address, - plannerFactory: TransactionPlannerFactory -): TransactionPlannerFactory { - return transformTransactionPlannerNewMessage( - ( - tx: TTransactionMessage - ) => - setTransactionMessageFeePayer(feePayer, tx) as TTransactionMessage & - ITransactionMessageWithFeePayer, - plannerFactory - ); -} - -export function setTransactionPlannerFeePayerSigner( - feePayerSigner: TransactionSigner, - plannerFactory: TransactionPlannerFactory -): TransactionPlannerFactory { - return transformTransactionPlannerNewMessage( - ( - tx: TTransactionMessage - ) => - setTransactionMessageFeePayerSigner( - feePayerSigner, - tx - ) as TTransactionMessage & ITransactionMessageWithFeePayerSigner, - plannerFactory - ); -} - -export function setTransactionPlannerLifetimeUsingLatestBlockhash( - rpc: Rpc, - plannerFactory: TransactionPlannerFactory -): TransactionPlannerFactory { - // Cache the latest blockhash for 60 seconds. - const getBlockhash = getTimedCacheFunction(async () => { - const { value } = await rpc.getLatestBlockhash().send(); - return value; - }, 60_000); - - return transformTransactionPlannerNewMessage( - async (tx) => - setTransactionMessageLifetimeUsingBlockhash(await getBlockhash(), tx), - plannerFactory - ); -} From 797fbd1b01e4db136257a25af35fa526c6eb36c7 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 14:53:41 +0100 Subject: [PATCH 081/112] Remove TransactionPlannerFactory --- clients/js/src/instructionPlansDraft/index.ts | 3 +- ...erFactory.ts => transactionPlannerBase.ts} | 85 +++++++++---------- .../transactionPlannerDefault.ts | 4 +- .../transactionPlanner.test.ts | 7 +- 4 files changed, 48 insertions(+), 51 deletions(-) rename clients/js/src/instructionPlansDraft/{transactionPlannerFactory.ts => transactionPlannerBase.ts} (84%) diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts index 81ccc73..9b14a60 100644 --- a/clients/js/src/instructionPlansDraft/index.ts +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -3,4 +3,5 @@ export * from './instructionPlan'; export * from './transactionHelpers'; export * from './transactionPlan'; export * from './transactionPlanner'; -export * from './transactionPlannerFactory'; +export * from './transactionPlannerBase'; +export * from './transactionPlannerDefault'; diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts b/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts similarity index 84% rename from clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts rename to clients/js/src/instructionPlansDraft/transactionPlannerBase.ts index 7c52d71..61ecba3 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerFactory.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts @@ -23,11 +23,8 @@ import { } from './transactionPlan'; import { TransactionPlanner } from './transactionPlanner'; -export type TransactionPlannerFactory = ( - configs: TransactionPlannerFactoryConfig -) => TransactionPlanner; - -export type TransactionPlannerFactoryConfig = { +export type TransactionPlannerConfig = { + abortSignal?: AbortSignal; // TODO createTransactionMessage: () => | Promise | CompilableTransactionMessage; @@ -38,51 +35,51 @@ export type TransactionPlannerFactoryConfig = { ) => Promise | TTransactionMessage; }; -export function createBaseTransactionPlannerFactory(): TransactionPlannerFactory { - return (config) => { - const createSingleTransactionPlan = async ( - instructions: IInstruction[] = [] - ): Promise => { - const plan: SingleTransactionPlan = { - kind: 'single', - message: await Promise.resolve(config.createTransactionMessage()), - }; - if (instructions.length > 0) { - await addInstructionsToSingleTransactionPlan(plan, instructions); - } - return plan; +export function createBaseTransactionPlanner( + config: TransactionPlannerConfig +): TransactionPlanner { + const createSingleTransactionPlan = async ( + instructions: IInstruction[] = [] + ): Promise => { + const plan: SingleTransactionPlan = { + kind: 'single', + message: await Promise.resolve(config.createTransactionMessage()), }; + if (instructions.length > 0) { + await addInstructionsToSingleTransactionPlan(plan, instructions); + } + return plan; + }; - const addInstructionsToSingleTransactionPlan = async ( - plan: SingleTransactionPlan, - instructions: IInstruction[] - ): Promise => { - let message = appendTransactionMessageInstructions( - instructions, - plan.message + const addInstructionsToSingleTransactionPlan = async ( + plan: SingleTransactionPlan, + instructions: IInstruction[] + ): Promise => { + let message = appendTransactionMessageInstructions( + instructions, + plan.message + ); + if (config?.newInstructionsTransformer) { + message = await Promise.resolve( + config.newInstructionsTransformer(plan.message) ); - if (config?.newInstructionsTransformer) { - message = await Promise.resolve( - config.newInstructionsTransformer(plan.message) - ); - } - (plan as Mutable).message = message; - }; + } + (plan as Mutable).message = message; + }; - return async (originalInstructionPlan): Promise => { - const plan = await traverse(originalInstructionPlan, { - parent: null, - parentCandidates: [], - createSingleTransactionPlan, - addInstructionsToSingleTransactionPlan, - }); + return async (originalInstructionPlan): Promise => { + const plan = await traverse(originalInstructionPlan, { + parent: null, + parentCandidates: [], + createSingleTransactionPlan, + addInstructionsToSingleTransactionPlan, + }); - if (!plan) { - throw new Error('No instructions were found in the instruction plan.'); - } + if (!plan) { + throw new Error('No instructions were found in the instruction plan.'); + } - return plan; - }; + return plan; }; } diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts index 08393d4..07199ea 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts @@ -5,14 +5,14 @@ import { TransactionSigner, } from '@solana/kit'; import { TransactionPlanner } from './transactionPlanner'; -import { createBaseTransactionPlannerFactory } from './transactionPlannerFactory'; +import { createBaseTransactionPlanner } from './transactionPlannerBase'; import { setTransactionMessageLifetimeUsingProvisoryBlockhash } from './transactionHelpers'; import { fillProvisorySetComputeUnitLimitInstruction } from './computeBudgetHelpers'; export function createDefaultTransactionPlanner( feePayer: TransactionSigner ): TransactionPlanner { - return createBaseTransactionPlannerFactory()({ + return createBaseTransactionPlanner({ createTransactionMessage: () => pipe( kitCreateTransactionMessage({ version: 0 }), diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 468a3a2..8419beb 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -15,7 +15,7 @@ import { sequentialTransactionPlan, singleTransactionPlanFactory, } from './_transactionPlanHelpers'; -import { createBaseTransactionPlannerFactory } from '../../src'; +import { createBaseTransactionPlanner } from '../../src'; import { CompilableTransactionMessage } from '@solana/kit'; function defaultFactories( @@ -25,9 +25,8 @@ function defaultFactories( createTransactionMessage ?? getMockCreateTransactionMessage; return { createPlanner: () => - createBaseTransactionPlannerFactory()({ - createTransactionMessage: () => - Promise.resolve(effectiveCreateTransactionMessage()), + createBaseTransactionPlanner({ + createTransactionMessage: effectiveCreateTransactionMessage, }), instruction: instructionFactory(), iterator: instructionIteratorFactory(), From e59df5a30cf88530456dd78acb69ca66856911f0 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 14:58:38 +0100 Subject: [PATCH 082/112] wip --- .../js/src/instructionPlansDraft/transactionPlannerDefault.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts index 07199ea..690b5d3 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts @@ -1,5 +1,5 @@ import { - createTransactionMessage as kitCreateTransactionMessage, + createTransactionMessage, pipe, setTransactionMessageFeePayerSigner, TransactionSigner, @@ -15,7 +15,7 @@ export function createDefaultTransactionPlanner( return createBaseTransactionPlanner({ createTransactionMessage: () => pipe( - kitCreateTransactionMessage({ version: 0 }), + createTransactionMessage({ version: 0 }), setTransactionMessageLifetimeUsingProvisoryBlockhash, fillProvisorySetComputeUnitLimitInstruction, (tx) => setTransactionMessageFeePayerSigner(feePayer, tx) From ffc2cd4886ef6f3edd2d2f55ffe87e0788ccc40d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 15:17:16 +0100 Subject: [PATCH 083/112] wip --- .../transactionPlanExecutor.ts | 3 +- .../transactionPlanExecutorFactory.ts | 32 +++++++++++-------- .../transactionPlanner.ts | 3 +- .../transactionPlannerBase.ts | 1 - 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index ab0aaff..fe7b6d1 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -2,5 +2,6 @@ import { TransactionPlan } from './transactionPlan'; import { TransactionPlanResult } from './transactionPlanResult'; export type TransactionPlanExecutor = ( - transactionPlan: TransactionPlan + transactionPlan: TransactionPlan, + config?: { abortSignal?: AbortSignal } // TODO: Use ) => Promise>; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts index 360fb8d..d64a1d9 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts @@ -61,19 +61,25 @@ async function traverse( transactionPlan: TransactionPlan, context: TraverseContext ): Promise { - switch (transactionPlan.kind) { - case 'sequential': - return await traverseSequential(transactionPlan, context); - case 'parallel': - return await traverseParallel(transactionPlan, context); - case 'single': - return await traverseSingle(transactionPlan, context); - default: - transactionPlan satisfies never; - throw new Error( - `Unknown instruction plan kind: ${(transactionPlan as { kind: string }).kind}` - ); - } + const next = async ( + nextTransactionPlan: TransactionPlan + ): Promise => { + switch (nextTransactionPlan.kind) { + case 'sequential': + return await traverseSequential(nextTransactionPlan, context); + case 'parallel': + return await traverseParallel(nextTransactionPlan, context); + case 'single': + return await traverseSingle(nextTransactionPlan, context); + default: + nextTransactionPlan satisfies never; + throw new Error( + `Unknown instruction plan kind: ${(nextTransactionPlan as { kind: string }).kind}` + ); + } + }; + + return await context.wrapInTransformer(transactionPlan, next); } async function traverseSequential( diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlansDraft/transactionPlanner.ts index 44a4dd4..4dedaf0 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanner.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanner.ts @@ -2,5 +2,6 @@ import { InstructionPlan } from './instructionPlan'; import { TransactionPlan } from './transactionPlan'; export type TransactionPlanner = ( - instructionPlan: InstructionPlan + instructionPlan: InstructionPlan, + config?: { abortSignal?: AbortSignal } // TODO: Use ) => Promise; diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts b/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts index 61ecba3..3c59aa5 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts @@ -24,7 +24,6 @@ import { import { TransactionPlanner } from './transactionPlanner'; export type TransactionPlannerConfig = { - abortSignal?: AbortSignal; // TODO createTransactionMessage: () => | Promise | CompilableTransactionMessage; From 3877e0591dd532e03f5029c61dd8e256e446b60a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 15:55:58 +0100 Subject: [PATCH 084/112] wip --- .../transactionPlanExecutor.ts | 2 +- .../transactionPlanExecutorFactory.ts | 6 ++--- .../transactionPlanResult.ts | 26 +++++++++---------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index fe7b6d1..8d3e7d1 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -1,7 +1,7 @@ import { TransactionPlan } from './transactionPlan'; import { TransactionPlanResult } from './transactionPlanResult'; -export type TransactionPlanExecutor = ( +export type TransactionPlanExecutor = ( transactionPlan: TransactionPlan, config?: { abortSignal?: AbortSignal } // TODO: Use ) => Promise>; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts index d64a1d9..3fa6a15 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts @@ -21,9 +21,7 @@ export type TransactionPlanExecutorFactoryConfig = { transformer?: TransactionPlanExecutorFactoryTransformer; }; -export type TransactionPlanExecutorFactory< - TContext extends object | null = null, -> = ( +export type TransactionPlanExecutorFactory = ( config?: TransactionPlanExecutorFactoryConfig ) => TransactionPlanExecutor; @@ -119,7 +117,7 @@ async function traverseSingle( return { kind: 'single', - context: null, + context: {}, message: transactionPlan.message, signature: getSignatureFromTransaction(transaction), status: { kind: 'success' }, diff --git a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts index a0e0342..97ec1d1 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts @@ -1,26 +1,24 @@ import { BaseTransactionMessage, Signature, SolanaError } from '@solana/kit'; -export type TransactionPlanResult = +export type TransactionPlanResult = | SequentialTransactionPlanResult | ParallelTransactionPlanResult | SingleTransactionPlanResult; -export type SequentialTransactionPlanResult< - TContext extends object | null = null, -> = Readonly<{ - kind: 'sequential'; - plans: TransactionPlanResult[]; -}>; +export type SequentialTransactionPlanResult = + Readonly<{ + kind: 'sequential'; + plans: TransactionPlanResult[]; + }>; -export type ParallelTransactionPlanResult< - TContext extends object | null = null, -> = Readonly<{ - kind: 'parallel'; - plans: TransactionPlanResult[]; -}>; +export type ParallelTransactionPlanResult = + Readonly<{ + kind: 'parallel'; + plans: TransactionPlanResult[]; + }>; export type SingleTransactionPlanResult< - TContext extends object | null = null, + TContext extends object = object, TTransactionMessage extends BaseTransactionMessage = BaseTransactionMessage, > = Readonly<{ context: TContext; From 7064e5c6a5e1d6e901f8a77b456ecfc7bbc7d4e8 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 9 Apr 2025 16:51:30 +0100 Subject: [PATCH 085/112] Simplify TransactionPlanExecutor by removing factories --- .../transactionPlanExecutorBase.ts | 108 +++++++++++++++ .../transactionPlanExecutorDecorators.ts | 56 ++++++++ .../transactionPlanExecutorFactory.ts | 125 ------------------ ...ransactionPlanExecutorFactoryDecorators.ts | 91 ------------- .../transactionPlanResult.ts | 20 +-- 5 files changed, 176 insertions(+), 224 deletions(-) create mode 100644 clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts create mode 100644 clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts delete mode 100644 clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts delete mode 100644 clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts new file mode 100644 index 0000000..dbe951c --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts @@ -0,0 +1,108 @@ +import { + CompilableTransactionMessage, + SolanaError, + Transaction, +} from '@solana/kit'; +import { + ParallelTransactionPlan, + SequentialTransactionPlan, + SingleTransactionPlan, + TransactionPlan, +} from './transactionPlan'; +import { TransactionPlanResult } from './transactionPlanResult'; +import { TransactionPlanExecutor } from './transactionPlanExecutor'; + +export type TransactionPlanExecutorSendAndConfirm = < + TTransactionMessage extends CompilableTransactionMessage, + TContext extends object = object, +>( + transactionMessage: TTransactionMessage +) => Promise<{ context?: TContext; transaction: Transaction }>; + +export function createBaseTransactionPlanExecutor( + sendAndConfirm: TransactionPlanExecutorSendAndConfirm +): TransactionPlanExecutor { + return async (plan): Promise => { + const context: TraverseContext = { sendAndConfirm }; + return await traverse(plan, context); // TODO: Throw error unless everything is successful. + }; +} + +type TraverseContext = { + sendAndConfirm: TransactionPlanExecutorSendAndConfirm; +}; + +async function traverse( + transactionPlan: TransactionPlan, + context: TraverseContext +): Promise { + switch (transactionPlan.kind) { + case 'sequential': + return await traverseSequential(transactionPlan, context); + case 'parallel': + return await traverseParallel(transactionPlan, context); + case 'single': + return await traverseSingle(transactionPlan, context); + default: + transactionPlan satisfies never; + throw new Error( + `Unknown instruction plan kind: ${(transactionPlan as { kind: string }).kind}` + ); + } +} + +async function traverseSequential( + transactionPlan: SequentialTransactionPlan, + context: TraverseContext +): Promise { + const results: TransactionPlanResult[] = []; + for (const subPlan of transactionPlan.plans) { + const result = await traverse(subPlan, context); + results.push(result); + + // TODO: Handle cancellations. + } + return { kind: 'sequential', plans: results }; +} + +async function traverseParallel( + transactionPlan: ParallelTransactionPlan, + context: TraverseContext +): Promise { + const results = await Promise.all( + transactionPlan.plans.map((subPlan) => traverse(subPlan, context)) + ); + + // TODO: Handle chunking. + + return { kind: 'parallel', plans: results }; +} + +async function traverseSingle( + transactionPlan: SingleTransactionPlan, + traverseContext: TraverseContext +): Promise { + try { + const result = await traverseContext.sendAndConfirm( + transactionPlan.message + ); + + // TODO: Handle error. + + return { + kind: 'single', + message: transactionPlan.message, + status: { + kind: 'success', + transaction: result.transaction, + context: result.context ?? {}, + }, + }; + } catch (error) { + return { + kind: 'single', + message: transactionPlan.message, + status: { kind: 'error', error: error as SolanaError }, + }; + } +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts new file mode 100644 index 0000000..d2aff65 --- /dev/null +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -0,0 +1,56 @@ +import { + GetLatestBlockhashApi, + Rpc, + setTransactionMessageLifetimeUsingBlockhash, +} from '@solana/kit'; +import { getTimedCacheFunction } from './internal'; +import { TransactionPlanExecutorSendAndConfirm } from './transactionPlanExecutorBase'; + +// TODO: implement +// - Chunk parallel transactions (Needs special transformer) +// - Add support for custom + +export function refreshBlockhashForTransactionPlanExecutor( + rpc: Rpc, + sendAndConfirm: TransactionPlanExecutorSendAndConfirm +): TransactionPlanExecutorSendAndConfirm { + // Cache the latest blockhash for 60 seconds. + const getBlockhash = getTimedCacheFunction(async () => { + const { value } = await rpc.getLatestBlockhash().send(); + return value; + }, 60_000); + + return async (transactionMessage) => { + return await sendAndConfirm( + setTransactionMessageLifetimeUsingBlockhash( + await getBlockhash(), + transactionMessage + ) + ); + }; +} + +export function retryTransactionPlanExecutor( + maxRetries: number, + sendAndConfirm: TransactionPlanExecutorSendAndConfirm +): TransactionPlanExecutorSendAndConfirm { + return async (transactionMessage) => { + let retries = 0; + let lastError: Error | null = null; + + // x retries means x+1 attempts. + while (retries < maxRetries + 1) { + try { + return await sendAndConfirm(transactionMessage); + } catch (error) { + // TODO: Should we not retry on certain error codes? + retries++; + lastError = error as Error; + } + } + + throw lastError; + // TODO: Catch and return failed results instead of failing. + // TODO: Fail whilst returning the result in the context at the very end. + }; +} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts deleted file mode 100644 index 3fa6a15..0000000 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactory.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - BaseTransactionMessage, - getSignatureFromTransaction, - Transaction, -} from '@solana/kit'; -import { - ParallelTransactionPlan, - SequentialTransactionPlan, - SingleTransactionPlan, - TransactionPlan, -} from './transactionPlan'; -import { TransactionPlanResult } from './transactionPlanResult'; -import { TransactionPlanExecutor } from './transactionPlanExecutor'; - -type TransactionPlanExecutorFactoryTransformer = ( - transactionPlan: TransactionPlan, - next: (transactionPlan: TransactionPlan) => Promise -) => Promise; - -export type TransactionPlanExecutorFactoryConfig = { - transformer?: TransactionPlanExecutorFactoryTransformer; -}; - -export type TransactionPlanExecutorFactory = ( - config?: TransactionPlanExecutorFactoryConfig -) => TransactionPlanExecutor; - -export function createBaseTransactionPlanExecutorFactory( - sendAndConfirm: ( - transactionMessage: TTransactionMessage - ) => Promise -): TransactionPlanExecutorFactory { - return (config) => { - const wrapInTransformer: TransactionPlanExecutorFactoryTransformer = ( - transactionPlan, - next - ) => { - if (config?.transformer) { - return config.transformer(transactionPlan, next); - } - return next(transactionPlan); - }; - - return async (plan): Promise => { - const context: TraverseContext = { sendAndConfirm, wrapInTransformer }; - return await traverse(plan, context); - }; - }; -} - -type TraverseContext = { - sendAndConfirm: ( - transactionMessage: TTransactionMessage - ) => Promise; - wrapInTransformer: TransactionPlanExecutorFactoryTransformer; -}; - -async function traverse( - transactionPlan: TransactionPlan, - context: TraverseContext -): Promise { - const next = async ( - nextTransactionPlan: TransactionPlan - ): Promise => { - switch (nextTransactionPlan.kind) { - case 'sequential': - return await traverseSequential(nextTransactionPlan, context); - case 'parallel': - return await traverseParallel(nextTransactionPlan, context); - case 'single': - return await traverseSingle(nextTransactionPlan, context); - default: - nextTransactionPlan satisfies never; - throw new Error( - `Unknown instruction plan kind: ${(nextTransactionPlan as { kind: string }).kind}` - ); - } - }; - - return await context.wrapInTransformer(transactionPlan, next); -} - -async function traverseSequential( - transactionPlan: SequentialTransactionPlan, - context: TraverseContext -): Promise { - const results: TransactionPlanResult[] = []; - for (const subPlan of transactionPlan.plans) { - const result = await traverse(subPlan, context); - results.push(result); - - // TODO: Handle cancellations. - } - return { kind: 'sequential', plans: results }; -} - -async function traverseParallel( - transactionPlan: ParallelTransactionPlan, - context: TraverseContext -): Promise { - const results = await Promise.all( - transactionPlan.plans.map((subPlan) => traverse(subPlan, context)) - ); - - // TODO: Handle chunking via decorators. - - return { kind: 'parallel', plans: results }; -} - -async function traverseSingle( - transactionPlan: SingleTransactionPlan, - context: TraverseContext -): Promise { - const transaction = await context.sendAndConfirm(transactionPlan.message); - - // TODO: Handle error. - - return { - kind: 'single', - context: {}, - message: transactionPlan.message, - signature: getSignatureFromTransaction(transaction), - status: { kind: 'success' }, - }; -} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts deleted file mode 100644 index 3fd908a..0000000 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorFactoryDecorators.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - GetLatestBlockhashApi, - Rpc, - setTransactionMessageLifetimeUsingBlockhash, -} from '@solana/kit'; -import { getTimedCacheFunction } from './internal'; -import { - TransactionPlanExecutorFactory, - TransactionPlanExecutorFactoryConfig, -} from './transactionPlanExecutorFactory'; - -// TODO: implement -// - Chunk parallel transactions (Needs special transformer) -// - Add support for custom - -export function transformTransactionPlanExecutorFactory( - transformer: Required['transformer'], - executorFactory: TransactionPlanExecutorFactory -): TransactionPlanExecutorFactory { - return (config) => { - return executorFactory({ - ...config, - transformer: (transactionPlan, next) => { - const newNext: typeof next = (plan) => transformer(plan, next); - return config?.transformer - ? config.transformer(transactionPlan, newNext) - : newNext(transactionPlan); - }, - }); - }; -} - -export function refreshBlockhashForTransactionPlanExecutor( - rpc: Rpc, - executorFactory: TransactionPlanExecutorFactory -): TransactionPlanExecutorFactory { - // Cache the latest blockhash for 60 seconds. - const getBlockhash = getTimedCacheFunction(async () => { - const { value } = await rpc.getLatestBlockhash().send(); - return value; - }, 60_000); - - return transformTransactionPlanExecutorFactory( - async (transactionPlan, next) => { - if (transactionPlan.kind !== 'single') { - return await next(transactionPlan); - } - - return await next({ - ...transactionPlan, - message: setTransactionMessageLifetimeUsingBlockhash( - await getBlockhash(), - transactionPlan.message - ), - }); - }, - executorFactory - ); -} - -export function retryTransactionPlanExecutor( - maxRetries: number, - executorFactory: TransactionPlanExecutorFactory -): TransactionPlanExecutorFactory { - return transformTransactionPlanExecutorFactory( - async (transactionPlan, next) => { - if (transactionPlan.kind !== 'single') { - return await next(transactionPlan); - } - - let retries = 0; - let lastError: Error | null = null; - - // x retries means x+1 attempts. - while (retries < maxRetries + 1) { - try { - return await next(transactionPlan); - } catch (error) { - // TODO: Should we not retry on certain error codes? - retries++; - lastError = error as Error; - } - } - - throw lastError; - // TODO: Catch and return failed results instead of failing. - // TODO: Have another decorator that fails whilst returning the result in the context at the very end. - }, - executorFactory - ); -} diff --git a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts index 97ec1d1..a9cdacc 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts @@ -1,4 +1,8 @@ -import { BaseTransactionMessage, Signature, SolanaError } from '@solana/kit'; +import { + CompilableTransactionMessage, + SolanaError, + Transaction, +} from '@solana/kit'; export type TransactionPlanResult = | SequentialTransactionPlanResult @@ -19,16 +23,16 @@ export type ParallelTransactionPlanResult = export type SingleTransactionPlanResult< TContext extends object = object, - TTransactionMessage extends BaseTransactionMessage = BaseTransactionMessage, + TTransactionMessage extends + CompilableTransactionMessage = CompilableTransactionMessage, > = Readonly<{ - context: TContext; kind: 'single'; message: TTransactionMessage; - signature: Signature; - status: TransactionPlanResultStatus; + status: TransactionPlanResultStatus; }>; -export type TransactionPlanResultStatus = - | { kind: 'success' } +export type TransactionPlanResultStatus = + | { kind: 'aborted' } + | { kind: 'canceled' } | { kind: 'error'; error: SolanaError } - | { kind: 'canceled' }; + | { kind: 'success'; context: TContext; transaction: Transaction }; From 04cf8bac617f66adc7cac9ff72b83de43ee8a886 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 10:11:40 +0100 Subject: [PATCH 086/112] wip --- .../transactionPlanExecutor.ts | 2 +- .../transactionPlanExecutorBase.ts | 49 +++++++++++++++---- .../transactionPlanExecutorDecorators.ts | 2 - .../transactionPlanResult.ts | 1 - 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts index 8d3e7d1..27a9b77 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts @@ -3,5 +3,5 @@ import { TransactionPlanResult } from './transactionPlanResult'; export type TransactionPlanExecutor = ( transactionPlan: TransactionPlan, - config?: { abortSignal?: AbortSignal } // TODO: Use + config?: { abortSignal?: AbortSignal } ) => Promise>; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts index dbe951c..280e219 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts @@ -16,19 +16,41 @@ export type TransactionPlanExecutorSendAndConfirm = < TTransactionMessage extends CompilableTransactionMessage, TContext extends object = object, >( - transactionMessage: TTransactionMessage + transactionMessage: TTransactionMessage, + config?: { abortSignal?: AbortSignal } ) => Promise<{ context?: TContext; transaction: Transaction }>; export function createBaseTransactionPlanExecutor( sendAndConfirm: TransactionPlanExecutorSendAndConfirm ): TransactionPlanExecutor { - return async (plan): Promise => { - const context: TraverseContext = { sendAndConfirm }; - return await traverse(plan, context); // TODO: Throw error unless everything is successful. + return async (plan, config): Promise => { + const context: TraverseContext = { + abortSignal: config?.abortSignal, + canceled: false, + sendAndConfirm, + }; + + const cancelHandler = () => (context.canceled = true); + config?.abortSignal?.addEventListener('abort', cancelHandler); + const result = await traverse(plan, context); + config?.abortSignal?.removeEventListener('abort', cancelHandler); + + if (context.canceled) { + // TODO: Coded error. + const error = new Error('Transaction plan execution failed') as Error & { + result: TransactionPlanResult; + }; + error.result = result; + throw error; + } + + return result; }; } type TraverseContext = { + abortSignal?: AbortSignal; + canceled: boolean; sendAndConfirm: TransactionPlanExecutorSendAndConfirm; }; @@ -80,14 +102,20 @@ async function traverseParallel( async function traverseSingle( transactionPlan: SingleTransactionPlan, - traverseContext: TraverseContext + context: TraverseContext ): Promise { - try { - const result = await traverseContext.sendAndConfirm( - transactionPlan.message - ); + if (context.canceled) { + return { + kind: 'single', + message: transactionPlan.message, + status: { kind: 'canceled' }, + }; + } - // TODO: Handle error. + try { + const result = await context.sendAndConfirm(transactionPlan.message, { + abortSignal: context.abortSignal, + }); return { kind: 'single', @@ -99,6 +127,7 @@ async function traverseSingle( }, }; } catch (error) { + context.canceled = true; return { kind: 'single', message: transactionPlan.message, diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts index d2aff65..db8482a 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -50,7 +50,5 @@ export function retryTransactionPlanExecutor( } throw lastError; - // TODO: Catch and return failed results instead of failing. - // TODO: Fail whilst returning the result in the context at the very end. }; } diff --git a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts index a9cdacc..2b831fb 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts @@ -32,7 +32,6 @@ export type SingleTransactionPlanResult< }>; export type TransactionPlanResultStatus = - | { kind: 'aborted' } | { kind: 'canceled' } | { kind: 'error'; error: SolanaError } | { kind: 'success'; context: TContext; transaction: Transaction }; From 1d04269ac8d646b5b35579ce12f250b306e7f794 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 11:04:40 +0100 Subject: [PATCH 087/112] wip --- .../transactionPlanExecutorBase.ts | 48 ++++++-- .../transactionPlanExecutorDecorators.ts | 4 - .../transactionPlanExecutorDefault.ts | 115 +++++++++++++++++- 3 files changed, 151 insertions(+), 16 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts index 280e219..b8b34e3 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts @@ -20,14 +20,19 @@ export type TransactionPlanExecutorSendAndConfirm = < config?: { abortSignal?: AbortSignal } ) => Promise<{ context?: TContext; transaction: Transaction }>; +type TransactionPlanExecutorConfig = { + parallelChunkSize?: number; + sendAndConfirm: TransactionPlanExecutorSendAndConfirm; +}; + export function createBaseTransactionPlanExecutor( - sendAndConfirm: TransactionPlanExecutorSendAndConfirm + executorConfig: TransactionPlanExecutorConfig ): TransactionPlanExecutor { return async (plan, config): Promise => { const context: TraverseContext = { + ...executorConfig, abortSignal: config?.abortSignal, canceled: false, - sendAndConfirm, }; const cancelHandler = () => (context.canceled = true); @@ -48,10 +53,9 @@ export function createBaseTransactionPlanExecutor( }; } -type TraverseContext = { +type TraverseContext = TransactionPlanExecutorConfig & { abortSignal?: AbortSignal; canceled: boolean; - sendAndConfirm: TransactionPlanExecutorSendAndConfirm; }; async function traverse( @@ -81,8 +85,6 @@ async function traverseSequential( for (const subPlan of transactionPlan.plans) { const result = await traverse(subPlan, context); results.push(result); - - // TODO: Handle cancellations. } return { kind: 'sequential', plans: results }; } @@ -91,15 +93,41 @@ async function traverseParallel( transactionPlan: ParallelTransactionPlan, context: TraverseContext ): Promise { - const results = await Promise.all( - transactionPlan.plans.map((subPlan) => traverse(subPlan, context)) - ); + const chunks = chunkPlans(transactionPlan.plans, context.parallelChunkSize); + const results: TransactionPlanResult[] = []; - // TODO: Handle chunking. + for (const chunk of chunks) { + const chunkResults = await Promise.all( + chunk.map((plan) => traverse(plan, context)) + ); + results.push(...chunkResults); + } return { kind: 'parallel', plans: results }; } +function chunkPlans( + plans: TransactionPlan[], + chunkSize?: number +): TransactionPlan[][] { + if (!chunkSize) { + return [plans]; + } + + return plans.reduce( + (chunks, subPlan) => { + const lastChunk = chunks[chunks.length - 1]; + if (lastChunk && lastChunk.length < chunkSize) { + lastChunk.push(subPlan); + } else { + chunks.push([subPlan]); + } + return chunks; + }, + [[]] as TransactionPlan[][] + ); +} + async function traverseSingle( transactionPlan: SingleTransactionPlan, context: TraverseContext diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts index db8482a..f118cf3 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -6,10 +6,6 @@ import { import { getTimedCacheFunction } from './internal'; import { TransactionPlanExecutorSendAndConfirm } from './transactionPlanExecutorBase'; -// TODO: implement -// - Chunk parallel transactions (Needs special transformer) -// - Add support for custom - export function refreshBlockhashForTransactionPlanExecutor( rpc: Rpc, sendAndConfirm: TransactionPlanExecutorSendAndConfirm diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts index de5933f..d1562b4 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts @@ -1,5 +1,116 @@ +import { + AccountNotificationsApi, + Commitment, + compileTransaction, + FullySignedTransaction, + GetAccountInfoApi, + GetEpochInfoApi, + GetSignatureStatusesApi, + isTransactionMessageWithSingleSendingSigner, + Rpc, + RpcSubscriptions, + sendAndConfirmDurableNonceTransactionFactory, + sendAndConfirmTransactionFactory, + SendTransactionApi, + signAndSendTransactionMessageWithSigners, + SignatureNotificationsApi, + signTransactionMessageWithSigners, + SlotNotificationsApi, + TransactionWithBlockhashLifetime, + TransactionWithDurableNonceLifetime, + TransactionWithLifetime, +} from '@solana/kit'; import { TransactionPlanExecutor } from './transactionPlanExecutor'; +import { + createBaseTransactionPlanExecutor, + TransactionPlanExecutorSendAndConfirm, +} from './transactionPlanExecutorBase'; -export function createDefaultTransactionPlanExecutor(): TransactionPlanExecutor { - throw new Error('Not implemented'); +export function createDefaultTransactionPlanExecutor( + config: SendAndConfirmTransactionFactoryConfig & { + commitment?: Commitment; + parallelChunkSize?: number; + } +): TransactionPlanExecutor { + return createBaseTransactionPlanExecutor({ + parallelChunkSize: config.parallelChunkSize, + sendAndConfirm: getDefaultTransactionPlanExecutorSendAndConfirm({ + ...config, + commitment: config.commitment ?? 'confirmed', + }), + }); +} + +function getDefaultTransactionPlanExecutorSendAndConfirm( + config: SendAndConfirmTransactionFactoryConfig & { commitment: Commitment } +): TransactionPlanExecutorSendAndConfirm { + const sendAndConfirm = sendAndConfirmTransactionFactoryWithAnyLifetime({ + rpc: config.rpc, + rpcSubscriptions: config.rpcSubscriptions, + }); + return async (transactionMessage, executorConfig) => { + if (isTransactionMessageWithSingleSendingSigner(transactionMessage)) { + await signAndSendTransactionMessageWithSigners(transactionMessage, { + abortSignal: executorConfig?.abortSignal, + }); + return { transaction: compileTransaction(transactionMessage) }; + } + + const transaction = + await signTransactionMessageWithSigners(transactionMessage); + await sendAndConfirm(transaction, { + abortSignal: executorConfig?.abortSignal, + commitment: config.commitment, + }); + + return { transaction }; + }; +} + +type SendAndConfirmTransactionFactoryConfig = { + rpc: Rpc< + GetEpochInfoApi & + GetSignatureStatusesApi & + SendTransactionApi & + GetAccountInfoApi + >; + rpcSubscriptions: RpcSubscriptions< + SignatureNotificationsApi & SlotNotificationsApi & AccountNotificationsApi + >; +}; + +type SendAndConfirmTransactionFunction = ( + transaction: FullySignedTransaction & TransactionWithLifetime, + config: { abortSignal?: AbortSignal; commitment: Commitment } +) => Promise; + +function sendAndConfirmTransactionFactoryWithAnyLifetime( + factoryConfig: SendAndConfirmTransactionFactoryConfig +): SendAndConfirmTransactionFunction { + const sendAndConfirmWithBlockhash = + sendAndConfirmTransactionFactory(factoryConfig); + const sendAndConfirmWithDurableNonce = + sendAndConfirmDurableNonceTransactionFactory(factoryConfig); + + return async (transaction, config) => { + if (!isBlockhashTransaction(transaction)) { + return await sendAndConfirmWithDurableNonce( + transaction as typeof transaction & TransactionWithDurableNonceLifetime, + config + ); + } + return await sendAndConfirmWithBlockhash(transaction, config); + }; +} + +function isBlockhashTransaction( + transaction: TransactionWithLifetime +): transaction is TransactionWithBlockhashLifetime { + return ( + 'lifetimeConstraint' in transaction && + 'blockhash' in transaction.lifetimeConstraint && + 'lastValidBlockHeight' in transaction.lifetimeConstraint && + typeof transaction.lifetimeConstraint.blockhash === 'string' && + typeof transaction.lifetimeConstraint.lastValidBlockHeight === 'bigint' + ); } From 6c776ae9efc2cb82ef7611ac72a1268f78d65004 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 11:29:00 +0100 Subject: [PATCH 088/112] wip --- clients/js/src/instructionPlansDraft/index.ts | 2 + .../transactionPlanExecutorBase.ts | 10 ++- .../transactionPlanResult.ts | 5 +- .../_transactionPlanHelpers.ts | 2 +- .../_transactionPlanResultHelpers.ts | 63 +++++++++++++++++++ .../transactionPlanExecutor.test.ts | 45 +++++++++++++ 6 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts create mode 100644 clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts index 9b14a60..5fea370 100644 --- a/clients/js/src/instructionPlansDraft/index.ts +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -2,6 +2,8 @@ export * from './computeBudgetHelpers'; export * from './instructionPlan'; export * from './transactionHelpers'; export * from './transactionPlan'; +export * from './transactionPlanExecutor'; +export * from './transactionPlanExecutorBase'; export * from './transactionPlanner'; export * from './transactionPlannerBase'; export * from './transactionPlannerDefault'; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts index b8b34e3..df4cfac 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts @@ -86,7 +86,11 @@ async function traverseSequential( const result = await traverse(subPlan, context); results.push(result); } - return { kind: 'sequential', plans: results }; + return { + kind: 'sequential', + divisible: transactionPlan.divisible, + plans: results, + }; } async function traverseParallel( @@ -149,7 +153,7 @@ async function traverseSingle( kind: 'single', message: transactionPlan.message, status: { - kind: 'success', + kind: 'successful', transaction: result.transaction, context: result.context ?? {}, }, @@ -159,7 +163,7 @@ async function traverseSingle( return { kind: 'single', message: transactionPlan.message, - status: { kind: 'error', error: error as SolanaError }, + status: { kind: 'failed', error: error as SolanaError }, }; } } diff --git a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts index 2b831fb..618c441 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanResult.ts @@ -12,6 +12,7 @@ export type TransactionPlanResult = export type SequentialTransactionPlanResult = Readonly<{ kind: 'sequential'; + divisible: boolean; plans: TransactionPlanResult[]; }>; @@ -33,5 +34,5 @@ export type SingleTransactionPlanResult< export type TransactionPlanResultStatus = | { kind: 'canceled' } - | { kind: 'error'; error: SolanaError } - | { kind: 'success'; context: TContext; transaction: Transaction }; + | { kind: 'failed'; error: SolanaError } + | { kind: 'successful'; context: TContext; transaction: Transaction }; diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts index 2f16712..4a37cdf 100644 --- a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts @@ -47,7 +47,7 @@ export const getMockCreateTransactionMessage = () => { export function singleTransactionPlanFactory( createTransactionMessage?: () => CompilableTransactionMessage ) { - return (instructions: IInstruction[]): SingleTransactionPlan => { + return (instructions: IInstruction[] = []): SingleTransactionPlan => { return { kind: 'single', message: appendTransactionMessageInstructions( diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts b/clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts new file mode 100644 index 0000000..a4fd30b --- /dev/null +++ b/clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts @@ -0,0 +1,63 @@ +import { compileTransaction, SolanaError, Transaction } from '@solana/kit'; +import { SingleTransactionPlan } from '../../src'; +import { + ParallelTransactionPlanResult, + SequentialTransactionPlanResult, + SingleTransactionPlanResult, + TransactionPlanResult, +} from '../../src/instructionPlansDraft/transactionPlanResult'; + +export function parallelTransactionPlanResult( + plans: TransactionPlanResult[] +): ParallelTransactionPlanResult { + return { kind: 'parallel', plans }; +} + +export function sequentialTransactionPlanResult( + plans: TransactionPlanResult[] +): SequentialTransactionPlanResult { + return { kind: 'sequential', divisible: true, plans }; +} + +export function nonDivisibleSequentialTransactionPlanResult( + plans: TransactionPlanResult[] +): SequentialTransactionPlanResult { + return { kind: 'sequential', divisible: false, plans }; +} + +export function successfulSingleTransactionPlan( + plan: SingleTransactionPlan, + transaction?: Transaction, + context?: object +): SingleTransactionPlanResult { + return { + kind: 'single', + message: plan.message, + status: { + kind: 'successful', + transaction: transaction ?? compileTransaction(plan.message), + context: context ?? {}, + }, + }; +} + +export function failedSingleTransactionPlan( + plan: SingleTransactionPlan, + error: SolanaError +): SingleTransactionPlanResult { + return { + kind: 'single', + message: plan.message, + status: { kind: 'failed', error }, + }; +} + +export function canceledSingleTransactionPlan( + plan: SingleTransactionPlan +): SingleTransactionPlanResult { + return { + kind: 'single', + message: plan.message, + status: { kind: 'canceled' }, + }; +} diff --git a/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts new file mode 100644 index 0000000..429f13e --- /dev/null +++ b/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts @@ -0,0 +1,45 @@ +import test from 'ava'; +import { singleTransactionPlanFactory } from './_transactionPlanHelpers'; +import { createBaseTransactionPlanExecutor } from '../../src'; +import { compileTransaction, SolanaError } from '@solana/kit'; +import { + failedSingleTransactionPlan, + successfulSingleTransactionPlan, +} from './_transactionPlanResultHelpers'; +import { TransactionPlanResult } from '../../src/instructionPlansDraft/transactionPlanResult'; + +function getMockSolanaError(): SolanaError { + return {} as SolanaError; +} + +test('it handles successful single transactions', async (t) => { + const singleTransactionPlan = singleTransactionPlanFactory(); + const plan = singleTransactionPlan(); + + const executor = createBaseTransactionPlanExecutor({ + sendAndConfirm: (tx) => + Promise.resolve({ transaction: compileTransaction(tx) }), + }); + + t.deepEqual(await executor(plan), successfulSingleTransactionPlan(plan)); +}); + +test('it handles failed single transactions', async (t) => { + const singleTransactionPlan = singleTransactionPlanFactory(); + const plan = singleTransactionPlan(); + + const planError = getMockSolanaError(); + const executor = createBaseTransactionPlanExecutor({ + sendAndConfirm: () => Promise.reject(planError), + }); + + const promise = executor(plan); + const executorError = (await t.throwsAsync(promise)) as Error & { + result: TransactionPlanResult; + }; + + t.deepEqual( + executorError.result, + failedSingleTransactionPlan(plan, planError) + ); +}); From 3f0d08008abcf0cc57980ff2b5947e17328516cf Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 12:15:37 +0100 Subject: [PATCH 089/112] wip --- clients/js/src/instructionPlansDraft/index.ts | 1 + .../transactionPlanExecutorBase.ts | 6 +- .../_transactionPlanResultHelpers.ts | 4 +- .../transactionPlanExecutor.test.ts | 77 ++++++++++++++++--- 4 files changed, 74 insertions(+), 14 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts index 5fea370..79a87cd 100644 --- a/clients/js/src/instructionPlansDraft/index.ts +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -7,3 +7,4 @@ export * from './transactionPlanExecutorBase'; export * from './transactionPlanner'; export * from './transactionPlannerBase'; export * from './transactionPlannerDefault'; +export * from './transactionPlanResult'; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts index df4cfac..d8068e6 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts @@ -32,10 +32,12 @@ export function createBaseTransactionPlanExecutor( const context: TraverseContext = { ...executorConfig, abortSignal: config?.abortSignal, - canceled: false, + canceled: config?.abortSignal?.aborted ?? false, }; - const cancelHandler = () => (context.canceled = true); + const cancelHandler = () => { + context.canceled = true; + }; config?.abortSignal?.addEventListener('abort', cancelHandler); const result = await traverse(plan, context); config?.abortSignal?.removeEventListener('abort', cancelHandler); diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts b/clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts index a4fd30b..83429cd 100644 --- a/clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts @@ -1,11 +1,11 @@ import { compileTransaction, SolanaError, Transaction } from '@solana/kit'; -import { SingleTransactionPlan } from '../../src'; import { ParallelTransactionPlanResult, SequentialTransactionPlanResult, + SingleTransactionPlan, SingleTransactionPlanResult, TransactionPlanResult, -} from '../../src/instructionPlansDraft/transactionPlanResult'; +} from '../../src'; export function parallelTransactionPlanResult( plans: TransactionPlanResult[] diff --git a/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts index 429f13e..9ad58b0 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts @@ -1,17 +1,34 @@ -import test from 'ava'; -import { singleTransactionPlanFactory } from './_transactionPlanHelpers'; -import { createBaseTransactionPlanExecutor } from '../../src'; import { compileTransaction, SolanaError } from '@solana/kit'; +import test, { Assertions } from 'ava'; import { + createBaseTransactionPlanExecutor, + TransactionPlanResult, +} from '../../src'; +import { + sequentialTransactionPlan, + singleTransactionPlanFactory, +} from './_transactionPlanHelpers'; +import { + canceledSingleTransactionPlan, failedSingleTransactionPlan, + sequentialTransactionPlanResult, successfulSingleTransactionPlan, } from './_transactionPlanResultHelpers'; -import { TransactionPlanResult } from '../../src/instructionPlansDraft/transactionPlanResult'; function getMockSolanaError(): SolanaError { return {} as SolanaError; } +async function assertFailedResult( + t: Assertions, + promise: Promise +): Promise { + const executorError = (await t.throwsAsync(promise)) as Error & { + result: TransactionPlanResult; + }; + return executorError.result; +} + test('it handles successful single transactions', async (t) => { const singleTransactionPlan = singleTransactionPlanFactory(); const plan = singleTransactionPlan(); @@ -33,13 +50,53 @@ test('it handles failed single transactions', async (t) => { sendAndConfirm: () => Promise.reject(planError), }); - const promise = executor(plan); - const executorError = (await t.throwsAsync(promise)) as Error & { - result: TransactionPlanResult; - }; + const result = await assertFailedResult(t, executor(plan)); + + t.deepEqual(result, failedSingleTransactionPlan(plan, planError)); +}); + +test('it handles aborted single transactions', async (t) => { + const singleTransactionPlan = singleTransactionPlanFactory(); + const plan = singleTransactionPlan(); + + const executor = createBaseTransactionPlanExecutor({ + sendAndConfirm: (tx) => + Promise.resolve({ transaction: compileTransaction(tx) }), + }); + + const abortController = new AbortController(); + abortController.abort(); + const promise = executor(plan, { abortSignal: abortController.signal }); + + const result = await assertFailedResult(t, promise); + + t.deepEqual(result, canceledSingleTransactionPlan(plan)); +}); + +test('it cancels transactions after a failed one in a sequential plan', async (t) => { + const singleTransactionPlan = singleTransactionPlanFactory(); + + const planA = singleTransactionPlan(); + const planB = singleTransactionPlan(); + const planC = singleTransactionPlan(); + + const planBError = getMockSolanaError(); + const executor = createBaseTransactionPlanExecutor({ + sendAndConfirm: (tx) => + tx === planB.message + ? Promise.reject(planBError) + : Promise.resolve({ transaction: compileTransaction(tx) }), + }); + + const promise = executor(sequentialTransactionPlan([planA, planB, planC])); + const result = await assertFailedResult(t, promise); t.deepEqual( - executorError.result, - failedSingleTransactionPlan(plan, planError) + result, + sequentialTransactionPlanResult([ + successfulSingleTransactionPlan(planA), + failedSingleTransactionPlan(planB, planBError), + canceledSingleTransactionPlan(planC), + ]) ); }); From 1f4cd04b6b88825db1d0aadff3483542072bc738 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 12:29:22 +0100 Subject: [PATCH 090/112] wip --- clients/js/src/instructionPlansDraft/index.ts | 2 ++ .../transactionPlanExecutorDefault.ts | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlansDraft/index.ts index 79a87cd..f2eff47 100644 --- a/clients/js/src/instructionPlansDraft/index.ts +++ b/clients/js/src/instructionPlansDraft/index.ts @@ -4,6 +4,8 @@ export * from './transactionHelpers'; export * from './transactionPlan'; export * from './transactionPlanExecutor'; export * from './transactionPlanExecutorBase'; +export * from './transactionPlanExecutorDecorators'; +export * from './transactionPlanExecutorDefault'; export * from './transactionPlanner'; export * from './transactionPlannerBase'; export * from './transactionPlannerDefault'; diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts index d1562b4..ab52b7c 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts @@ -5,8 +5,10 @@ import { FullySignedTransaction, GetAccountInfoApi, GetEpochInfoApi, + GetLatestBlockhashApi, GetSignatureStatusesApi, isTransactionMessageWithSingleSendingSigner, + pipe, Rpc, RpcSubscriptions, sendAndConfirmDurableNonceTransactionFactory, @@ -25,19 +27,28 @@ import { createBaseTransactionPlanExecutor, TransactionPlanExecutorSendAndConfirm, } from './transactionPlanExecutorBase'; +import { + refreshBlockhashForTransactionPlanExecutor, + retryTransactionPlanExecutor, +} from './transactionPlanExecutorDecorators'; export function createDefaultTransactionPlanExecutor( config: SendAndConfirmTransactionFactoryConfig & { + rpc: Rpc; commitment?: Commitment; parallelChunkSize?: number; } ): TransactionPlanExecutor { return createBaseTransactionPlanExecutor({ parallelChunkSize: config.parallelChunkSize, - sendAndConfirm: getDefaultTransactionPlanExecutorSendAndConfirm({ - ...config, - commitment: config.commitment ?? 'confirmed', - }), + sendAndConfirm: pipe( + getDefaultTransactionPlanExecutorSendAndConfirm({ + ...config, + commitment: config.commitment ?? 'confirmed', + }), + (fn) => refreshBlockhashForTransactionPlanExecutor(config.rpc, fn), + (fn) => retryTransactionPlanExecutor(3, fn) + ), }); } From 54743a860d4763ed1acea6b8b16064321c38c9e9 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 12:32:53 +0100 Subject: [PATCH 091/112] wip --- .../transactionPlanExecutor.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts index 9ad58b0..d4e1dfe 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts @@ -5,12 +5,14 @@ import { TransactionPlanResult, } from '../../src'; import { + parallelTransactionPlan, sequentialTransactionPlan, singleTransactionPlanFactory, } from './_transactionPlanHelpers'; import { canceledSingleTransactionPlan, failedSingleTransactionPlan, + parallelTransactionPlanResult, sequentialTransactionPlanResult, successfulSingleTransactionPlan, } from './_transactionPlanResultHelpers'; @@ -100,3 +102,36 @@ test('it cancels transactions after a failed one in a sequential plan', async (t ]) ); }); + +test('it cancels transactions after a failed chunk in a chunked parallel plans', async (t) => { + const singleTransactionPlan = singleTransactionPlanFactory(); + + const planA = singleTransactionPlan(); + const planB = singleTransactionPlan(); + const planC = singleTransactionPlan(); + const planD = singleTransactionPlan(); + + const planAError = getMockSolanaError(); + const executor = createBaseTransactionPlanExecutor({ + parallelChunkSize: 2, + sendAndConfirm: (tx) => + tx === planA.message + ? Promise.reject(planAError) + : Promise.resolve({ transaction: compileTransaction(tx) }), + }); + + const promise = executor( + parallelTransactionPlan([planA, planB, planC, planD]) + ); + const result = await assertFailedResult(t, promise); + + t.deepEqual( + result, + parallelTransactionPlanResult([ + failedSingleTransactionPlan(planA, planAError), + successfulSingleTransactionPlan(planB), + canceledSingleTransactionPlan(planC), + canceledSingleTransactionPlan(planD), + ]) + ); +}); From 69760b0cc060e2e2975cd169f249d06a6e151791 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 13:08:44 +0100 Subject: [PATCH 092/112] wip --- .../computeBudgetHelpers.ts | 12 +++++++----- .../transactionPlanExecutorDecorators.ts | 16 ++++++++++++++++ .../transactionPlanExecutorDefault.ts | 9 ++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts b/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts index 28b6ec0..c296eba 100644 --- a/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts +++ b/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts @@ -39,12 +39,14 @@ export function fillProvisorySetComputeUnitLimitInstruction< ); } -export async function estimateAndUpdateProvisorySetComputeUnitLimitInstruction( - rpc: Rpc, - transactionMessage: +export async function estimateAndUpdateProvisorySetComputeUnitLimitInstruction< + TTransactionMessage extends | CompilableTransactionMessage - | (ITransactionMessageWithFeePayer & TransactionMessage) -) { + | (ITransactionMessageWithFeePayer & TransactionMessage), +>( + rpc: Rpc, + transactionMessage: TTransactionMessage +): Promise { const getComputeUnitEstimateForTransactionMessage = getComputeUnitEstimateForTransactionMessageFactory({ rpc }); diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts index f118cf3..8f3b348 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts @@ -2,10 +2,26 @@ import { GetLatestBlockhashApi, Rpc, setTransactionMessageLifetimeUsingBlockhash, + SimulateTransactionApi, } from '@solana/kit'; +import { estimateAndUpdateProvisorySetComputeUnitLimitInstruction } from './computeBudgetHelpers'; import { getTimedCacheFunction } from './internal'; import { TransactionPlanExecutorSendAndConfirm } from './transactionPlanExecutorBase'; +export function estimateAndUpdateComputeUnitLimitForTransactionPlanExecutor( + rpc: Rpc, + sendAndConfirm: TransactionPlanExecutorSendAndConfirm +): TransactionPlanExecutorSendAndConfirm { + return async (transactionMessage) => { + return await sendAndConfirm( + await estimateAndUpdateProvisorySetComputeUnitLimitInstruction( + rpc, + transactionMessage + ) + ); + }; +} + export function refreshBlockhashForTransactionPlanExecutor( rpc: Rpc, sendAndConfirm: TransactionPlanExecutorSendAndConfirm diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts index ab52b7c..9c19859 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts @@ -17,6 +17,7 @@ import { signAndSendTransactionMessageWithSigners, SignatureNotificationsApi, signTransactionMessageWithSigners, + SimulateTransactionApi, SlotNotificationsApi, TransactionWithBlockhashLifetime, TransactionWithDurableNonceLifetime, @@ -28,13 +29,14 @@ import { TransactionPlanExecutorSendAndConfirm, } from './transactionPlanExecutorBase'; import { + estimateAndUpdateComputeUnitLimitForTransactionPlanExecutor, refreshBlockhashForTransactionPlanExecutor, retryTransactionPlanExecutor, } from './transactionPlanExecutorDecorators'; export function createDefaultTransactionPlanExecutor( config: SendAndConfirmTransactionFactoryConfig & { - rpc: Rpc; + rpc: Rpc; commitment?: Commitment; parallelChunkSize?: number; } @@ -46,6 +48,11 @@ export function createDefaultTransactionPlanExecutor( ...config, commitment: config.commitment ?? 'confirmed', }), + (fn) => + estimateAndUpdateComputeUnitLimitForTransactionPlanExecutor( + config.rpc, + fn + ), (fn) => refreshBlockhashForTransactionPlanExecutor(config.rpc, fn), (fn) => retryTransactionPlanExecutor(3, fn) ), From d94dcbccfc663d1ba9362baf0aeca1540998fb23 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 13:20:33 +0100 Subject: [PATCH 093/112] wip --- .../instructionPlansDraft/instructionPlan.ts | 40 +++++++++++++++++++ .../instructionPlansDraft/transactionPlan.ts | 18 +++++++++ .../_instructionPlanHelpers.ts | 28 ------------- .../_transactionPlanHelpers.ts | 21 ---------- .../transactionPlanExecutor.test.ts | 8 ++-- .../transactionPlanner.test.ts | 16 ++++---- 6 files changed, 70 insertions(+), 61 deletions(-) diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index f2ff508..6f6fafa 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -53,6 +53,46 @@ export type InstructionIterator< ) => TInstruction | null; }>; +export function parallelInstructionPlan( + plans: (InstructionPlan | IInstruction)[] +): ParallelInstructionPlan { + return { kind: 'parallel', plans: parseSingleInstructionPlans(plans) }; +} + +export function sequentialInstructionPlan( + plans: (InstructionPlan | IInstruction)[] +): SequentialInstructionPlan { + return { + kind: 'sequential', + divisible: true, + plans: parseSingleInstructionPlans(plans), + }; +} + +export function nonDivisibleSequentialInstructionPlan( + plans: (InstructionPlan | IInstruction)[] +): SequentialInstructionPlan { + return { + kind: 'sequential', + divisible: false, + plans: parseSingleInstructionPlans(plans), + }; +} + +export function singleInstructionPlan( + instruction: IInstruction +): SingleInstructionPlan { + return { kind: 'single', instruction }; +} + +function parseSingleInstructionPlans( + plans: (InstructionPlan | IInstruction)[] +): InstructionPlan[] { + return plans.map((plan) => + 'kind' in plan ? plan : singleInstructionPlan(plan) + ); +} + export function getLinearIterableInstructionPlan({ getInstruction, totalBytes, diff --git a/clients/js/src/instructionPlansDraft/transactionPlan.ts b/clients/js/src/instructionPlansDraft/transactionPlan.ts index 66b4411..36914e5 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlan.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlan.ts @@ -24,6 +24,24 @@ export type SingleTransactionPlan< message: TTransactionMessage; }>; +export function parallelTransactionPlan( + plans: TransactionPlan[] +): ParallelTransactionPlan { + return { kind: 'parallel', plans }; +} + +export function sequentialTransactionPlan( + plans: TransactionPlan[] +): SequentialTransactionPlan { + return { kind: 'sequential', divisible: true, plans }; +} + +export function nonDivisibleSequentialTransactionPlan( + plans: TransactionPlan[] +): SequentialTransactionPlan { + return { kind: 'sequential', divisible: false, plans }; +} + export function getAllSingleTransactionPlans( transactionPlan: TransactionPlan ): SingleTransactionPlan[] { diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts index 8661c48..d409c0a 100644 --- a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts @@ -9,11 +9,7 @@ import { } from '@solana/kit'; import { getTransactionSize, - InstructionPlan, IterableInstructionPlan, - ParallelInstructionPlan, - SequentialInstructionPlan, - SingleInstructionPlan, TRANSACTION_SIZE_LIMIT, } from '../../src'; @@ -21,30 +17,6 @@ const MINIMUM_INSTRUCTION_SIZE = 35; const MINIMUM_TRANSACTION_SIZE = 136; const MAXIMUM_TRANSACTION_SIZE = TRANSACTION_SIZE_LIMIT - 1; // (for shortU16) -export function parallelInstructionPlan( - plans: InstructionPlan[] -): ParallelInstructionPlan { - return { kind: 'parallel', plans }; -} - -export function sequentialInstructionPlan( - plans: InstructionPlan[] -): SequentialInstructionPlan { - return { kind: 'sequential', divisible: true, plans }; -} - -export function nonDivisibleSequentialInstructionPlan( - plans: InstructionPlan[] -): SequentialInstructionPlan { - return { kind: 'sequential', divisible: false, plans }; -} - -export function singleInstructionPlan( - instruction: IInstruction -): SingleInstructionPlan { - return { kind: 'single', instruction }; -} - export function instructionIteratorFactory() { const baseCounter = 1_000_000_000n; const iteratorIncrement = 1_000_000_000n; diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts index 4a37cdf..abc0ccd 100644 --- a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts +++ b/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts @@ -8,31 +8,10 @@ import { setTransactionMessageFeePayer, } from '@solana/kit'; import { - ParallelTransactionPlan, - SequentialTransactionPlan, setTransactionMessageLifetimeUsingProvisoryBlockhash, SingleTransactionPlan, - TransactionPlan, } from '../../src'; -export function parallelTransactionPlan( - plans: TransactionPlan[] -): ParallelTransactionPlan { - return { kind: 'parallel', plans }; -} - -export function sequentialTransactionPlan( - plans: TransactionPlan[] -): SequentialTransactionPlan { - return { kind: 'sequential', divisible: true, plans }; -} - -export function nonDivisibleSequentialTransactionPlan( - plans: TransactionPlan[] -): SequentialTransactionPlan { - return { kind: 'sequential', divisible: false, plans }; -} - const MOCK_FEE_PAYER = 'Gm1uVH3JxiLgafByNNmnoxLncB7ytpyWNqX3kRM9tSxN' as Address; diff --git a/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts index d4e1dfe..5bdd941 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts @@ -2,13 +2,11 @@ import { compileTransaction, SolanaError } from '@solana/kit'; import test, { Assertions } from 'ava'; import { createBaseTransactionPlanExecutor, - TransactionPlanResult, -} from '../../src'; -import { parallelTransactionPlan, sequentialTransactionPlan, - singleTransactionPlanFactory, -} from './_transactionPlanHelpers'; + TransactionPlanResult, +} from '../../src'; +import { singleTransactionPlanFactory } from './_transactionPlanHelpers'; import { canceledSingleTransactionPlan, failedSingleTransactionPlan, diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 8419beb..9679eec 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -1,22 +1,24 @@ +import { CompilableTransactionMessage } from '@solana/kit'; import test from 'ava'; import { - instructionFactory, - instructionIteratorFactory, + createBaseTransactionPlanner, nonDivisibleSequentialInstructionPlan, + nonDivisibleSequentialTransactionPlan, parallelInstructionPlan, + parallelTransactionPlan, sequentialInstructionPlan, + sequentialTransactionPlan, singleInstructionPlan, +} from '../../src'; +import { + instructionFactory, + instructionIteratorFactory, transactionPercentFactory, } from './_instructionPlanHelpers'; import { getMockCreateTransactionMessage, - nonDivisibleSequentialTransactionPlan, - parallelTransactionPlan, - sequentialTransactionPlan, singleTransactionPlanFactory, } from './_transactionPlanHelpers'; -import { createBaseTransactionPlanner } from '../../src'; -import { CompilableTransactionMessage } from '@solana/kit'; function defaultFactories( createTransactionMessage?: () => CompilableTransactionMessage From 547ba484a1432829260143e2579d399a0f1806b2 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 15:14:22 +0100 Subject: [PATCH 094/112] wip --- clients/js/src/createMetadata.ts | 67 ++++++++++ .../instructionPlansDraft/instructionPlan.ts | 4 +- clients/js/src/internals.ts | 47 ++++++- clients/js/src/updateMetadata.ts | 124 ++++++++++++++++++ 4 files changed, 236 insertions(+), 6 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index ba0ff63..006b514 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -4,10 +4,12 @@ import { GetMinimumBalanceForRentExemptionApi, Lamports, Rpc, + TransactionSigner, } from '@solana/kit'; import { getAllocateInstruction, getInitializeInstruction, + InitializeInput, PROGRAM_METADATA_PROGRAM_ADDRESS, } from './generated'; import { @@ -15,13 +17,19 @@ import { InstructionPlan, MessageInstructionPlan, } from './instructionPlans'; +import { + parallelInstructionPlan, + sequentialInstructionPlan, +} from './instructionPlansDraft'; import { calculateMaxChunkSize, getComputeUnitInstructions, getExtendedMetadataInput, getExtendInstructionPlan, + getExtendInstructionPlan__NEW, getMetadataInstructionPlanExecutor, getWriteInstructionPlan, + getWriteInstructionPlan__NEW, messageFitsInOneTransaction, PdaDetails, REALLOC_LIMIT, @@ -171,3 +179,62 @@ export function getCreateMetadataInstructionPlanUsingBuffer( return mainPlan; } + +export function getCreateMetadataInstructionPlanUsingInstructionData__NEW( + input: InitializeInput & { payer: TransactionSigner; rent: Lamports } +) { + return sequentialInstructionPlan([ + getTransferSolInstruction({ + source: input.payer, + destination: input.metadata, + amount: input.rent, + }), + getInitializeInstruction(input), + ]); +} + +export function getCreateMetadataInstructionPlanUsingBuffer__NEW( + input: InitializeInput & { + data: Uint8Array; + payer: TransactionSigner; + rent: Lamports; + } +) { + return sequentialInstructionPlan([ + getTransferSolInstruction({ + source: input.payer, + destination: input.metadata, + amount: input.rent, + }), + getAllocateInstruction({ + buffer: input.metadata, + authority: input.authority, + program: input.program, + programData: input.programData, + seed: input.seed, + }), + ...(input.data.length > REALLOC_LIMIT + ? [ + getExtendInstructionPlan__NEW({ + account: input.metadata, + authority: input.authority, + extraLength: input.data.length, + program: input.program, + programData: input.programData, + }), + ] + : []), + parallelInstructionPlan([ + getWriteInstructionPlan__NEW({ + buffer: input.metadata, + authority: input.authority, + data: input.data, + }), + ]), + getInitializeInstruction({ + ...input, + system: PROGRAM_METADATA_PROGRAM_ADDRESS, + data: undefined, + }), + ]); +} diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlansDraft/instructionPlan.ts index 6f6fafa..2378e95 100644 --- a/clients/js/src/instructionPlansDraft/instructionPlan.ts +++ b/clients/js/src/instructionPlansDraft/instructionPlan.ts @@ -95,10 +95,10 @@ function parseSingleInstructionPlans( export function getLinearIterableInstructionPlan({ getInstruction, - totalBytes, + totalLength: totalBytes, }: { getInstruction: (offset: number, length: number) => IInstruction; - totalBytes: number; + totalLength: number; }): IterableInstructionPlan { return { kind: 'iterable', diff --git a/clients/js/src/internals.ts b/clients/js/src/internals.ts index 8b6ba97..fe7577f 100644 --- a/clients/js/src/internals.ts +++ b/clients/js/src/internals.ts @@ -37,12 +37,14 @@ import { MessageInstructionPlan, } from './instructionPlans'; import { getProgramAuthority, MetadataInput, MetadataResponse } from './utils'; +import { + getLinearIterableInstructionPlan, + getReallocIterableInstructionPlan, + IterableInstructionPlan, + TRANSACTION_SIZE_LIMIT, +} from './instructionPlansDraft'; export const REALLOC_LIMIT = 10_240; -const TRANSACTION_SIZE_LIMIT = - 1_280 - - 40 /* 40 bytes is the size of the IPv6 header. */ - - 8; /* 8 bytes is the size of the fragment header. */ export type ExtendedMetadataInput = MetadataInput & PdaDetails & { @@ -247,6 +249,26 @@ export function getExtendInstructionPlan(input: { return { kind: 'parallel', plans }; } +export function getExtendInstructionPlan__NEW(input: { + account: Address; + authority: TransactionSigner; + extraLength: number; + program?: Address; + programData?: Address; +}): IterableInstructionPlan { + return getReallocIterableInstructionPlan({ + totalSize: input.extraLength, + getInstruction: (size) => + getExtendInstruction({ + account: input.account, + authority: input.authority, + length: size, + program: input.program, + programData: input.programData, + }), + }); +} + export function getWriteInstructionPlan(input: { buffer: Address; authority: TransactionSigner; @@ -266,6 +288,23 @@ export function getWriteInstructionPlan(input: { }; } +export function getWriteInstructionPlan__NEW(input: { + buffer: Address; + authority: TransactionSigner; + data: ReadonlyUint8Array; +}): IterableInstructionPlan { + return getLinearIterableInstructionPlan({ + totalLength: input.data.length, + getInstruction: (offset, length) => + getWriteInstruction({ + buffer: input.buffer, + authority: input.authority, + offset, + data: input.data.slice(offset, offset + length), + }), + }); +} + export function getMetadataInstructionPlanExecutor( input: Pick< ExtendedMetadataInput, diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index 49dcb0e..c17d99b 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -20,14 +20,17 @@ import { getSetDataInstruction, getTrimInstruction, PROGRAM_METADATA_PROGRAM_ADDRESS, + SetDataInput, } from './generated'; import { calculateMaxChunkSize, getComputeUnitInstructions, getExtendedMetadataInput, getExtendInstructionPlan, + getExtendInstructionPlan__NEW, getMetadataInstructionPlanExecutor, getWriteInstructionPlan, + getWriteInstructionPlan__NEW, messageFitsInOneTransaction, PdaDetails, REALLOC_LIMIT, @@ -38,6 +41,10 @@ import { InstructionPlan, MessageInstructionPlan, } from './instructionPlans'; +import { + parallelInstructionPlan, + sequentialInstructionPlan, +} from './instructionPlansDraft'; export async function updateMetadata( input: MetadataInput @@ -287,3 +294,120 @@ export function getUpdateMetadataInstructionPlanUsingBuffer( return mainPlan; } + +export function getUpdateMetadataInstructionPlanUsingInstructionData__NEW( + input: SetDataInput & { + extraRent: Lamports; + payer: TransactionSigner; + sizeDifference: bigint; + } +) { + return sequentialInstructionPlan([ + ...(input.sizeDifference > 0 + ? [ + getTransferSolInstruction({ + source: input.payer, + destination: input.metadata, + amount: input.extraRent, + }), + ] + : []), + getSetDataInstruction({ ...input, buffer: undefined }), + ...(input.sizeDifference < 0 + ? [ + getTrimInstruction({ + account: input.metadata, + authority: input.authority, + destination: input.payer.address, + program: input.program, + programData: input.program, + }), + ] + : []), + ]); +} + +export function getUpdateMetadataInstructionPlanUsingBuffer__NEW( + input: SetDataInput & { + buffer: TransactionSigner; + bufferRent: Lamports; + closeBuffer?: boolean; + data: Uint8Array; + extraRent: Lamports; + payer: TransactionSigner; + sizeDifference: number; + } +) { + return sequentialInstructionPlan([ + ...(input.sizeDifference > 0 + ? [ + getTransferSolInstruction({ + source: input.payer, + destination: input.metadata, + amount: input.extraRent, + }), + ] + : []), + getCreateAccountInstruction({ + payer: input.payer, + newAccount: input.buffer, + lamports: input.bufferRent, + space: getAccountSize(input.data.length), + programAddress: PROGRAM_METADATA_PROGRAM_ADDRESS, + }), + getAllocateInstruction({ + buffer: input.buffer.address, + authority: input.buffer, + }), + getSetAuthorityInstruction({ + account: input.buffer.address, + authority: input.buffer, + newAuthority: input.authority.address, + }), + ...(input.sizeDifference > REALLOC_LIMIT + ? [ + getExtendInstructionPlan__NEW({ + account: input.metadata, + authority: input.authority, + extraLength: input.sizeDifference, + program: input.program, + programData: input.programData, + }), + ] + : []), + parallelInstructionPlan([ + getWriteInstructionPlan__NEW({ + buffer: input.buffer.address, + authority: input.authority, + data: input.data, + }), + ]), + getSetDataInstruction({ + ...input, + buffer: input.buffer.address, + data: undefined, + }), + ...(input.closeBuffer + ? [ + getCloseInstruction({ + account: input.buffer.address, + authority: input.authority, + destination: input.payer.address, + program: input.program, + programData: input.programData, + }), + ] + : []), + ...(input.sizeDifference < 0 + ? [ + getTrimInstruction({ + account: input.metadata, + authority: input.authority, + destination: input.payer.address, + program: input.program, + programData: input.programData, + }), + ] + : []), + ]); +} From 71ebf240b3bb6565b08c9367353349296bc358a0 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 17:49:47 +0100 Subject: [PATCH 095/112] wip --- clients/js/src/createMetadata.ts | 59 ++++++++++++++++++- .../transactionPlannerDefault.ts | 21 +++++-- clients/js/src/utils.ts | 36 +++++++++++ clients/js/test/createMetadata.test.ts | 3 +- 4 files changed, 112 insertions(+), 7 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 006b514..08a9486 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -3,6 +3,7 @@ import { CompilableTransactionMessage, GetMinimumBalanceForRentExemptionApi, Lamports, + ReadonlyUint8Array, Rpc, TransactionSigner, } from '@solana/kit'; @@ -18,6 +19,8 @@ import { MessageInstructionPlan, } from './instructionPlans'; import { + createDefaultTransactionPlanExecutor, + createDefaultTransactionPlanner, parallelInstructionPlan, sequentialInstructionPlan, } from './instructionPlansDraft'; @@ -28,13 +31,20 @@ import { getExtendInstructionPlan, getExtendInstructionPlan__NEW, getMetadataInstructionPlanExecutor, + getPdaDetails, getWriteInstructionPlan, getWriteInstructionPlan__NEW, messageFitsInOneTransaction, PdaDetails, REALLOC_LIMIT, } from './internals'; -import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; +import { + getAccountSize, + MetadataInput, + MetadataInput__NEW, + MetadataResponse, + MetadataResponse__NEW, +} from './utils'; export async function createMetadata( input: MetadataInput @@ -180,6 +190,51 @@ export function getCreateMetadataInstructionPlanUsingBuffer( return mainPlan; } +export async function createMetadata__NEW( + input: MetadataInput__NEW & { + rpc: Rpc & + Parameters[0]['rpc']; + rpcSubscriptions: Parameters< + typeof createDefaultTransactionPlanExecutor + >[0]['rpcSubscriptions']; + } +): Promise { + const planner = createDefaultTransactionPlanner({ + feePayer: input.payer, + computeUnitPrice: input.priorityFees, + }); + const executor = createDefaultTransactionPlanExecutor({ + rpc: input.rpc, + rpcSubscriptions: input.rpcSubscriptions, + parallelChunkSize: 5, + }); + + const [{ programData, isCanonical, metadata }, rent] = await Promise.all([ + getPdaDetails(input), + input.rpc + .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .send(), + ]); + const extendedInput = { + ...input, + programData: isCanonical ? programData : undefined, + metadata, + rent, + }; + + const planWithInstructionData = + getCreateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput); + const planWithBuffer = + getCreateMetadataInstructionPlanUsingBuffer__NEW(extendedInput); + const transactionPlan = await planner(planWithInstructionData).catch(() => + planner(planWithBuffer) + ); + + const result = await executor(transactionPlan); + + return { metadata, result }; +} + export function getCreateMetadataInstructionPlanUsingInstructionData__NEW( input: InitializeInput & { payer: TransactionSigner; rent: Lamports } ) { @@ -195,7 +250,7 @@ export function getCreateMetadataInstructionPlanUsingInstructionData__NEW( export function getCreateMetadataInstructionPlanUsingBuffer__NEW( input: InitializeInput & { - data: Uint8Array; + data: ReadonlyUint8Array; payer: TransactionSigner; rent: Lamports; } diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts index 690b5d3..95682cb 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts @@ -1,5 +1,7 @@ import { + appendTransactionMessageInstruction, createTransactionMessage, + MicroLamports, pipe, setTransactionMessageFeePayerSigner, TransactionSigner, @@ -8,17 +10,28 @@ import { TransactionPlanner } from './transactionPlanner'; import { createBaseTransactionPlanner } from './transactionPlannerBase'; import { setTransactionMessageLifetimeUsingProvisoryBlockhash } from './transactionHelpers'; import { fillProvisorySetComputeUnitLimitInstruction } from './computeBudgetHelpers'; +import { getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget'; -export function createDefaultTransactionPlanner( - feePayer: TransactionSigner -): TransactionPlanner { +export function createDefaultTransactionPlanner(config: { + feePayer: TransactionSigner; + computeUnitPrice?: MicroLamports; +}): TransactionPlanner { return createBaseTransactionPlanner({ createTransactionMessage: () => pipe( createTransactionMessage({ version: 0 }), setTransactionMessageLifetimeUsingProvisoryBlockhash, fillProvisorySetComputeUnitLimitInstruction, - (tx) => setTransactionMessageFeePayerSigner(feePayer, tx) + (tx) => setTransactionMessageFeePayerSigner(config.feePayer, tx), + (tx) => + config.computeUnitPrice + ? appendTransactionMessageInstruction( + getSetComputeUnitPriceInstruction({ + microLamports: config.computeUnitPrice, + }), + tx + ) + : tx ), }); } diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index cbc5b57..664cc3f 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -34,6 +34,7 @@ import { FormatArgs, SeedArgs, } from './generated'; +import { TransactionPlanResult } from './instructionPlansDraft'; export const ACCOUNT_HEADER_LENGTH = 96; @@ -94,11 +95,46 @@ export type MetadataInput = { extractLastTransaction?: boolean; }; +export type MetadataInput__NEW = { + payer: TransactionSigner; + authority: TransactionSigner; + program: Address; + seed: SeedArgs; + encoding: EncodingArgs; + compression: CompressionArgs; + format: FormatArgs; + dataSource: DataSourceArgs; + data: ReadonlyUint8Array; + /** + * Extra fees to pay in microlamports per CU. + * Defaults to no extra fees. + */ + priorityFees?: MicroLamports; + /** + * Whether to use a buffer for creating or updating a metadata account. + * If a `TransactionSigner` is provided, the provided buffer will be used for updating only. + * Defaults to `true` unless the entire operation can be done in a single transaction. + */ + buffer?: TransactionSigner | boolean; + /** + * When using a buffer, whether to close the buffer account after the operation. + * This is only relevant when updating a metadata account since, when creating + * them, buffer accounts are transformed into metadata accounts. + * Defaults to `true`. + */ + closeBuffer?: boolean; +}; + export type MetadataResponse = { metadata: Address; lastTransaction?: Transaction; }; +export type MetadataResponse__NEW = { + metadata: Address; + result: TransactionPlanResult; +}; + export function getAccountSize(dataLength: bigint | number) { return BigInt(ACCOUNT_HEADER_LENGTH) + BigInt(dataLength); } diff --git a/clients/js/test/createMetadata.test.ts b/clients/js/test/createMetadata.test.ts index 0c70f40..beaee5a 100644 --- a/clients/js/test/createMetadata.test.ts +++ b/clients/js/test/createMetadata.test.ts @@ -4,6 +4,7 @@ import { AccountDiscriminator, Compression, createMetadata, + createMetadata__NEW, DataSource, Encoding, fetchMetadata, @@ -24,7 +25,7 @@ test('it creates a canonical metadata account', async (t) => { // When we create a canonical metadata account for the program. const data = getUtf8Encoder().encode('{"standard":"dummyIdl"}'); - const { metadata } = await createMetadata({ + const { metadata } = await createMetadata__NEW({ ...client, payer: authority, authority, From 59728c0d5924608548466cfbec8f65d8a429f665 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 18:15:23 +0100 Subject: [PATCH 096/112] Throw if created TransactionPlan is invalid --- .../transactionPlannerBase.ts | 17 +++++++++++++++++ .../transactionPlanner.test.ts | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts b/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts index 3c59aa5..8762de0 100644 --- a/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts @@ -78,6 +78,15 @@ export function createBaseTransactionPlanner( 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; + } + return plan; }; } @@ -347,3 +356,11 @@ export function getRemainingTransactionSize( ) { return TRANSACTION_SIZE_LIMIT - getTransactionSize(message); } + +function isValidTransactionPlan(transactionPlan: TransactionPlan): boolean { + if (transactionPlan.kind === 'single') { + const transactionSize = getTransactionSize(transactionPlan.message); + return transactionSize <= TRANSACTION_SIZE_LIMIT; + } + return transactionPlan.plans.every(isValidTransactionPlan); +} diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts index 9679eec..8d8d773 100644 --- a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts +++ b/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts @@ -9,6 +9,7 @@ import { sequentialInstructionPlan, sequentialTransactionPlan, singleInstructionPlan, + TransactionPlan, } from '../../src'; import { instructionFactory, @@ -55,6 +56,23 @@ 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 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])); +}); + /** * [Seq] ───────────────────▶ [Tx: A + B] * │ From 315482e7f07e53f69d1a2dfea6841f9524c82905 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 18:18:11 +0100 Subject: [PATCH 097/112] wip --- clients/js/test/createMetadata.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/clients/js/test/createMetadata.test.ts b/clients/js/test/createMetadata.test.ts index beaee5a..c6f54af 100644 --- a/clients/js/test/createMetadata.test.ts +++ b/clients/js/test/createMetadata.test.ts @@ -3,7 +3,6 @@ import test from 'ava'; import { AccountDiscriminator, Compression, - createMetadata, createMetadata__NEW, DataSource, Encoding, @@ -64,7 +63,7 @@ test('it creates a canonical metadata account with data larger than a transactio // When we create a canonical metadata account for the program with a lot of data. const largeData = getUtf8Encoder().encode('x'.repeat(3_000)); - const { metadata } = await createMetadata({ + const { metadata } = await createMetadata__NEW({ ...client, payer: authority, authority, @@ -103,7 +102,7 @@ test('it creates a non-canonical metadata account', async (t) => { // When we create a non-canonical metadata account for the program. const data = getUtf8Encoder().encode('{"standard":"dummyIdl"}'); - const { metadata } = await createMetadata({ + const { metadata } = await createMetadata__NEW({ ...client, payer: authority, authority, @@ -142,7 +141,7 @@ test('it creates a non-canonical metadata account with data larger than a transa // When we create a non-canonical metadata account for the program with a lot of data. const largeData = getUtf8Encoder().encode('x'.repeat(3_000)); - const { metadata } = await createMetadata({ + const { metadata } = await createMetadata__NEW({ ...client, payer: authority, authority, From b2e448e96a50badae2e85df0ed9974d798dc96dd Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 10 Apr 2025 18:40:04 +0100 Subject: [PATCH 098/112] wip --- clients/js/src/createMetadata.ts | 12 ++-- clients/js/src/updateMetadata.ts | 104 ++++++++++++++++++++++++++----- clients/js/src/utils.ts | 6 -- 3 files changed, 94 insertions(+), 28 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 08a9486..52be5ba 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -222,12 +222,10 @@ export async function createMetadata__NEW( rent, }; - const planWithInstructionData = - getCreateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput); - const planWithBuffer = - getCreateMetadataInstructionPlanUsingBuffer__NEW(extendedInput); - const transactionPlan = await planner(planWithInstructionData).catch(() => - planner(planWithBuffer) + const transactionPlan = await planner( + getCreateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput) + ).catch(() => + planner(getCreateMetadataInstructionPlanUsingBuffer__NEW(extendedInput)) ); const result = await executor(transactionPlan); @@ -249,7 +247,7 @@ export function getCreateMetadataInstructionPlanUsingInstructionData__NEW( } export function getCreateMetadataInstructionPlanUsingBuffer__NEW( - input: InitializeInput & { + input: Omit & { data: ReadonlyUint8Array; payer: TransactionSigner; rent: Lamports; diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index c17d99b..a126c8a 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -5,10 +5,12 @@ import { import { CompilableTransactionMessage, generateKeyPairSigner, + GetAccountInfoApi, GetMinimumBalanceForRentExemptionApi, isTransactionSigner, lamports, Lamports, + ReadonlyUint8Array, Rpc, TransactionSigner, } from '@solana/kit'; @@ -22,6 +24,17 @@ import { PROGRAM_METADATA_PROGRAM_ADDRESS, SetDataInput, } from './generated'; +import { + getTransactionMessageFromPlan, + InstructionPlan, + MessageInstructionPlan, +} from './instructionPlans'; +import { + createDefaultTransactionPlanExecutor, + createDefaultTransactionPlanner, + parallelInstructionPlan, + sequentialInstructionPlan, +} from './instructionPlansDraft'; import { calculateMaxChunkSize, getComputeUnitInstructions, @@ -29,22 +42,20 @@ import { getExtendInstructionPlan, getExtendInstructionPlan__NEW, getMetadataInstructionPlanExecutor, + getPdaDetails, getWriteInstructionPlan, getWriteInstructionPlan__NEW, messageFitsInOneTransaction, PdaDetails, REALLOC_LIMIT, } from './internals'; -import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; import { - getTransactionMessageFromPlan, - InstructionPlan, - MessageInstructionPlan, -} from './instructionPlans'; -import { - parallelInstructionPlan, - sequentialInstructionPlan, -} from './instructionPlansDraft'; + getAccountSize, + MetadataInput, + MetadataInput__NEW, + MetadataResponse, + MetadataResponse__NEW, +} from './utils'; export async function updateMetadata( input: MetadataInput @@ -295,11 +306,74 @@ export function getUpdateMetadataInstructionPlanUsingBuffer( return mainPlan; } +export async function updateMetadata__NEW( + input: MetadataInput__NEW & { + rpc: Rpc & + Parameters[0]['rpc']; + rpcSubscriptions: Parameters< + typeof createDefaultTransactionPlanExecutor + >[0]['rpcSubscriptions']; + } +): Promise { + const planner = createDefaultTransactionPlanner({ + feePayer: input.payer, + computeUnitPrice: input.priorityFees, + }); + const executor = createDefaultTransactionPlanExecutor({ + rpc: input.rpc, + rpcSubscriptions: input.rpcSubscriptions, + parallelChunkSize: 5, + }); + + const [{ programData, isCanonical, metadata }, bufferRent] = + await Promise.all([ + getPdaDetails(input), + input.rpc + .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .send(), + ]); + + const metadataAccount = await fetchMetadata(input.rpc, metadata); + if (!metadataAccount.data.mutable) { + throw new Error('Metadata account is immutable'); + } + + const sizeDifference = BigInt(input.data.length) - metadataAccount.space; + const extraRentPromise = + sizeDifference > 0 + ? input.rpc.getMinimumBalanceForRentExemption(sizeDifference).send() + : Promise.resolve(lamports(0n)); + const [extraRent, buffer] = await Promise.all([ + extraRentPromise, + generateKeyPairSigner(), + ]); + + const extendedInput = { + ...input, + programData: isCanonical ? programData : undefined, + metadata, + buffer, + bufferRent, + extraRent, + sizeDifference, + }; + + const transactionPlan = await planner( + getUpdateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput) + ).catch(() => + planner(getUpdateMetadataInstructionPlanUsingBuffer__NEW(extendedInput)) + ); + + const result = await executor(transactionPlan); + + return { metadata, result }; +} + export function getUpdateMetadataInstructionPlanUsingInstructionData__NEW( - input: SetDataInput & { + input: Omit & { extraRent: Lamports; payer: TransactionSigner; - sizeDifference: bigint; + sizeDifference: bigint | number; } ) { return sequentialInstructionPlan([ @@ -328,14 +402,14 @@ export function getUpdateMetadataInstructionPlanUsingInstructionData__NEW( } export function getUpdateMetadataInstructionPlanUsingBuffer__NEW( - input: SetDataInput & { + input: Omit & { buffer: TransactionSigner; bufferRent: Lamports; closeBuffer?: boolean; - data: Uint8Array; + data: ReadonlyUint8Array; extraRent: Lamports; payer: TransactionSigner; - sizeDifference: number; + sizeDifference: number | bigint; } ) { return sequentialInstructionPlan([ @@ -369,7 +443,7 @@ export function getUpdateMetadataInstructionPlanUsingBuffer__NEW( getExtendInstructionPlan__NEW({ account: input.metadata, authority: input.authority, - extraLength: input.sizeDifference, + extraLength: Number(input.sizeDifference), program: input.program, programData: input.programData, }), diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 664cc3f..2bdadab 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -110,12 +110,6 @@ export type MetadataInput__NEW = { * Defaults to no extra fees. */ priorityFees?: MicroLamports; - /** - * Whether to use a buffer for creating or updating a metadata account. - * If a `TransactionSigner` is provided, the provided buffer will be used for updating only. - * Defaults to `true` unless the entire operation can be done in a single transaction. - */ - buffer?: TransactionSigner | boolean; /** * When using a buffer, whether to close the buffer account after the operation. * This is only relevant when updating a metadata account since, when creating From 463d972e12b8086c3d7985e6ddf822fec2fa3888 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 08:46:14 +0100 Subject: [PATCH 099/112] wip --- clients/js/src/updateMetadata.ts | 3 ++- clients/js/test/updateMetadata.test.ts | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index a126c8a..f74debc 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -338,7 +338,8 @@ export async function updateMetadata__NEW( throw new Error('Metadata account is immutable'); } - const sizeDifference = BigInt(input.data.length) - metadataAccount.space; + const sizeDifference = + BigInt(input.data.length) - BigInt(metadataAccount.data.data.length); const extraRentPromise = sizeDifference > 0 ? input.rpc.getMinimumBalanceForRentExemption(sizeDifference).send() diff --git a/clients/js/test/updateMetadata.test.ts b/clients/js/test/updateMetadata.test.ts index e5d8802..7ea59cf 100644 --- a/clients/js/test/updateMetadata.test.ts +++ b/clients/js/test/updateMetadata.test.ts @@ -4,12 +4,14 @@ import { AccountDiscriminator, Compression, createMetadata, + createMetadata__NEW, DataSource, Encoding, fetchMetadata, Format, Metadata, updateMetadata, + updateMetadata__NEW, } from '../src'; import { createDefaultSolanaClient, @@ -24,7 +26,7 @@ test('it updates a canonical metadata account', async (t) => { const [program] = await createDeployedProgram(client, authority); // And the following existing canonical metadata account. - await createMetadata({ + await createMetadata__NEW({ ...client, payer: authority, authority, @@ -39,7 +41,7 @@ test('it updates a canonical metadata account', async (t) => { // When we update the metadata account with new data. const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); - const { metadata } = await updateMetadata({ + const { metadata } = await updateMetadata__NEW({ ...client, payer: authority, authority, From e4e3f6f45c44a23f6b4f0699f59cf840c771e91a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 08:49:23 +0100 Subject: [PATCH 100/112] wip --- clients/js/test/updateMetadata.test.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/clients/js/test/updateMetadata.test.ts b/clients/js/test/updateMetadata.test.ts index 7ea59cf..1c5725e 100644 --- a/clients/js/test/updateMetadata.test.ts +++ b/clients/js/test/updateMetadata.test.ts @@ -3,14 +3,12 @@ import test from 'ava'; import { AccountDiscriminator, Compression, - createMetadata, createMetadata__NEW, DataSource, Encoding, fetchMetadata, Format, Metadata, - updateMetadata, updateMetadata__NEW, } from '../src'; import { @@ -79,7 +77,7 @@ test('it updates a canonical metadata account with data larger than a transactio const [program] = await createDeployedProgram(client, authority); // And the following existing canonical metadata account. - await createMetadata({ + await createMetadata__NEW({ ...client, payer: authority, authority, @@ -94,7 +92,7 @@ test('it updates a canonical metadata account with data larger than a transactio // When we update the metadata account with new data with a lot of data. const newData = getUtf8Encoder().encode('x'.repeat(3_000)); - const { metadata } = await updateMetadata({ + const { metadata } = await updateMetadata__NEW({ ...client, payer: authority, authority, @@ -132,7 +130,7 @@ test('it updates a non-canonical metadata account', async (t) => { const program = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); // And the following existing non-canonical metadata account. - await createMetadata({ + await createMetadata__NEW({ ...client, payer: authority, authority, @@ -147,7 +145,7 @@ test('it updates a non-canonical metadata account', async (t) => { // When we update the metadata account with new data. const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); - const { metadata } = await updateMetadata({ + const { metadata } = await updateMetadata__NEW({ ...client, payer: authority, authority, @@ -185,7 +183,7 @@ test('it updates a non-canonical metadata account with data larger than a transa const program = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); // And the following existing non-canonical metadata account. - await createMetadata({ + await createMetadata__NEW({ ...client, payer: authority, authority, @@ -200,7 +198,7 @@ test('it updates a non-canonical metadata account with data larger than a transa // When we update the metadata account with new data. const newData = getUtf8Encoder().encode('x'.repeat(3_000)); - const { metadata } = await updateMetadata({ + const { metadata } = await updateMetadata__NEW({ ...client, payer: authority, authority, From 4dd04aef73ba528f21201851ed56fd517d2f0d8a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 08:58:41 +0100 Subject: [PATCH 101/112] wip --- clients/js/src/createMetadata.ts | 1 - clients/js/src/updateMetadata.ts | 1 - clients/js/src/uploadMetadata.ts | 112 ++++++++++++++++++++++++- clients/js/test/uploadMetadata.test.ts | 10 +-- 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 52be5ba..0ed1287 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -229,7 +229,6 @@ export async function createMetadata__NEW( ); const result = await executor(transactionPlan); - return { metadata, result }; } diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index f74debc..11a1f67 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -366,7 +366,6 @@ export async function updateMetadata__NEW( ); const result = await executor(transactionPlan); - return { metadata, result }; } diff --git a/clients/js/src/uploadMetadata.ts b/clients/js/src/uploadMetadata.ts index b673cbb..5caa8e9 100644 --- a/clients/js/src/uploadMetadata.ts +++ b/clients/js/src/uploadMetadata.ts @@ -1,11 +1,36 @@ -import { getCreateMetadataInstructionPlan } from './createMetadata'; +import { + generateKeyPairSigner, + GetAccountInfoApi, + GetMinimumBalanceForRentExemptionApi, + lamports, + Rpc, +} from '@solana/kit'; +import { + getCreateMetadataInstructionPlan, + getCreateMetadataInstructionPlanUsingBuffer__NEW, + getCreateMetadataInstructionPlanUsingInstructionData__NEW, +} from './createMetadata'; import { fetchMaybeMetadata } from './generated'; +import { + createDefaultTransactionPlanExecutor, + createDefaultTransactionPlanner, +} from './instructionPlansDraft'; import { getExtendedMetadataInput, getMetadataInstructionPlanExecutor, + getPdaDetails, } from './internals'; -import { getUpdateMetadataInstructionPlan } from './updateMetadata'; -import { MetadataInput } from './utils'; +import { + getUpdateMetadataInstructionPlan, + getUpdateMetadataInstructionPlanUsingBuffer__NEW, + getUpdateMetadataInstructionPlanUsingInstructionData__NEW, +} from './updateMetadata'; +import { + getAccountSize, + MetadataInput, + MetadataInput__NEW, + MetadataResponse__NEW, +} from './utils'; export async function uploadMetadata(input: MetadataInput) { const extendedInput = await getExtendedMetadataInput(input); @@ -31,3 +56,84 @@ export async function uploadMetadata(input: MetadataInput) { }); return await executor(plan); } + +export async function uploadMetadata__NEW( + input: MetadataInput__NEW & { + rpc: Rpc & + Parameters[0]['rpc']; + rpcSubscriptions: Parameters< + typeof createDefaultTransactionPlanExecutor + >[0]['rpcSubscriptions']; + } +): Promise { + const planner = createDefaultTransactionPlanner({ + feePayer: input.payer, + computeUnitPrice: input.priorityFees, + }); + const executor = createDefaultTransactionPlanExecutor({ + rpc: input.rpc, + rpcSubscriptions: input.rpcSubscriptions, + parallelChunkSize: 5, + }); + + const [{ programData, isCanonical, metadata }, rent] = await Promise.all([ + getPdaDetails(input), + input.rpc + .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .send(), + ]); + + const metadataAccount = await fetchMaybeMetadata(input.rpc, metadata); + + if (!metadataAccount.exists) { + const extendedInput = { + ...input, + programData: isCanonical ? programData : undefined, + metadata, + rent, + }; + + const transactionPlan = await planner( + getCreateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput) + ).catch(() => + planner(getCreateMetadataInstructionPlanUsingBuffer__NEW(extendedInput)) + ); + + const result = await executor(transactionPlan); + return { metadata, result }; + } + + if (!metadataAccount.data.mutable) { + throw new Error('Metadata account is immutable'); + } + + const sizeDifference = + BigInt(input.data.length) - BigInt(metadataAccount.data.data.length); + const extraRentPromise = + sizeDifference > 0 + ? input.rpc.getMinimumBalanceForRentExemption(sizeDifference).send() + : Promise.resolve(lamports(0n)); + const [extraRent, buffer] = await Promise.all([ + extraRentPromise, + generateKeyPairSigner(), + ]); + + const extendedInput = { + ...input, + programData: isCanonical ? programData : undefined, + metadata, + buffer, + bufferRent: rent, + extraRent, + sizeDifference, + }; + + const transactionPlan = await planner( + getUpdateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput) + ).catch(() => + planner(getUpdateMetadataInstructionPlanUsingBuffer__NEW(extendedInput)) + ); + + const result = await executor(transactionPlan); + return { metadata, result }; +} diff --git a/clients/js/test/uploadMetadata.test.ts b/clients/js/test/uploadMetadata.test.ts index 92e6251..8060521 100644 --- a/clients/js/test/uploadMetadata.test.ts +++ b/clients/js/test/uploadMetadata.test.ts @@ -3,14 +3,14 @@ import test from 'ava'; import { AccountDiscriminator, Compression, - createMetadata, + createMetadata__NEW, DataSource, Encoding, fetchMetadata, findCanonicalPda, Format, Metadata, - uploadMetadata, + uploadMetadata__NEW, } from '../src'; import { createDefaultSolanaClient, @@ -30,7 +30,7 @@ test('it creates a new metadata account if it does not exist', async (t) => { // When we upload this canonical metadata account. const data = getUtf8Encoder().encode('Some data'); - await uploadMetadata({ + await uploadMetadata__NEW({ ...client, payer: authority, authority, @@ -68,7 +68,7 @@ test('it updates a metadata account if it exists', async (t) => { const [program] = await createDeployedProgram(client, authority); // And given the following canonical metadata account exists. - await createMetadata({ + await createMetadata__NEW({ ...client, payer: authority, authority, @@ -83,7 +83,7 @@ test('it updates a metadata account if it exists', async (t) => { // When we upload this canonical metadata account with different data. const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); - const { metadata } = await uploadMetadata({ + const { metadata } = await uploadMetadata__NEW({ ...client, payer: authority, authority, From 47b0c52c0c3e6de145e71e45a77e96ae240ea7f9 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 09:04:46 +0100 Subject: [PATCH 102/112] wip --- clients/js/src/cli.ts | 15 ++++++++------- clients/js/src/createMetadata.ts | 12 ------------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 0b81f40..33499d8 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -21,6 +21,7 @@ import { RpcSubscriptions, SolanaRpcApi, SolanaRpcSubscriptionsApi, + Transaction, } from '@solana/kit'; import chalk from 'chalk'; import { Command, Option } from 'commander'; @@ -46,7 +47,7 @@ import { packExternalData, packUrlData, } from './packData'; -import { uploadMetadata } from './uploadMetadata'; +import { uploadMetadata__NEW } from './uploadMetadata'; import { getProgramAuthority } from './utils'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; @@ -175,7 +176,7 @@ program 'You must be the program authority to upload a canonical metadata account. Use `--non-canonical` option to upload as a third party.' ); } - const { lastTransaction } = await uploadMetadata({ + await uploadMetadata__NEW({ ...client, ...getPackedData(content, options), payer, @@ -183,14 +184,14 @@ program program, seed, format: getFormat(options), - buffer: options.bufferOnly ? true : undefined, - extractLastTransaction: options.bufferOnly, closeBuffer: true, priorityFees: options.priorityFees, }); - if (lastTransaction) { - const transactionBytes = - getTransactionEncoder().encode(lastTransaction); + const exportTransaction = false; // TODO: Option + if (exportTransaction) { + const transactionBytes = getTransactionEncoder().encode( + {} as Transaction + ); const base64EncodedTransaction = getBase64Decoder().decode(transactionBytes); const base58EncodedTransaction = diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 0ed1287..33d3a06 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -27,10 +27,8 @@ import { import { calculateMaxChunkSize, getComputeUnitInstructions, - getExtendedMetadataInput, getExtendInstructionPlan, getExtendInstructionPlan__NEW, - getMetadataInstructionPlanExecutor, getPdaDetails, getWriteInstructionPlan, getWriteInstructionPlan__NEW, @@ -42,19 +40,9 @@ import { getAccountSize, MetadataInput, MetadataInput__NEW, - MetadataResponse, MetadataResponse__NEW, } from './utils'; -export async function createMetadata( - input: MetadataInput -): Promise { - const extendedInput = await getExtendedMetadataInput(input); - const executor = getMetadataInstructionPlanExecutor(extendedInput); - const plan = await getCreateMetadataInstructionPlan(extendedInput); - return await executor(plan); -} - export async function getCreateMetadataInstructionPlan( input: Omit & PdaDetails & { From bb70d2d64246f74d339b65630b9f4f5a6e528a7d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 09:06:08 +0100 Subject: [PATCH 103/112] wip --- clients/js/src/uploadMetadata.ts | 34 +----------------------- clients/js/test/downloadMetadata.test.ts | 6 ++--- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/clients/js/src/uploadMetadata.ts b/clients/js/src/uploadMetadata.ts index 5caa8e9..2f4e22c 100644 --- a/clients/js/src/uploadMetadata.ts +++ b/clients/js/src/uploadMetadata.ts @@ -6,7 +6,6 @@ import { Rpc, } from '@solana/kit'; import { - getCreateMetadataInstructionPlan, getCreateMetadataInstructionPlanUsingBuffer__NEW, getCreateMetadataInstructionPlanUsingInstructionData__NEW, } from './createMetadata'; @@ -15,48 +14,17 @@ import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, } from './instructionPlansDraft'; +import { getPdaDetails } from './internals'; import { - getExtendedMetadataInput, - getMetadataInstructionPlanExecutor, - getPdaDetails, -} from './internals'; -import { - getUpdateMetadataInstructionPlan, getUpdateMetadataInstructionPlanUsingBuffer__NEW, getUpdateMetadataInstructionPlanUsingInstructionData__NEW, } from './updateMetadata'; import { getAccountSize, - MetadataInput, MetadataInput__NEW, MetadataResponse__NEW, } from './utils'; -export async function uploadMetadata(input: MetadataInput) { - const extendedInput = await getExtendedMetadataInput(input); - const executor = getMetadataInstructionPlanExecutor(extendedInput); - const metadataAccount = await fetchMaybeMetadata( - input.rpc, - extendedInput.metadata - ); - - // Create metadata if it doesn't exist. - if (!metadataAccount.exists) { - const plan = await getCreateMetadataInstructionPlan(extendedInput); - return await executor(plan); - } - - // Update metadata if it exists. - if (!metadataAccount.data.mutable) { - throw new Error('Metadata account is immutable'); - } - const plan = await getUpdateMetadataInstructionPlan({ - ...extendedInput, - currentDataLength: BigInt(metadataAccount.data.data.length), - }); - return await executor(plan); -} - export async function uploadMetadata__NEW( input: MetadataInput__NEW & { rpc: Rpc & diff --git a/clients/js/test/downloadMetadata.test.ts b/clients/js/test/downloadMetadata.test.ts index c8b96da..d0b0c48 100644 --- a/clients/js/test/downloadMetadata.test.ts +++ b/clients/js/test/downloadMetadata.test.ts @@ -4,7 +4,7 @@ import { downloadAndParseMetadata, Format, packDirectData, - uploadMetadata, + uploadMetadata__NEW, } from '../src'; import { createDefaultSolanaClient, @@ -20,7 +20,7 @@ test('it fetches and parses direct IDLs from canonical metadata accounts', async // And given the following IDL exists for the program. const idl = '{"kind":"rootNode","standard":"codama","version":"1.0.0"}'; - await uploadMetadata({ + await uploadMetadata__NEW({ ...client, ...packDirectData({ content: idl }), payer: authority, @@ -49,7 +49,7 @@ test('it fetches and parses direct IDLs from non-canonical metadata accounts', a // And given the following IDL exists for the program. const idl = '{"kind":"rootNode","standard":"codama","version":"1.0.0"}'; - await uploadMetadata({ + await uploadMetadata__NEW({ ...client, ...packDirectData({ content: idl }), payer: authority, From 7b2723be1cd252b6fbc5371622913d3b717b677f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 09:08:59 +0100 Subject: [PATCH 104/112] wip --- clients/js/src/createMetadata.ts | 148 ----------------- clients/js/src/updateMetadata.ts | 266 ------------------------------- 2 files changed, 414 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 33d3a06..33e4153 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -1,6 +1,5 @@ import { getTransferSolInstruction } from '@solana-program/system'; import { - CompilableTransactionMessage, GetMinimumBalanceForRentExemptionApi, Lamports, ReadonlyUint8Array, @@ -13,11 +12,6 @@ import { InitializeInput, PROGRAM_METADATA_PROGRAM_ADDRESS, } from './generated'; -import { - getTransactionMessageFromPlan, - InstructionPlan, - MessageInstructionPlan, -} from './instructionPlans'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, @@ -25,159 +19,17 @@ import { sequentialInstructionPlan, } from './instructionPlansDraft'; import { - calculateMaxChunkSize, - getComputeUnitInstructions, - getExtendInstructionPlan, getExtendInstructionPlan__NEW, getPdaDetails, - getWriteInstructionPlan, getWriteInstructionPlan__NEW, - messageFitsInOneTransaction, - PdaDetails, REALLOC_LIMIT, } from './internals'; import { getAccountSize, - MetadataInput, MetadataInput__NEW, MetadataResponse__NEW, } from './utils'; -export async function getCreateMetadataInstructionPlan( - input: Omit & - PdaDetails & { - rpc: Rpc; - defaultMessage: CompilableTransactionMessage; - } -): Promise { - const rent = await input.rpc - .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) - .send(); - const planUsingInstructionData = - getCreateMetadataInstructionPlanUsingInstructionData({ ...input, rent }); - const messageUsingInstructionData = getTransactionMessageFromPlan( - input.defaultMessage, - planUsingInstructionData - ); - const useBuffer = - input.buffer === undefined - ? !messageFitsInOneTransaction(messageUsingInstructionData) - : !!input.buffer; - - if (!useBuffer) { - return planUsingInstructionData; - } - - const chunkSize = calculateMaxChunkSize(input.defaultMessage, { - ...input, - buffer: input.metadata, - }); - return getCreateMetadataInstructionPlanUsingBuffer({ - ...input, - chunkSize, - rent, - }); -} - -export function getCreateMetadataInstructionPlanUsingInstructionData( - input: Omit & - PdaDetails & { rent: Lamports } -): MessageInstructionPlan { - return { - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: input.priorityFees, - computeUnitLimit: 'simulated', - }), - getTransferSolInstruction({ - source: input.payer, - destination: input.metadata, - amount: input.rent, - }), - getInitializeInstruction({ - ...input, - programData: input.isCanonical ? input.programData : undefined, - }), - ], - }; -} - -export function getCreateMetadataInstructionPlanUsingBuffer( - input: Omit & - PdaDetails & { rent: Lamports; chunkSize: number } -): InstructionPlan { - const mainPlan: InstructionPlan = { kind: 'sequential', plans: [] }; - - mainPlan.plans.push({ - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: input.priorityFees, - computeUnitLimit: 'simulated', - }), - getTransferSolInstruction({ - source: input.payer, - destination: input.metadata, - amount: input.rent, - }), - getAllocateInstruction({ - buffer: input.metadata, - authority: input.authority, - program: input.program, - programData: input.isCanonical ? input.programData : undefined, - seed: input.seed, - }), - ], - }); - - if (input.data.length > REALLOC_LIMIT) { - mainPlan.plans.push( - getExtendInstructionPlan({ - account: input.metadata, - authority: input.authority, - extraLength: input.data.length, - program: input.program, - programData: input.isCanonical ? input.programData : undefined, - }) - ); - } - - let offset = 0; - const writePlan: InstructionPlan = { kind: 'parallel', plans: [] }; - while (offset < input.data.length) { - writePlan.plans.push( - getWriteInstructionPlan({ - buffer: input.metadata, - authority: input.authority, - offset, - data: input.data.slice(offset, offset + input.chunkSize), - priorityFees: input.priorityFees, - }) - ); - offset += input.chunkSize; - } - mainPlan.plans.push(writePlan); - - mainPlan.plans.push({ - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: input.priorityFees, - computeUnitLimit: 'simulated', - }), - getInitializeInstruction({ - ...input, - programData: input.isCanonical ? input.programData : undefined, - system: PROGRAM_METADATA_PROGRAM_ADDRESS, - data: undefined, - }), - ], - }); - - return mainPlan; -} - export async function createMetadata__NEW( input: MetadataInput__NEW & { rpc: Rpc & diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index 11a1f67..453aacc 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -3,11 +3,9 @@ import { getTransferSolInstruction, } from '@solana-program/system'; import { - CompilableTransactionMessage, generateKeyPairSigner, GetAccountInfoApi, GetMinimumBalanceForRentExemptionApi, - isTransactionSigner, lamports, Lamports, ReadonlyUint8Array, @@ -24,11 +22,6 @@ import { PROGRAM_METADATA_PROGRAM_ADDRESS, SetDataInput, } from './generated'; -import { - getTransactionMessageFromPlan, - InstructionPlan, - MessageInstructionPlan, -} from './instructionPlans'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, @@ -36,276 +29,17 @@ import { sequentialInstructionPlan, } from './instructionPlansDraft'; import { - calculateMaxChunkSize, - getComputeUnitInstructions, - getExtendedMetadataInput, - getExtendInstructionPlan, getExtendInstructionPlan__NEW, - getMetadataInstructionPlanExecutor, getPdaDetails, - getWriteInstructionPlan, getWriteInstructionPlan__NEW, - messageFitsInOneTransaction, - PdaDetails, REALLOC_LIMIT, } from './internals'; import { getAccountSize, - MetadataInput, MetadataInput__NEW, - MetadataResponse, MetadataResponse__NEW, } from './utils'; -export async function updateMetadata( - input: MetadataInput -): Promise { - const extendedInput = await getExtendedMetadataInput(input); - const executor = getMetadataInstructionPlanExecutor(extendedInput); - const metadataAccount = await fetchMetadata( - input.rpc, - extendedInput.metadata - ); - if (!metadataAccount.data.mutable) { - throw new Error('Metadata account is immutable'); - } - const plan = await getUpdateMetadataInstructionPlan({ - ...extendedInput, - currentDataLength: BigInt(metadataAccount.data.data.length), - }); - return await executor(plan); -} - -export async function getUpdateMetadataInstructionPlan( - input: Omit & - PdaDetails & { - rpc: Rpc; - currentDataLength: bigint; - defaultMessage: CompilableTransactionMessage; - } -): Promise { - const newDataLength = BigInt(input.data.length); - const sizeDifference = newDataLength - BigInt(input.currentDataLength); - const extraRent = - sizeDifference > 0 - ? await input.rpc.getMinimumBalanceForRentExemption(sizeDifference).send() - : lamports(0n); - const planUsingInstructionData = - getUpdateMetadataInstructionPlanUsingInstructionData({ - ...input, - sizeDifference, - extraRent, - }); - const messageUsingInstructionData = getTransactionMessageFromPlan( - input.defaultMessage, - planUsingInstructionData - ); - const useBuffer = - input.buffer === undefined - ? !messageFitsInOneTransaction(messageUsingInstructionData) - : !!input.buffer; - - if (!useBuffer) { - return planUsingInstructionData; - } - - const newAccountSize = getAccountSize(newDataLength); - const [buffer, bufferRent] = await Promise.all([ - typeof input.buffer === 'object' && isTransactionSigner(input.buffer) - ? Promise.resolve(input.buffer) - : generateKeyPairSigner(), - input.rpc.getMinimumBalanceForRentExemption(newAccountSize).send(), - ]); - const chunkSize = calculateMaxChunkSize(input.defaultMessage, { - ...input, - buffer: buffer.address, - authority: buffer, - }); - return getUpdateMetadataInstructionPlanUsingBuffer({ - ...input, - sizeDifference, - extraRent, - bufferRent, - buffer, - chunkSize, - }); -} - -export function getUpdateMetadataInstructionPlanUsingInstructionData( - input: Omit & - PdaDetails & { - sizeDifference: bigint; - extraRent: Lamports; - } -): MessageInstructionPlan { - const plan: InstructionPlan = { - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: input.priorityFees, - computeUnitLimit: 'simulated', - }), - ], - }; - - if (input.sizeDifference > 0) { - plan.instructions.push( - getTransferSolInstruction({ - source: input.payer, - destination: input.metadata, - amount: input.extraRent, - }) - ); - } - - plan.instructions.push( - getSetDataInstruction({ - ...input, - programData: input.isCanonical ? input.programData : undefined, - buffer: undefined, - }) - ); - - if (input.sizeDifference < 0) { - plan.instructions.push( - getTrimInstruction({ - account: input.metadata, - authority: input.authority, - destination: input.payer.address, - program: input.program, - programData: input.isCanonical ? input.programData : undefined, - }) - ); - } - - return plan; -} - -export function getUpdateMetadataInstructionPlanUsingBuffer( - input: Omit & - PdaDetails & { - chunkSize: number; - sizeDifference: bigint; - extraRent: Lamports; - bufferRent: Lamports; - buffer: TransactionSigner; - } -): InstructionPlan { - const mainPlan: InstructionPlan = { kind: 'sequential', plans: [] }; - const initialMessage: InstructionPlan = { - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: input.priorityFees, - computeUnitLimit: 'simulated', - }), - ], - }; - - if (input.sizeDifference > 0) { - initialMessage.instructions.push( - getTransferSolInstruction({ - source: input.payer, - destination: input.metadata, - amount: input.extraRent, - }) - ); - } - - initialMessage.instructions.push( - getCreateAccountInstruction({ - payer: input.payer, - newAccount: input.buffer, - lamports: input.bufferRent, - space: getAccountSize(input.data.length), - programAddress: PROGRAM_METADATA_PROGRAM_ADDRESS, - }), - getAllocateInstruction({ - buffer: input.buffer.address, - authority: input.buffer, - }), - getSetAuthorityInstruction({ - account: input.buffer.address, - authority: input.buffer, - newAuthority: input.authority.address, - }) - ); - mainPlan.plans.push(initialMessage); - - if (input.sizeDifference > REALLOC_LIMIT) { - mainPlan.plans.push( - getExtendInstructionPlan({ - account: input.metadata, - authority: input.authority, - extraLength: Number(input.sizeDifference), - program: input.program, - programData: input.isCanonical ? input.programData : undefined, - priorityFees: input.priorityFees, - }) - ); - } - - let offset = 0; - const writePlan: InstructionPlan = { kind: 'parallel', plans: [] }; - while (offset < input.data.length) { - writePlan.plans.push( - getWriteInstructionPlan({ - buffer: input.buffer.address, - authority: input.authority, - offset, - data: input.data.slice(offset, offset + input.chunkSize), - priorityFees: input.priorityFees, - }) - ); - offset += input.chunkSize; - } - mainPlan.plans.push(writePlan); - - const finalizeMessage: InstructionPlan = { - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: input.priorityFees, - computeUnitLimit: 'simulated', - }), - getSetDataInstruction({ - ...input, - buffer: input.buffer.address, - programData: input.isCanonical ? input.programData : undefined, - data: undefined, - }), - ], - }; - - if (input.closeBuffer) { - finalizeMessage.instructions.push( - getCloseInstruction({ - account: input.buffer.address, - authority: input.authority, - destination: input.payer.address, - program: input.program, - programData: input.isCanonical ? input.programData : undefined, - }) - ); - } - - if (input.sizeDifference < 0) { - finalizeMessage.instructions.push( - getTrimInstruction({ - account: input.metadata, - authority: input.authority, - destination: input.payer.address, - program: input.program, - programData: input.isCanonical ? input.programData : undefined, - }) - ); - } - - mainPlan.plans.push(finalizeMessage); - - return mainPlan; -} - export async function updateMetadata__NEW( input: MetadataInput__NEW & { rpc: Rpc & From db69ec4081bf71397c8225d1207aed87ec83b26f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 09:34:53 +0100 Subject: [PATCH 105/112] wip --- clients/js/src/cli.ts | 251 ++++++++++++++++++++++-------------------- 1 file changed, 131 insertions(+), 120 deletions(-) diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 33499d8..0075638 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -36,11 +36,15 @@ import { getSetImmutableInstruction, } from './generated'; import { - getComputeUnitInstructions, - getDefaultMessageFactory, - getMetadataInstructionPlanExecutor, - getPdaDetails, -} from './internals'; + createDefaultTransactionPlanExecutor, + createDefaultTransactionPlanner, + InstructionPlan, + sequentialInstructionPlan, + TransactionPlanExecutor, + TransactionPlanner, + TransactionPlanResult, +} from './instructionPlansDraft'; +import { getPdaDetails } from './internals'; import { packDirectData, PackedData, @@ -165,13 +169,15 @@ program cmd: Command ) => { const options = cmd.optsWithGlobals() as UploadOptions; - const client = getClient(options); - const [keypair, payer] = await getKeyPairSigners(options, client.configs); + const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( client.rpc, program ); - if (!options.nonCanonical && keypair.address !== programAuthority) { + if ( + !options.nonCanonical && + client.authority.address !== programAuthority + ) { logErrorAndExit( 'You must be the program authority to upload a canonical metadata account. Use `--non-canonical` option to upload as a third party.' ); @@ -179,8 +185,8 @@ program await uploadMetadata__NEW({ ...client, ...getPackedData(content, options), - payer, - authority: keypair, + payer: client.payer, + authority: client.authority, program, seed, format: getFormat(options), @@ -231,7 +237,7 @@ program ) .action(async (seed: string, program: Address, _, cmd: Command) => { const options = cmd.optsWithGlobals() as DownloadOptions; - const client = getClient(options); + const client = getReadonlyClient(options); const authority = options.nonCanonical === true ? (await getKeyPairSigners(options, client.configs))[0].address @@ -279,31 +285,24 @@ program cmd: Command ) => { const options = cmd.optsWithGlobals() as GlobalOptions; - const client = getClient(options); - const [keypair, payer] = await getKeyPairSigners(options, client.configs); + const client = await getClient(options); const { metadata, programData } = await getPdaDetails({ rpc: client.rpc, program, - authority: keypair, + authority: client.authority, seed, }); - const planExecutor = await getCliPlanExecutor(client, payer, metadata); - await planExecutor({ - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: options.priorityFees, - computeUnitLimit: 'simulated', - }), + await client.planAndExecute( + sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, - authority: keypair, + authority: client.authority, newAuthority: address(newAuthority), program, programData, }), - ], - }); + ]) + ); logSuccess( `Additional authority successfully set to ${chalk.bold(newAuthority)}` ); @@ -321,31 +320,24 @@ program .description('Remove the additional authority on canonical metadata accounts') .action(async (seed: string, program: Address, _, cmd: Command) => { const options = cmd.optsWithGlobals() as GlobalOptions; - const client = getClient(options); - const [keypair, payer] = await getKeyPairSigners(options, client.configs); + const client = await getClient(options); const { metadata, programData } = await getPdaDetails({ rpc: client.rpc, program, - authority: keypair, + authority: client.authority, seed, }); - const planExecutor = await getCliPlanExecutor(client, payer, metadata); - await planExecutor({ - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: options.priorityFees, - computeUnitLimit: 'simulated', - }), + await client.planAndExecute( + sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, - authority: keypair, + authority: client.authority, newAuthority: null, program, programData, }), - ], - }); + ]) + ); logSuccess('Additional authority successfully removed'); }); @@ -370,13 +362,15 @@ program ) .action(async (seed: string, program: Address, _, cmd: Command) => { const options = cmd.optsWithGlobals() as SetImmutableOptions; - const client = getClient(options); - const [keypair, payer] = await getKeyPairSigners(options, client.configs); + const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( client.rpc, program ); - if (!options.nonCanonical && keypair.address !== programAuthority) { + if ( + !options.nonCanonical && + client.authority.address !== programAuthority + ) { logErrorAndExit( 'You must be the program authority to update a canonical metadata account. Use `--non-canonical` option to update as a third party.' ); @@ -384,25 +378,19 @@ program const { metadata, programData } = await getPdaDetails({ rpc: client.rpc, program, - authority: keypair, + authority: client.authority, seed, }); - const planExecutor = await getCliPlanExecutor(client, payer, metadata); - await planExecutor({ - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: options.priorityFees, - computeUnitLimit: 'simulated', - }), + await client.planAndExecute( + sequentialInstructionPlan([ getSetImmutableInstruction({ metadata, - authority: keypair, + authority: client.authority, program, programData, }), - ], - }); + ]) + ); logSuccess('Metadata account successfully set as immutable'); }); @@ -425,13 +413,15 @@ program ) .action(async (seed: string, program: Address, _, cmd: Command) => { const options = cmd.optsWithGlobals() as CloseOptions; - const client = getClient(options); - const [keypair, payer] = await getKeyPairSigners(options, client.configs); + const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( client.rpc, program ); - if (!options.nonCanonical && keypair.address !== programAuthority) { + if ( + !options.nonCanonical && + client.authority.address !== programAuthority + ) { logErrorAndExit( 'You must be the program authority to close a canonical metadata account. Use `--non-canonical` option to close as a third party.' ); @@ -439,26 +429,20 @@ program const { metadata, programData } = await getPdaDetails({ rpc: client.rpc, program, - authority: keypair, + authority: client.authority, seed, }); - const planExecutor = await getCliPlanExecutor(client, payer, metadata); - await planExecutor({ - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: options.priorityFees, - computeUnitLimit: 'simulated', - }), + await client.planAndExecute( + sequentialInstructionPlan([ getCloseInstruction({ account: metadata, - authority: keypair, + authority: client.authority, program, programData, - destination: payer.address, + destination: client.payer.address, }), - ], - }); + ]) + ); logSuccess('Account successfully closed and rent recovered'); }); @@ -476,71 +460,66 @@ program // TODO }); -async function getKeyPairSigners( - options: { keypair?: string; payer?: string }, - configs: SolanaConfigs -): Promise<[KeyPairSigner, KeyPairSigner]> { - const keypairPath = getKeyPairPath(options, configs); - const keypairPromise = getKeyPairSignerFromPath(keypairPath); - const payerPromise = options.payer - ? getKeyPairSignerFromPath(options.payer) - : keypairPromise; - return await Promise.all([keypairPromise, payerPromise]); -} - -function getKeyPairPath( - options: { keypair?: string }, - configs: SolanaConfigs -): string { - if (options.keypair) return options.keypair; - if (configs.keypair_path) return configs.keypair_path; - return path.join(os.homedir(), '.config', 'solana', 'id.json'); -} - -async function getKeyPairSignerFromPath( - keypairPath: string -): Promise { - if (!fs.existsSync(keypairPath)) { - logErrorAndExit(`Keypair file not found at: ${keypairPath}`); - } - const keypairString = fs.readFileSync(keypairPath, 'utf-8'); - const keypairData = new Uint8Array(JSON.parse(keypairString)); - return await createKeyPairSignerFromBytes(keypairData); -} +type Client = ReadonlyClient & { + authority: KeyPairSigner; + executor: TransactionPlanExecutor; + payer: KeyPairSigner; + planAndExecute: ( + instructionPlan: InstructionPlan + ) => Promise; + planner: TransactionPlanner; +}; -async function getCliPlanExecutor( - client: Client, - payer: KeyPairSigner, - metadata: Address -) { - const getDefaultMessage = getDefaultMessageFactory({ - rpc: client.rpc, - payer, +async function getClient(options: { + keypair?: string; + payer?: string; + priorityFees?: MicroLamports; + rpc?: string; +}): Promise { + const readonlyClient = getReadonlyClient(options); + const [authority, payer] = await getKeyPairSigners( + options, + readonlyClient.configs + ); + const planner = createDefaultTransactionPlanner({ + feePayer: payer, + computeUnitPrice: options.priorityFees, }); - const [defaultMessage] = await Promise.all([getDefaultMessage()]); - return getMetadataInstructionPlanExecutor({ - ...client, - getDefaultMessage, - payer, - metadata, - defaultMessage, + const executor = createDefaultTransactionPlanExecutor({ + rpc: readonlyClient.rpc, + rpcSubscriptions: readonlyClient.rpcSubscriptions, + parallelChunkSize: 5, }); + const planAndExecute = async ( + instructionPlan: InstructionPlan + ): Promise => { + const transactionPlan = await planner(instructionPlan); + return await executor(transactionPlan); + }; + return { + ...readonlyClient, + authority, + executor, + payer, + planAndExecute, + planner, + }; } -type Client = { +type ReadonlyClient = { + configs: SolanaConfigs; rpc: Rpc; rpcSubscriptions: RpcSubscriptions; - configs: SolanaConfigs; }; -function getClient(options: { rpc?: string }): Client { +function getReadonlyClient(options: { rpc?: string }): ReadonlyClient { const configs = getSolanaConfigs(); const rpcUrl = getRpcUrl(options, configs); const rpcSubscriptionsUrl = getRpcSubscriptionsUrl(rpcUrl, configs); return { + configs, rpc: createSolanaRpc(rpcUrl), rpcSubscriptions: createSolanaRpcSubscriptions(rpcSubscriptionsUrl), - configs, }; } @@ -579,6 +558,38 @@ function getSolanaConfigPath(): string { return path.join(os.homedir(), '.config', 'solana', 'cli', 'config.yml'); } +async function getKeyPairSigners( + options: { keypair?: string; payer?: string }, + configs: SolanaConfigs +): Promise<[KeyPairSigner, KeyPairSigner]> { + const keypairPath = getKeyPairPath(options, configs); + const keypairPromise = getKeyPairSignerFromPath(keypairPath); + const payerPromise = options.payer + ? getKeyPairSignerFromPath(options.payer) + : keypairPromise; + return await Promise.all([keypairPromise, payerPromise]); +} + +function getKeyPairPath( + options: { keypair?: string }, + configs: SolanaConfigs +): string { + if (options.keypair) return options.keypair; + if (configs.keypair_path) return configs.keypair_path; + return path.join(os.homedir(), '.config', 'solana', 'id.json'); +} + +async function getKeyPairSignerFromPath( + keypairPath: string +): Promise { + if (!fs.existsSync(keypairPath)) { + logErrorAndExit(`Keypair file not found at: ${keypairPath}`); + } + const keypairString = fs.readFileSync(keypairPath, 'utf-8'); + const keypairData = new Uint8Array(JSON.parse(keypairString)); + return await createKeyPairSignerFromBytes(keypairData); +} + function getCompression(options: { compression?: string }): Compression { switch (options.compression) { case 'none': From 760d9d9f7e3dd05bc9c3d8b45a7c7ad5111bc7b2 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 09:42:25 +0100 Subject: [PATCH 106/112] wip --- .../defaultInstructionPlanExecutor.ts | 180 --------------- clients/js/src/instructionPlans/index.ts | 3 - .../src/instructionPlans/instructionPlan.ts | 39 ---- .../instructionPlanExecutor.ts | 71 ------ clients/js/src/internals.ts | 217 +----------------- 5 files changed, 1 insertion(+), 509 deletions(-) delete mode 100644 clients/js/src/instructionPlans/defaultInstructionPlanExecutor.ts delete mode 100644 clients/js/src/instructionPlans/index.ts delete mode 100644 clients/js/src/instructionPlans/instructionPlan.ts delete mode 100644 clients/js/src/instructionPlans/instructionPlanExecutor.ts diff --git a/clients/js/src/instructionPlans/defaultInstructionPlanExecutor.ts b/clients/js/src/instructionPlans/defaultInstructionPlanExecutor.ts deleted file mode 100644 index 71c4aa7..0000000 --- a/clients/js/src/instructionPlans/defaultInstructionPlanExecutor.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { - COMPUTE_BUDGET_PROGRAM_ADDRESS, - ComputeBudgetInstruction, - getSetComputeUnitLimitInstruction, - identifyComputeBudgetInstruction, -} from '@solana-program/compute-budget'; -import { - Commitment, - CompilableTransactionMessage, - compileTransaction, - FullySignedTransaction, - getBase64EncodedWireTransaction, - GetEpochInfoApi, - GetSignatureStatusesApi, - pipe, - Rpc, - RpcSubscriptions, - sendAndConfirmTransactionFactory, - SendTransactionApi, - SignatureNotificationsApi, - signTransactionMessageWithSigners, - SimulateTransactionApi, - SlotNotificationsApi, - TransactionMessageWithBlockhashLifetime, - TransactionMessageWithDurableNonceLifetime, - TransactionWithBlockhashLifetime, -} from '@solana/kit'; -import { - getTransactionMessageFromPlan, - MessageInstructionPlan, -} from './instructionPlan'; -import { - chunkParallelInstructionPlans, - createInstructionPlanExecutor, - InstructionPlanExecutor, -} from './instructionPlanExecutor'; - -export type DefaultInstructionPlanExecutorConfig = Readonly<{ - rpc: Rpc< - GetEpochInfoApi & - GetSignatureStatusesApi & - SendTransactionApi & - SimulateTransactionApi - >; - - rpcSubscriptions: RpcSubscriptions< - SignatureNotificationsApi & SlotNotificationsApi - >; - - /** - * The commitment to use when confirming transactions. - */ - commitment?: Commitment; - - /** - * When provided, chunks the plans inside a {@link ParallelInstructionPlan}. - * Each chunk is executed sequentially but each plan within a chunk is - * executed in parallel. - */ - parallelChunkSize?: number; - - /** - * If true _and_ if the transaction message contains an instruction - * that sets the compute unit limit to any value, the executor will - * simulate the transaction to determine the optimal compute unit limit - * before updating the compute budget instruction with the computed value. - */ - simulateComputeUnitLimit?: boolean; - - /** - * Returns the default transaction message used to send transactions. - * Any instructions inside a {@link MessageInstructionPlan} will be - * appended to this message. - */ - getDefaultMessage: (config?: { - abortSignal?: AbortSignal; - }) => Promise< - CompilableTransactionMessage & - ( - | TransactionMessageWithBlockhashLifetime - | TransactionMessageWithDurableNonceLifetime - ) - >; -}>; - -export function getDefaultInstructionPlanExecutor( - config: DefaultInstructionPlanExecutorConfig -): InstructionPlanExecutor { - const { - rpc, - commitment, - getDefaultMessage, - parallelChunkSize: chunkSize, - simulateComputeUnitLimit: shouldSimulateComputeUnitLimit, - } = config; - const sendAndConfirm = sendAndConfirmTransactionFactory(config); - - return async (plan, config) => { - const handleMessage = async (plan: MessageInstructionPlan) => { - const defaultMessage = await getDefaultMessage(config); - let message = getTransactionMessageFromPlan(defaultMessage, plan); - - if (shouldSimulateComputeUnitLimit) { - message = await setComputeUnitLimitBySimulatingTransaction( - message, - rpc - ); - } - - const tx = (await signTransactionMessageWithSigners( - message - )) as FullySignedTransaction & TransactionWithBlockhashLifetime; - await sendAndConfirm(tx, { - ...config, - commitment: commitment ?? 'confirmed', - skipPreflight: shouldSimulateComputeUnitLimit, - }); - }; - - const executor = pipe(createInstructionPlanExecutor(handleMessage), (e) => - chunkSize ? chunkParallelInstructionPlans(e, chunkSize) : e - ); - - return await executor(plan, config); - }; -} - -async function setComputeUnitLimitBySimulatingTransaction< - TTransactionMessage extends - CompilableTransactionMessage = CompilableTransactionMessage, ->( - message: TTransactionMessage, - rpc: Rpc -): Promise { - const instructionIndex = message.instructions.findIndex((instruction) => { - return ( - instruction.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS && - identifyComputeBudgetInstruction(instruction.data as Uint8Array) === - ComputeBudgetInstruction.SetComputeUnitLimit - ); - }); - - // Ignore if no compute unit limit instruction is found. - if (instructionIndex === -1) { - return message; - } - - const limit = await getComputeUnitLimitBySimulatingTransaction(message, rpc); - - // Ignore if the limit is not found. - if (limit === undefined) { - return message; - } - - return Object.freeze({ - ...message, - instructions: [ - ...message.instructions.slice(0, instructionIndex), - getSetComputeUnitLimitInstruction({ - // Use a 1.1x multiplier to the computed limit. - units: Number((limit * 110n) / 100n), - }), - ...message.instructions.slice(instructionIndex + 1), - ], - } as TTransactionMessage); -} - -async function getComputeUnitLimitBySimulatingTransaction< - TTransactionMessage extends - CompilableTransactionMessage = CompilableTransactionMessage, ->( - message: TTransactionMessage, - rpc: Rpc -): Promise { - const tx = getBase64EncodedWireTransaction(compileTransaction(message)); - const result = await rpc - .simulateTransaction(tx, { encoding: 'base64' }) - .send(); - return result.value.unitsConsumed; -} diff --git a/clients/js/src/instructionPlans/index.ts b/clients/js/src/instructionPlans/index.ts deleted file mode 100644 index 3aa870b..0000000 --- a/clients/js/src/instructionPlans/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './defaultInstructionPlanExecutor'; -export * from './instructionPlan'; -export * from './instructionPlanExecutor'; diff --git a/clients/js/src/instructionPlans/instructionPlan.ts b/clients/js/src/instructionPlans/instructionPlan.ts deleted file mode 100644 index 47da486..0000000 --- a/clients/js/src/instructionPlans/instructionPlan.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - appendTransactionMessageInstructions, - BaseTransactionMessage, - IInstruction, -} from '@solana/kit'; - -export type InstructionPlan = - | SequentialInstructionPlan - | ParallelInstructionPlan - | MessageInstructionPlan; - -export type SequentialInstructionPlan = Readonly<{ - kind: 'sequential'; - plans: InstructionPlan[]; -}>; - -export type ParallelInstructionPlan = Readonly<{ - kind: 'parallel'; - plans: InstructionPlan[]; -}>; - -export type MessageInstructionPlan< - TInstructions extends IInstruction[] = IInstruction[], -> = Readonly<{ - kind: 'message'; - instructions: TInstructions; -}>; - -export function getTransactionMessageFromPlan< - TTransactionMessage extends BaseTransactionMessage = BaseTransactionMessage, ->( - defaultMessage: TTransactionMessage, - plan: MessageInstructionPlan -): TTransactionMessage { - return appendTransactionMessageInstructions( - plan.instructions, - defaultMessage - ); -} diff --git a/clients/js/src/instructionPlans/instructionPlanExecutor.ts b/clients/js/src/instructionPlans/instructionPlanExecutor.ts deleted file mode 100644 index 1a261c1..0000000 --- a/clients/js/src/instructionPlans/instructionPlanExecutor.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - InstructionPlan, - MessageInstructionPlan, - ParallelInstructionPlan, -} from './instructionPlan'; - -export type InstructionPlanExecutor = ( - plan: InstructionPlan, - config?: { abortSignal?: AbortSignal } -) => Promise; - -export function createInstructionPlanExecutor( - handleMessage: ( - plan: MessageInstructionPlan, - config?: { abortSignal?: AbortSignal } - ) => Promise -): InstructionPlanExecutor { - return async function self(plan, config) { - switch (plan.kind) { - case 'sequential': - for (const subPlan of plan.plans) { - await self(subPlan, config); - } - break; - case 'parallel': - await Promise.all(plan.plans.map((subPlan) => self(subPlan, config))); - break; - case 'message': - return await handleMessage(plan, config); - default: - throw new Error('Unsupported instruction plan'); - } - }; -} - -export function chunkParallelInstructionPlans( - executor: InstructionPlanExecutor, - chunkSize: number -): InstructionPlanExecutor { - const chunkPlan = ( - plan: ParallelInstructionPlan - ): ParallelInstructionPlan[] => { - return plan.plans - .reduce( - (chunks, subPlan) => { - const lastChunk = chunks[chunks.length - 1]; - if (lastChunk && lastChunk.length < chunkSize) { - lastChunk.push(subPlan); - } else { - chunks.push([subPlan]); - } - return chunks; - }, - [[]] as InstructionPlan[][] - ) - .map((plans) => ({ kind: 'parallel', plans }) as ParallelInstructionPlan); - }; - return async function self(plan, config) { - switch (plan.kind) { - case 'sequential': - return await self(plan, config); - case 'parallel': - for (const chunk of chunkPlan(plan)) { - await executor(chunk, config); - } - break; - default: - return await executor(plan, config); - } - }; -} diff --git a/clients/js/src/internals.ts b/clients/js/src/internals.ts index fe7577f..79c61c4 100644 --- a/clients/js/src/internals.ts +++ b/clients/js/src/internals.ts @@ -4,13 +4,10 @@ import { } from '@solana-program/compute-budget'; import { Address, - Commitment, CompilableTransactionMessage, - compileTransaction, createTransactionMessage, GetAccountInfoApi, GetLatestBlockhashApi, - getTransactionEncoder, IInstruction, MicroLamports, pipe, @@ -18,31 +15,21 @@ import { Rpc, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, - Transaction, TransactionMessageWithBlockhashLifetime, TransactionSigner, } from '@solana/kit'; import { - ExtendInstruction, findMetadataPda, getExtendInstruction, getWriteInstruction, SeedArgs, } from './generated'; -import { - getDefaultInstructionPlanExecutor, - getTransactionMessageFromPlan, - InstructionPlan, - InstructionPlanExecutor, - MessageInstructionPlan, -} from './instructionPlans'; -import { getProgramAuthority, MetadataInput, MetadataResponse } from './utils'; import { getLinearIterableInstructionPlan, getReallocIterableInstructionPlan, IterableInstructionPlan, - TRANSACTION_SIZE_LIMIT, } from './instructionPlansDraft'; +import { getProgramAuthority, MetadataInput } from './utils'; export const REALLOC_LIMIT = 10_240; @@ -163,92 +150,6 @@ export function getComputeUnitInstructions(input: { return instructions; } -export function calculateMaxChunkSize( - defaultMessage: CompilableTransactionMessage, - input: { - buffer: Address; - authority: TransactionSigner; - priorityFees?: MicroLamports; - } -) { - const plan = getWriteInstructionPlan({ - ...input, - offset: 0, - data: new Uint8Array(0), - }); - const message = getTransactionMessageFromPlan(defaultMessage, plan); - return getRemainingTransactionSpaceFromMessage(message); -} - -export function messageFitsInOneTransaction( - message: CompilableTransactionMessage -): boolean { - return getRemainingTransactionSpaceFromMessage(message) >= 0; -} - -function getRemainingTransactionSpaceFromMessage( - message: CompilableTransactionMessage -) { - return ( - TRANSACTION_SIZE_LIMIT - - getTransactionSizeFromMessage(message) - - 1 /* Subtract 1 byte buffer to account for shortvec encoding. */ - ); -} - -function getTransactionSizeFromMessage( - message: CompilableTransactionMessage -): number { - const transaction = compileTransaction(message); - return getTransactionEncoder().encode(transaction).length; -} - -export function getExtendInstructionPlan(input: { - account: Address; - authority: TransactionSigner; - extraLength: number; - priorityFees?: MicroLamports; - program?: Address; - programData?: Address; -}): InstructionPlan { - const plans: MessageInstructionPlan[] = []; - const extendsPerTransaction = 50; - let chunkOffset = 0; - - while (chunkOffset < input.extraLength) { - const extendInstructions: ExtendInstruction[] = []; - let offset = chunkOffset; - - while (offset < input.extraLength) { - const length = Math.min(input.extraLength - offset, REALLOC_LIMIT); - extendInstructions.push( - getExtendInstruction({ - account: input.account, - authority: input.authority, - length, - program: input.program, - programData: input.programData, - }) - ); - offset += length; - } - - plans.push({ - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: input.priorityFees, - computeUnitLimit: 'simulated', - }), - ...extendInstructions, - ], - }); - chunkOffset += REALLOC_LIMIT * extendsPerTransaction; - } - - return { kind: 'parallel', plans }; -} - export function getExtendInstructionPlan__NEW(input: { account: Address; authority: TransactionSigner; @@ -269,25 +170,6 @@ export function getExtendInstructionPlan__NEW(input: { }); } -export function getWriteInstructionPlan(input: { - buffer: Address; - authority: TransactionSigner; - offset: number; - data: ReadonlyUint8Array; - priorityFees?: MicroLamports; -}): MessageInstructionPlan { - return { - kind: 'message', - instructions: [ - ...getComputeUnitInstructions({ - computeUnitPrice: input.priorityFees, - computeUnitLimit: 'simulated', - }), - getWriteInstruction({ ...input }), - ], - }; -} - export function getWriteInstructionPlan__NEW(input: { buffer: Address; authority: TransactionSigner; @@ -304,100 +186,3 @@ export function getWriteInstructionPlan__NEW(input: { }), }); } - -export function getMetadataInstructionPlanExecutor( - input: Pick< - ExtendedMetadataInput, - | 'rpc' - | 'rpcSubscriptions' - | 'payer' - | 'extractLastTransaction' - | 'metadata' - | 'defaultMessage' - | 'getDefaultMessage' - > & { commitment?: Commitment } -): ( - plan: InstructionPlan, - config?: { abortSignal?: AbortSignal } -) => Promise { - const executor = getDefaultInstructionPlanExecutor({ - ...input, - simulateComputeUnitLimit: true, - getDefaultMessage: input.getDefaultMessage, - }); - - return async (plan, config) => { - const [planToSend, lastTransaction] = extractLastTransactionIfRequired( - plan, - input - ); - await executor(planToSend, config); - return { metadata: input.metadata, lastTransaction }; - }; -} - -export async function executeInstructionPlanAndGetMetadataResponse( - plan: InstructionPlan, - executor: InstructionPlanExecutor, - input: { - metadata: Address; - defaultMessage: CompilableTransactionMessage; - extractLastTransaction?: boolean; - } -): Promise { - const [planToSend, lastTransaction] = extractLastTransactionIfRequired( - plan, - input - ); - await executor(planToSend); - return { metadata: input.metadata, lastTransaction }; -} - -function extractLastTransactionIfRequired( - plan: InstructionPlan, - input: { - defaultMessage: CompilableTransactionMessage; - extractLastTransaction?: boolean; - } -): [InstructionPlan, Transaction | undefined] { - if (!input.extractLastTransaction) { - return [plan, undefined]; - } - const result = extractLastMessageFromPlan(plan); - const lastMessage = getTransactionMessageFromPlan( - input.defaultMessage, - result.lastMessage - ); - return [result.plan, compileTransaction(lastMessage)]; -} - -function extractLastMessageFromPlan(plan: InstructionPlan): { - plan: InstructionPlan; - lastMessage: MessageInstructionPlan; -} { - switch (plan.kind) { - case 'sequential': - // eslint-disable-next-line no-case-declarations - const lastMessage = plan.plans[plan.plans.length - 1]; - if (lastMessage.kind !== 'message') { - throw Error( - `Expected last plan to be a message plan, got: ${lastMessage.kind}` - ); - } - return { - plan: { kind: 'sequential', plans: plan.plans.slice(0, -1) }, - lastMessage, - }; - case 'message': - throw new Error( - 'This operation can be executed without a buffer. ' + - 'Therefore, the `extractLastTransaction` option is redundant. ' + - 'Use the `buffer` option to force the use of a buffer.' - ); - case 'parallel': - default: - throw Error( - `Cannot extract last transaction from plan kind: "${plan.kind}"` - ); - } -} From bd8e7a54e9ddf70efc4404b2f98340ab6f2b39c8 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 09:46:39 +0100 Subject: [PATCH 107/112] wip --- clients/js/src/internals.ts | 103 +----------------------------------- clients/js/src/utils.ts | 63 ---------------------- 2 files changed, 1 insertion(+), 165 deletions(-) diff --git a/clients/js/src/internals.ts b/clients/js/src/internals.ts index 79c61c4..99253e2 100644 --- a/clients/js/src/internals.ts +++ b/clients/js/src/internals.ts @@ -1,21 +1,8 @@ -import { - getSetComputeUnitLimitInstruction, - getSetComputeUnitPriceInstruction, -} from '@solana-program/compute-budget'; import { Address, - CompilableTransactionMessage, - createTransactionMessage, GetAccountInfoApi, - GetLatestBlockhashApi, - IInstruction, - MicroLamports, - pipe, ReadonlyUint8Array, Rpc, - setTransactionMessageFeePayerSigner, - setTransactionMessageLifetimeUsingBlockhash, - TransactionMessageWithBlockhashLifetime, TransactionSigner, } from '@solana/kit'; import { @@ -29,30 +16,10 @@ import { getReallocIterableInstructionPlan, IterableInstructionPlan, } from './instructionPlansDraft'; -import { getProgramAuthority, MetadataInput } from './utils'; +import { getProgramAuthority } from './utils'; export const REALLOC_LIMIT = 10_240; -export type ExtendedMetadataInput = MetadataInput & - PdaDetails & { - getDefaultMessage: () => Promise< - CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime - >; - defaultMessage: CompilableTransactionMessage & - TransactionMessageWithBlockhashLifetime; - }; - -export async function getExtendedMetadataInput( - input: MetadataInput -): Promise { - const getDefaultMessage = getDefaultMessageFactory(input); - const [pdaDetails, defaultMessage] = await Promise.all([ - getPdaDetails(input), - getDefaultMessage(), - ]); - return { ...input, ...pdaDetails, defaultMessage, getDefaultMessage }; -} - export type PdaDetails = { metadata: Address; isCanonical: boolean; @@ -82,74 +49,6 @@ export async function getPdaDetails(input: { return { metadata, isCanonical, programData }; } -export function getDefaultMessageFactory(input: { - rpc: Rpc; - payer: TransactionSigner; -}): () => Promise< - CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime -> { - const getBlockhash = getTimedCacheFunction(async () => { - const { value } = await input.rpc.getLatestBlockhash().send(); - return value; - }, 60_000); - return async () => { - const latestBlockhash = await getBlockhash(); - return pipe( - createTransactionMessage({ version: 0 }), - (tx) => setTransactionMessageFeePayerSigner(input.payer, tx), - (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx) - ); - }; -} - -function getTimedCacheFunction( - fn: () => Promise, - timeoutInMilliseconds: number -): () => Promise { - let cache: T | null = null; - let lastFetchTime = 0; - return async () => { - const currentTime = Date.now(); - - // Cache hit. - if (cache && currentTime - lastFetchTime < timeoutInMilliseconds) { - return cache; - } - - // Cache miss. - cache = await fn(); - lastFetchTime = currentTime; - return cache; - }; -} - -const MAX_COMPUTE_UNIT_LIMIT = 1_400_000; - -export function getComputeUnitInstructions(input: { - computeUnitPrice?: MicroLamports; - computeUnitLimit?: number | 'simulated'; -}) { - const instructions: IInstruction[] = []; - if (input.computeUnitPrice !== undefined) { - instructions.push( - getSetComputeUnitPriceInstruction({ - microLamports: input.computeUnitPrice, - }) - ); - } - if (input.computeUnitLimit !== undefined) { - instructions.push( - getSetComputeUnitLimitInstruction({ - units: - input.computeUnitLimit === 'simulated' - ? MAX_COMPUTE_UNIT_LIMIT - : input.computeUnitLimit, - }) - ); - } - return instructions; -} - export function getExtendInstructionPlan__NEW(input: { account: Address; authority: TransactionSigner; diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 2bdadab..881abb0 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -6,24 +6,14 @@ import { GetAccountInfoApi, getAddressDecoder, getAddressEncoder, - GetEpochInfoApi, - GetLatestBlockhashApi, - GetMinimumBalanceForRentExemptionApi, getOptionDecoder, getProgramDerivedAddress, - GetSignatureStatusesApi, getStructDecoder, getU32Decoder, getU64Decoder, MicroLamports, ReadonlyUint8Array, Rpc, - RpcSubscriptions, - SendTransactionApi, - SignatureNotificationsApi, - SimulateTransactionApi, - SlotNotificationsApi, - Transaction, TransactionSigner, unwrapOption, } from '@solana/kit'; @@ -47,54 +37,6 @@ export const LOADER_V3_PROGRAM_ADDRESS = export const LOADER_V4_PROGRAM_ADDRESS = 'CoreBPFLoaderV41111111111111111111111111111' as Address<'CoreBPFLoaderV41111111111111111111111111111'>; -export type MetadataInput = { - rpc: Rpc< - GetLatestBlockhashApi & - GetEpochInfoApi & - GetSignatureStatusesApi & - SendTransactionApi & - SimulateTransactionApi & - GetAccountInfoApi & - GetMinimumBalanceForRentExemptionApi - >; - rpcSubscriptions: RpcSubscriptions< - SignatureNotificationsApi & SlotNotificationsApi - >; - payer: TransactionSigner; - authority: TransactionSigner; - program: Address; - seed: SeedArgs; - encoding: EncodingArgs; - compression: CompressionArgs; - format: FormatArgs; - dataSource: DataSourceArgs; - data: ReadonlyUint8Array; - /** - * Extra fees to pay in microlamports per CU. - * Defaults to no extra fees. - */ - priorityFees?: MicroLamports; - /** - * Whether to use a buffer for creating or updating a metadata account. - * If a `TransactionSigner` is provided, the provided buffer will be used for updating only. - * Defaults to `true` unless the entire operation can be done in a single transaction. - */ - buffer?: TransactionSigner | boolean; - /** - * When using a buffer, whether to close the buffer account after the operation. - * This is only relevant when updating a metadata account since, when creating - * them, buffer accounts are transformed into metadata accounts. - * Defaults to `true`. - */ - closeBuffer?: boolean; - /** - * When using a buffer, whether to extract the last transaction from the buffer - * and return it as serialized bytes instead of sending it. - * Defaults to `false`. - */ - extractLastTransaction?: boolean; -}; - export type MetadataInput__NEW = { payer: TransactionSigner; authority: TransactionSigner; @@ -119,11 +61,6 @@ export type MetadataInput__NEW = { closeBuffer?: boolean; }; -export type MetadataResponse = { - metadata: Address; - lastTransaction?: Transaction; -}; - export type MetadataResponse__NEW = { metadata: Address; result: TransactionPlanResult; From a9e1c73a2844b9b340682ee838116ff8c8cc2bb8 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 09:47:43 +0100 Subject: [PATCH 108/112] wip --- clients/js/src/cli.ts | 2 +- clients/js/src/createMetadata.ts | 2 +- clients/js/src/index.ts | 2 +- .../computeBudgetHelpers.ts | 0 .../js/src/{instructionPlansDraft => instructionPlans}/index.ts | 0 .../instructionPlan.ts | 0 .../src/{instructionPlansDraft => instructionPlans}/internal.ts | 0 .../transactionHelpers.ts | 0 .../transactionPlan.ts | 0 .../transactionPlanExecutor.ts | 0 .../transactionPlanExecutorBase.ts | 0 .../transactionPlanExecutorDecorators.ts | 0 .../transactionPlanExecutorDefault.ts | 0 .../transactionPlanResult.ts | 0 .../transactionPlanner.ts | 0 .../transactionPlannerBase.ts | 0 .../transactionPlannerDefault.ts | 0 clients/js/src/internals.ts | 2 +- clients/js/src/updateMetadata.ts | 2 +- clients/js/src/uploadMetadata.ts | 2 +- clients/js/src/utils.ts | 2 +- 21 files changed, 7 insertions(+), 7 deletions(-) rename clients/js/src/{instructionPlansDraft => instructionPlans}/computeBudgetHelpers.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/index.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/instructionPlan.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/internal.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionHelpers.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlan.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlanExecutor.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlanExecutorBase.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlanExecutorDecorators.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlanExecutorDefault.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlanResult.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlanner.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlannerBase.ts (100%) rename clients/js/src/{instructionPlansDraft => instructionPlans}/transactionPlannerDefault.ts (100%) diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 0075638..7c4eaf2 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -43,7 +43,7 @@ import { TransactionPlanExecutor, TransactionPlanner, TransactionPlanResult, -} from './instructionPlansDraft'; +} from './instructionPlans'; import { getPdaDetails } from './internals'; import { packDirectData, diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 33e4153..1fb7081 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -17,7 +17,7 @@ import { createDefaultTransactionPlanner, parallelInstructionPlan, sequentialInstructionPlan, -} from './instructionPlansDraft'; +} from './instructionPlans'; import { getExtendInstructionPlan__NEW, getPdaDetails, diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index df2d459..5ce6031 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -1,5 +1,5 @@ export * from './generated'; -export * from './instructionPlansDraft'; +export * from './instructionPlans'; export * from './createMetadata'; export * from './downloadMetadata'; diff --git a/clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts b/clients/js/src/instructionPlans/computeBudgetHelpers.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/computeBudgetHelpers.ts rename to clients/js/src/instructionPlans/computeBudgetHelpers.ts diff --git a/clients/js/src/instructionPlansDraft/index.ts b/clients/js/src/instructionPlans/index.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/index.ts rename to clients/js/src/instructionPlans/index.ts diff --git a/clients/js/src/instructionPlansDraft/instructionPlan.ts b/clients/js/src/instructionPlans/instructionPlan.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/instructionPlan.ts rename to clients/js/src/instructionPlans/instructionPlan.ts diff --git a/clients/js/src/instructionPlansDraft/internal.ts b/clients/js/src/instructionPlans/internal.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/internal.ts rename to clients/js/src/instructionPlans/internal.ts diff --git a/clients/js/src/instructionPlansDraft/transactionHelpers.ts b/clients/js/src/instructionPlans/transactionHelpers.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionHelpers.ts rename to clients/js/src/instructionPlans/transactionHelpers.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlan.ts b/clients/js/src/instructionPlans/transactionPlan.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlan.ts rename to clients/js/src/instructionPlans/transactionPlan.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts b/clients/js/src/instructionPlans/transactionPlanExecutor.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlanExecutor.ts rename to clients/js/src/instructionPlans/transactionPlanExecutor.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts b/clients/js/src/instructionPlans/transactionPlanExecutorBase.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlanExecutorBase.ts rename to clients/js/src/instructionPlans/transactionPlanExecutorBase.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts b/clients/js/src/instructionPlans/transactionPlanExecutorDecorators.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlanExecutorDecorators.ts rename to clients/js/src/instructionPlans/transactionPlanExecutorDecorators.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts b/clients/js/src/instructionPlans/transactionPlanExecutorDefault.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlanExecutorDefault.ts rename to clients/js/src/instructionPlans/transactionPlanExecutorDefault.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanResult.ts b/clients/js/src/instructionPlans/transactionPlanResult.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlanResult.ts rename to clients/js/src/instructionPlans/transactionPlanResult.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlanner.ts b/clients/js/src/instructionPlans/transactionPlanner.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlanner.ts rename to clients/js/src/instructionPlans/transactionPlanner.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlannerBase.ts rename to clients/js/src/instructionPlans/transactionPlannerBase.ts diff --git a/clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts b/clients/js/src/instructionPlans/transactionPlannerDefault.ts similarity index 100% rename from clients/js/src/instructionPlansDraft/transactionPlannerDefault.ts rename to clients/js/src/instructionPlans/transactionPlannerDefault.ts diff --git a/clients/js/src/internals.ts b/clients/js/src/internals.ts index 99253e2..2f063be 100644 --- a/clients/js/src/internals.ts +++ b/clients/js/src/internals.ts @@ -15,7 +15,7 @@ import { getLinearIterableInstructionPlan, getReallocIterableInstructionPlan, IterableInstructionPlan, -} from './instructionPlansDraft'; +} from './instructionPlans'; import { getProgramAuthority } from './utils'; export const REALLOC_LIMIT = 10_240; diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index 453aacc..f4dcd91 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -27,7 +27,7 @@ import { createDefaultTransactionPlanner, parallelInstructionPlan, sequentialInstructionPlan, -} from './instructionPlansDraft'; +} from './instructionPlans'; import { getExtendInstructionPlan__NEW, getPdaDetails, diff --git a/clients/js/src/uploadMetadata.ts b/clients/js/src/uploadMetadata.ts index 2f4e22c..a4b6dcb 100644 --- a/clients/js/src/uploadMetadata.ts +++ b/clients/js/src/uploadMetadata.ts @@ -13,7 +13,7 @@ import { fetchMaybeMetadata } from './generated'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, -} from './instructionPlansDraft'; +} from './instructionPlans'; import { getPdaDetails } from './internals'; import { getUpdateMetadataInstructionPlanUsingBuffer__NEW, diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 881abb0..29d9e47 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -24,7 +24,7 @@ import { FormatArgs, SeedArgs, } from './generated'; -import { TransactionPlanResult } from './instructionPlansDraft'; +import { TransactionPlanResult } from './instructionPlans'; export const ACCOUNT_HEADER_LENGTH = 96; From 279865cd4b74226bdc99cdbe1ee81911cb6fc68d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 10:24:59 +0100 Subject: [PATCH 109/112] wip --- clients/js/src/cli.ts | 4 ++-- clients/js/src/createMetadata.ts | 28 ++++++++++-------------- clients/js/src/internals.ts | 4 ++-- clients/js/src/updateMetadata.ts | 28 ++++++++++-------------- clients/js/src/uploadMetadata.ts | 28 ++++++++++-------------- clients/js/src/utils.ts | 4 ++-- clients/js/test/createMetadata.test.ts | 10 ++++----- clients/js/test/downloadMetadata.test.ts | 6 ++--- clients/js/test/updateMetadata.test.ts | 20 ++++++++--------- clients/js/test/uploadMetadata.test.ts | 10 ++++----- 10 files changed, 65 insertions(+), 77 deletions(-) diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 7c4eaf2..6e8ca71 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -51,7 +51,7 @@ import { packExternalData, packUrlData, } from './packData'; -import { uploadMetadata__NEW } from './uploadMetadata'; +import { uploadMetadata } from './uploadMetadata'; import { getProgramAuthority } from './utils'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; @@ -182,7 +182,7 @@ program 'You must be the program authority to upload a canonical metadata account. Use `--non-canonical` option to upload as a third party.' ); } - await uploadMetadata__NEW({ + await uploadMetadata({ ...client, ...getPackedData(content, options), payer: client.payer, diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 1fb7081..86a108e 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -19,26 +19,22 @@ import { sequentialInstructionPlan, } from './instructionPlans'; import { - getExtendInstructionPlan__NEW, + getExtendInstructionPlan, getPdaDetails, - getWriteInstructionPlan__NEW, + getWriteInstructionPlan, REALLOC_LIMIT, } from './internals'; -import { - getAccountSize, - MetadataInput__NEW, - MetadataResponse__NEW, -} from './utils'; +import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; -export async function createMetadata__NEW( - input: MetadataInput__NEW & { +export async function createMetadata( + input: MetadataInput & { rpc: Rpc & Parameters[0]['rpc']; rpcSubscriptions: Parameters< typeof createDefaultTransactionPlanExecutor >[0]['rpcSubscriptions']; } -): Promise { +): Promise { const planner = createDefaultTransactionPlanner({ feePayer: input.payer, computeUnitPrice: input.priorityFees, @@ -63,16 +59,16 @@ export async function createMetadata__NEW( }; const transactionPlan = await planner( - getCreateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput) + getCreateMetadataInstructionPlanUsingInstructionData(extendedInput) ).catch(() => - planner(getCreateMetadataInstructionPlanUsingBuffer__NEW(extendedInput)) + planner(getCreateMetadataInstructionPlanUsingBuffer(extendedInput)) ); const result = await executor(transactionPlan); return { metadata, result }; } -export function getCreateMetadataInstructionPlanUsingInstructionData__NEW( +export function getCreateMetadataInstructionPlanUsingInstructionData( input: InitializeInput & { payer: TransactionSigner; rent: Lamports } ) { return sequentialInstructionPlan([ @@ -85,7 +81,7 @@ export function getCreateMetadataInstructionPlanUsingInstructionData__NEW( ]); } -export function getCreateMetadataInstructionPlanUsingBuffer__NEW( +export function getCreateMetadataInstructionPlanUsingBuffer( input: Omit & { data: ReadonlyUint8Array; payer: TransactionSigner; @@ -107,7 +103,7 @@ export function getCreateMetadataInstructionPlanUsingBuffer__NEW( }), ...(input.data.length > REALLOC_LIMIT ? [ - getExtendInstructionPlan__NEW({ + getExtendInstructionPlan({ account: input.metadata, authority: input.authority, extraLength: input.data.length, @@ -117,7 +113,7 @@ export function getCreateMetadataInstructionPlanUsingBuffer__NEW( ] : []), parallelInstructionPlan([ - getWriteInstructionPlan__NEW({ + getWriteInstructionPlan({ buffer: input.metadata, authority: input.authority, data: input.data, diff --git a/clients/js/src/internals.ts b/clients/js/src/internals.ts index 2f063be..127c8c6 100644 --- a/clients/js/src/internals.ts +++ b/clients/js/src/internals.ts @@ -49,7 +49,7 @@ export async function getPdaDetails(input: { return { metadata, isCanonical, programData }; } -export function getExtendInstructionPlan__NEW(input: { +export function getExtendInstructionPlan(input: { account: Address; authority: TransactionSigner; extraLength: number; @@ -69,7 +69,7 @@ export function getExtendInstructionPlan__NEW(input: { }); } -export function getWriteInstructionPlan__NEW(input: { +export function getWriteInstructionPlan(input: { buffer: Address; authority: TransactionSigner; data: ReadonlyUint8Array; diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index f4dcd91..ac47487 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -29,26 +29,22 @@ import { sequentialInstructionPlan, } from './instructionPlans'; import { - getExtendInstructionPlan__NEW, + getExtendInstructionPlan, getPdaDetails, - getWriteInstructionPlan__NEW, + getWriteInstructionPlan, REALLOC_LIMIT, } from './internals'; -import { - getAccountSize, - MetadataInput__NEW, - MetadataResponse__NEW, -} from './utils'; +import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; -export async function updateMetadata__NEW( - input: MetadataInput__NEW & { +export async function updateMetadata( + input: MetadataInput & { rpc: Rpc & Parameters[0]['rpc']; rpcSubscriptions: Parameters< typeof createDefaultTransactionPlanExecutor >[0]['rpcSubscriptions']; } -): Promise { +): Promise { const planner = createDefaultTransactionPlanner({ feePayer: input.payer, computeUnitPrice: input.priorityFees, @@ -94,16 +90,16 @@ export async function updateMetadata__NEW( }; const transactionPlan = await planner( - getUpdateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput) + getUpdateMetadataInstructionPlanUsingInstructionData(extendedInput) ).catch(() => - planner(getUpdateMetadataInstructionPlanUsingBuffer__NEW(extendedInput)) + planner(getUpdateMetadataInstructionPlanUsingBuffer(extendedInput)) ); const result = await executor(transactionPlan); return { metadata, result }; } -export function getUpdateMetadataInstructionPlanUsingInstructionData__NEW( +export function getUpdateMetadataInstructionPlanUsingInstructionData( input: Omit & { extraRent: Lamports; payer: TransactionSigner; @@ -135,7 +131,7 @@ export function getUpdateMetadataInstructionPlanUsingInstructionData__NEW( ]); } -export function getUpdateMetadataInstructionPlanUsingBuffer__NEW( +export function getUpdateMetadataInstructionPlanUsingBuffer( input: Omit & { buffer: TransactionSigner; bufferRent: Lamports; @@ -174,7 +170,7 @@ export function getUpdateMetadataInstructionPlanUsingBuffer__NEW( }), ...(input.sizeDifference > REALLOC_LIMIT ? [ - getExtendInstructionPlan__NEW({ + getExtendInstructionPlan({ account: input.metadata, authority: input.authority, extraLength: Number(input.sizeDifference), @@ -184,7 +180,7 @@ export function getUpdateMetadataInstructionPlanUsingBuffer__NEW( ] : []), parallelInstructionPlan([ - getWriteInstructionPlan__NEW({ + getWriteInstructionPlan({ buffer: input.buffer.address, authority: input.authority, data: input.data, diff --git a/clients/js/src/uploadMetadata.ts b/clients/js/src/uploadMetadata.ts index a4b6dcb..ff965a8 100644 --- a/clients/js/src/uploadMetadata.ts +++ b/clients/js/src/uploadMetadata.ts @@ -6,8 +6,8 @@ import { Rpc, } from '@solana/kit'; import { - getCreateMetadataInstructionPlanUsingBuffer__NEW, - getCreateMetadataInstructionPlanUsingInstructionData__NEW, + getCreateMetadataInstructionPlanUsingBuffer, + getCreateMetadataInstructionPlanUsingInstructionData, } from './createMetadata'; import { fetchMaybeMetadata } from './generated'; import { @@ -16,24 +16,20 @@ import { } from './instructionPlans'; import { getPdaDetails } from './internals'; import { - getUpdateMetadataInstructionPlanUsingBuffer__NEW, - getUpdateMetadataInstructionPlanUsingInstructionData__NEW, + getUpdateMetadataInstructionPlanUsingBuffer, + getUpdateMetadataInstructionPlanUsingInstructionData, } from './updateMetadata'; -import { - getAccountSize, - MetadataInput__NEW, - MetadataResponse__NEW, -} from './utils'; +import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; -export async function uploadMetadata__NEW( - input: MetadataInput__NEW & { +export async function uploadMetadata( + input: MetadataInput & { rpc: Rpc & Parameters[0]['rpc']; rpcSubscriptions: Parameters< typeof createDefaultTransactionPlanExecutor >[0]['rpcSubscriptions']; } -): Promise { +): Promise { const planner = createDefaultTransactionPlanner({ feePayer: input.payer, computeUnitPrice: input.priorityFees, @@ -62,9 +58,9 @@ export async function uploadMetadata__NEW( }; const transactionPlan = await planner( - getCreateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput) + getCreateMetadataInstructionPlanUsingInstructionData(extendedInput) ).catch(() => - planner(getCreateMetadataInstructionPlanUsingBuffer__NEW(extendedInput)) + planner(getCreateMetadataInstructionPlanUsingBuffer(extendedInput)) ); const result = await executor(transactionPlan); @@ -97,9 +93,9 @@ export async function uploadMetadata__NEW( }; const transactionPlan = await planner( - getUpdateMetadataInstructionPlanUsingInstructionData__NEW(extendedInput) + getUpdateMetadataInstructionPlanUsingInstructionData(extendedInput) ).catch(() => - planner(getUpdateMetadataInstructionPlanUsingBuffer__NEW(extendedInput)) + planner(getUpdateMetadataInstructionPlanUsingBuffer(extendedInput)) ); const result = await executor(transactionPlan); diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 29d9e47..9a22f40 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -37,7 +37,7 @@ export const LOADER_V3_PROGRAM_ADDRESS = export const LOADER_V4_PROGRAM_ADDRESS = 'CoreBPFLoaderV41111111111111111111111111111' as Address<'CoreBPFLoaderV41111111111111111111111111111'>; -export type MetadataInput__NEW = { +export type MetadataInput = { payer: TransactionSigner; authority: TransactionSigner; program: Address; @@ -61,7 +61,7 @@ export type MetadataInput__NEW = { closeBuffer?: boolean; }; -export type MetadataResponse__NEW = { +export type MetadataResponse = { metadata: Address; result: TransactionPlanResult; }; diff --git a/clients/js/test/createMetadata.test.ts b/clients/js/test/createMetadata.test.ts index c6f54af..0c70f40 100644 --- a/clients/js/test/createMetadata.test.ts +++ b/clients/js/test/createMetadata.test.ts @@ -3,7 +3,7 @@ import test from 'ava'; import { AccountDiscriminator, Compression, - createMetadata__NEW, + createMetadata, DataSource, Encoding, fetchMetadata, @@ -24,7 +24,7 @@ test('it creates a canonical metadata account', async (t) => { // When we create a canonical metadata account for the program. const data = getUtf8Encoder().encode('{"standard":"dummyIdl"}'); - const { metadata } = await createMetadata__NEW({ + const { metadata } = await createMetadata({ ...client, payer: authority, authority, @@ -63,7 +63,7 @@ test('it creates a canonical metadata account with data larger than a transactio // When we create a canonical metadata account for the program with a lot of data. const largeData = getUtf8Encoder().encode('x'.repeat(3_000)); - const { metadata } = await createMetadata__NEW({ + const { metadata } = await createMetadata({ ...client, payer: authority, authority, @@ -102,7 +102,7 @@ test('it creates a non-canonical metadata account', async (t) => { // When we create a non-canonical metadata account for the program. const data = getUtf8Encoder().encode('{"standard":"dummyIdl"}'); - const { metadata } = await createMetadata__NEW({ + const { metadata } = await createMetadata({ ...client, payer: authority, authority, @@ -141,7 +141,7 @@ test('it creates a non-canonical metadata account with data larger than a transa // When we create a non-canonical metadata account for the program with a lot of data. const largeData = getUtf8Encoder().encode('x'.repeat(3_000)); - const { metadata } = await createMetadata__NEW({ + const { metadata } = await createMetadata({ ...client, payer: authority, authority, diff --git a/clients/js/test/downloadMetadata.test.ts b/clients/js/test/downloadMetadata.test.ts index d0b0c48..c8b96da 100644 --- a/clients/js/test/downloadMetadata.test.ts +++ b/clients/js/test/downloadMetadata.test.ts @@ -4,7 +4,7 @@ import { downloadAndParseMetadata, Format, packDirectData, - uploadMetadata__NEW, + uploadMetadata, } from '../src'; import { createDefaultSolanaClient, @@ -20,7 +20,7 @@ test('it fetches and parses direct IDLs from canonical metadata accounts', async // And given the following IDL exists for the program. const idl = '{"kind":"rootNode","standard":"codama","version":"1.0.0"}'; - await uploadMetadata__NEW({ + await uploadMetadata({ ...client, ...packDirectData({ content: idl }), payer: authority, @@ -49,7 +49,7 @@ test('it fetches and parses direct IDLs from non-canonical metadata accounts', a // And given the following IDL exists for the program. const idl = '{"kind":"rootNode","standard":"codama","version":"1.0.0"}'; - await uploadMetadata__NEW({ + await uploadMetadata({ ...client, ...packDirectData({ content: idl }), payer: authority, diff --git a/clients/js/test/updateMetadata.test.ts b/clients/js/test/updateMetadata.test.ts index 1c5725e..e5d8802 100644 --- a/clients/js/test/updateMetadata.test.ts +++ b/clients/js/test/updateMetadata.test.ts @@ -3,13 +3,13 @@ import test from 'ava'; import { AccountDiscriminator, Compression, - createMetadata__NEW, + createMetadata, DataSource, Encoding, fetchMetadata, Format, Metadata, - updateMetadata__NEW, + updateMetadata, } from '../src'; import { createDefaultSolanaClient, @@ -24,7 +24,7 @@ test('it updates a canonical metadata account', async (t) => { const [program] = await createDeployedProgram(client, authority); // And the following existing canonical metadata account. - await createMetadata__NEW({ + await createMetadata({ ...client, payer: authority, authority, @@ -39,7 +39,7 @@ test('it updates a canonical metadata account', async (t) => { // When we update the metadata account with new data. const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); - const { metadata } = await updateMetadata__NEW({ + const { metadata } = await updateMetadata({ ...client, payer: authority, authority, @@ -77,7 +77,7 @@ test('it updates a canonical metadata account with data larger than a transactio const [program] = await createDeployedProgram(client, authority); // And the following existing canonical metadata account. - await createMetadata__NEW({ + await createMetadata({ ...client, payer: authority, authority, @@ -92,7 +92,7 @@ test('it updates a canonical metadata account with data larger than a transactio // When we update the metadata account with new data with a lot of data. const newData = getUtf8Encoder().encode('x'.repeat(3_000)); - const { metadata } = await updateMetadata__NEW({ + const { metadata } = await updateMetadata({ ...client, payer: authority, authority, @@ -130,7 +130,7 @@ test('it updates a non-canonical metadata account', async (t) => { const program = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); // And the following existing non-canonical metadata account. - await createMetadata__NEW({ + await createMetadata({ ...client, payer: authority, authority, @@ -145,7 +145,7 @@ test('it updates a non-canonical metadata account', async (t) => { // When we update the metadata account with new data. const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); - const { metadata } = await updateMetadata__NEW({ + const { metadata } = await updateMetadata({ ...client, payer: authority, authority, @@ -183,7 +183,7 @@ test('it updates a non-canonical metadata account with data larger than a transa const program = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); // And the following existing non-canonical metadata account. - await createMetadata__NEW({ + await createMetadata({ ...client, payer: authority, authority, @@ -198,7 +198,7 @@ test('it updates a non-canonical metadata account with data larger than a transa // When we update the metadata account with new data. const newData = getUtf8Encoder().encode('x'.repeat(3_000)); - const { metadata } = await updateMetadata__NEW({ + const { metadata } = await updateMetadata({ ...client, payer: authority, authority, diff --git a/clients/js/test/uploadMetadata.test.ts b/clients/js/test/uploadMetadata.test.ts index 8060521..92e6251 100644 --- a/clients/js/test/uploadMetadata.test.ts +++ b/clients/js/test/uploadMetadata.test.ts @@ -3,14 +3,14 @@ import test from 'ava'; import { AccountDiscriminator, Compression, - createMetadata__NEW, + createMetadata, DataSource, Encoding, fetchMetadata, findCanonicalPda, Format, Metadata, - uploadMetadata__NEW, + uploadMetadata, } from '../src'; import { createDefaultSolanaClient, @@ -30,7 +30,7 @@ test('it creates a new metadata account if it does not exist', async (t) => { // When we upload this canonical metadata account. const data = getUtf8Encoder().encode('Some data'); - await uploadMetadata__NEW({ + await uploadMetadata({ ...client, payer: authority, authority, @@ -68,7 +68,7 @@ test('it updates a metadata account if it exists', async (t) => { const [program] = await createDeployedProgram(client, authority); // And given the following canonical metadata account exists. - await createMetadata__NEW({ + await createMetadata({ ...client, payer: authority, authority, @@ -83,7 +83,7 @@ test('it updates a metadata account if it exists', async (t) => { // When we upload this canonical metadata account with different data. const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); - const { metadata } = await uploadMetadata__NEW({ + const { metadata } = await uploadMetadata({ ...client, payer: authority, authority, From 5edbfc05da2c3c8102dc895a40e3a6baa78b3777 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 10:34:28 +0100 Subject: [PATCH 110/112] wip --- .../src/instructionPlans/computeBudgetHelpers.ts | 10 ++++++++++ .../transactionPlannerDefault.ts | 16 ++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/clients/js/src/instructionPlans/computeBudgetHelpers.ts b/clients/js/src/instructionPlans/computeBudgetHelpers.ts index c296eba..f7f8368 100644 --- a/clients/js/src/instructionPlans/computeBudgetHelpers.ts +++ b/clients/js/src/instructionPlans/computeBudgetHelpers.ts @@ -7,6 +7,7 @@ import { COMPUTE_BUDGET_PROGRAM_ADDRESS, ComputeBudgetInstruction, getSetComputeUnitLimitInstruction, + getSetComputeUnitPriceInstruction, identifyComputeBudgetInstruction, } from '@solana-program/compute-budget'; import { @@ -29,6 +30,15 @@ export const PROVISORY_COMPUTE_UNIT_LIMIT = 0; // This is the maximum compute unit limit that can be set for a transaction. export const MAX_COMPUTE_UNIT_LIMIT = 1_400_000; +export function setTransactionMessageComputeUnitPrice< + TTransactionMessage extends BaseTransactionMessage, +>(microLamports: number | bigint, transactionMessage: TTransactionMessage) { + return appendTransactionMessageInstruction( + getSetComputeUnitPriceInstruction({ microLamports }), + transactionMessage + ); +} + export function fillProvisorySetComputeUnitLimitInstruction< TTransactionMessage extends BaseTransactionMessage, >(transactionMessage: TTransactionMessage) { diff --git a/clients/js/src/instructionPlans/transactionPlannerDefault.ts b/clients/js/src/instructionPlans/transactionPlannerDefault.ts index 95682cb..08d0ea9 100644 --- a/clients/js/src/instructionPlans/transactionPlannerDefault.ts +++ b/clients/js/src/instructionPlans/transactionPlannerDefault.ts @@ -1,16 +1,17 @@ import { - appendTransactionMessageInstruction, createTransactionMessage, MicroLamports, pipe, setTransactionMessageFeePayerSigner, TransactionSigner, } from '@solana/kit'; +import { + fillProvisorySetComputeUnitLimitInstruction, + setTransactionMessageComputeUnitPrice, +} from './computeBudgetHelpers'; +import { setTransactionMessageLifetimeUsingProvisoryBlockhash } from './transactionHelpers'; import { TransactionPlanner } from './transactionPlanner'; import { createBaseTransactionPlanner } from './transactionPlannerBase'; -import { setTransactionMessageLifetimeUsingProvisoryBlockhash } from './transactionHelpers'; -import { fillProvisorySetComputeUnitLimitInstruction } from './computeBudgetHelpers'; -import { getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget'; export function createDefaultTransactionPlanner(config: { feePayer: TransactionSigner; @@ -25,12 +26,7 @@ export function createDefaultTransactionPlanner(config: { (tx) => setTransactionMessageFeePayerSigner(config.feePayer, tx), (tx) => config.computeUnitPrice - ? appendTransactionMessageInstruction( - getSetComputeUnitPriceInstruction({ - microLamports: config.computeUnitPrice, - }), - tx - ) + ? setTransactionMessageComputeUnitPrice(config.computeUnitPrice, tx) : tx ), }); From 1ec0180459804102d75ab1d2d0c25626f48e1b34 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 10:44:40 +0100 Subject: [PATCH 111/112] wip --- .../instructionPlans/transactionPlanner.ts | 2 +- .../transactionPlannerBase.ts | 63 ++++++++++++++----- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/clients/js/src/instructionPlans/transactionPlanner.ts b/clients/js/src/instructionPlans/transactionPlanner.ts index 4dedaf0..f960d73 100644 --- a/clients/js/src/instructionPlans/transactionPlanner.ts +++ b/clients/js/src/instructionPlans/transactionPlanner.ts @@ -3,5 +3,5 @@ import { TransactionPlan } from './transactionPlan'; export type TransactionPlanner = ( instructionPlan: InstructionPlan, - config?: { abortSignal?: AbortSignal } // TODO: Use + config?: { abortSignal?: AbortSignal } ) => Promise; diff --git a/clients/js/src/instructionPlans/transactionPlannerBase.ts b/clients/js/src/instructionPlans/transactionPlannerBase.ts index 8762de0..f45a321 100644 --- a/clients/js/src/instructionPlans/transactionPlannerBase.ts +++ b/clients/js/src/instructionPlans/transactionPlannerBase.ts @@ -24,13 +24,14 @@ import { import { TransactionPlanner } from './transactionPlanner'; export type TransactionPlannerConfig = { - createTransactionMessage: () => - | Promise - | CompilableTransactionMessage; + createTransactionMessage: (config?: { + abortSignal?: AbortSignal; + }) => Promise | CompilableTransactionMessage; newInstructionsTransformer?: < TTransactionMessage extends CompilableTransactionMessage, >( - transactionMessage: TTransactionMessage + transactionMessage: TTransactionMessage, + config?: { abortSignal?: AbortSignal } ) => Promise | TTransactionMessage; }; @@ -38,36 +39,51 @@ export function createBaseTransactionPlanner( config: TransactionPlannerConfig ): TransactionPlanner { const createSingleTransactionPlan = async ( - instructions: IInstruction[] = [] + instructions: IInstruction[] = [], + abortSignal?: AbortSignal ): Promise => { + abortSignal?.throwIfAborted(); const plan: SingleTransactionPlan = { kind: 'single', - message: await Promise.resolve(config.createTransactionMessage()), + message: await Promise.resolve( + config.createTransactionMessage({ abortSignal }) + ), }; if (instructions.length > 0) { - await addInstructionsToSingleTransactionPlan(plan, instructions); + abortSignal?.throwIfAborted(); + await addInstructionsToSingleTransactionPlan( + plan, + instructions, + abortSignal + ); } return plan; }; const addInstructionsToSingleTransactionPlan = async ( plan: SingleTransactionPlan, - instructions: IInstruction[] + instructions: IInstruction[], + abortSignal?: AbortSignal ): Promise => { let message = appendTransactionMessageInstructions( instructions, plan.message ); if (config?.newInstructionsTransformer) { + abortSignal?.throwIfAborted(); message = await Promise.resolve( - config.newInstructionsTransformer(plan.message) + config.newInstructionsTransformer(plan.message, { abortSignal }) ); } (plan as Mutable).message = message; }; - return async (originalInstructionPlan): Promise => { + return async ( + originalInstructionPlan, + { abortSignal } = {} + ): Promise => { const plan = await traverse(originalInstructionPlan, { + abortSignal, parent: null, parentCandidates: [], createSingleTransactionPlan, @@ -92,14 +108,17 @@ export function createBaseTransactionPlanner( } type TraverseContext = { + abortSignal?: AbortSignal; parent: InstructionPlan | null; parentCandidates: SingleTransactionPlan[]; createSingleTransactionPlan: ( - instructions?: IInstruction[] + instructions?: IInstruction[], + abortSignal?: AbortSignal ) => Promise; addInstructionsToSingleTransactionPlan: ( plan: SingleTransactionPlan, - instructions: IInstruction[] + instructions: IInstruction[], + abortSignal?: AbortSignal ) => Promise; }; @@ -107,6 +126,7 @@ async function traverse( instructionPlan: InstructionPlan, context: TraverseContext ): Promise { + context.abortSignal?.throwIfAborted(); switch (instructionPlan.kind) { case 'sequential': return await traverseSequential(instructionPlan, context); @@ -226,7 +246,7 @@ async function traverseSingle( await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); return null; } - return await context.createSingleTransactionPlan([ix]); + return await context.createSingleTransactionPlan([ix], context.abortSignal); } async function traverseIterable( @@ -241,16 +261,27 @@ async function traverseIterable( const candidateResult = selectCandidateForIterator(candidates, iterator); if (candidateResult) { const [candidate, ix] = candidateResult; - await context.addInstructionsToSingleTransactionPlan(candidate, [ix]); + await context.addInstructionsToSingleTransactionPlan( + candidate, + [ix], + context.abortSignal + ); } else { - const newPlan = await context.createSingleTransactionPlan(); + const newPlan = await context.createSingleTransactionPlan( + [], + context.abortSignal + ); const ix = iterator.next(newPlan.message); if (!ix) { throw new Error( 'Could not fit `InterableInstructionPlan` into a transaction' ); } - await context.addInstructionsToSingleTransactionPlan(newPlan, [ix]); + await context.addInstructionsToSingleTransactionPlan( + newPlan, + [ix], + context.abortSignal + ); transactionPlans.push(newPlan); // Adding the new plan to the candidates is important for cases From 4e7715ff36c8a125f110b3d1ac6c51b99412c8ff Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 10:57:20 +0100 Subject: [PATCH 112/112] wip --- .../_instructionPlanHelpers.ts | 0 .../_transactionPlanHelpers.ts | 0 .../_transactionPlanResultHelpers.ts | 0 .../transactionPlanExecutor.test.ts | 0 .../transactionPlanner.test.ts | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename clients/js/test/{instructionPlansDraft => instructionPlans}/_instructionPlanHelpers.ts (100%) rename clients/js/test/{instructionPlansDraft => instructionPlans}/_transactionPlanHelpers.ts (100%) rename clients/js/test/{instructionPlansDraft => instructionPlans}/_transactionPlanResultHelpers.ts (100%) rename clients/js/test/{instructionPlansDraft => instructionPlans}/transactionPlanExecutor.test.ts (100%) rename clients/js/test/{instructionPlansDraft => instructionPlans}/transactionPlanner.test.ts (100%) diff --git a/clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts b/clients/js/test/instructionPlans/_instructionPlanHelpers.ts similarity index 100% rename from clients/js/test/instructionPlansDraft/_instructionPlanHelpers.ts rename to clients/js/test/instructionPlans/_instructionPlanHelpers.ts diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts b/clients/js/test/instructionPlans/_transactionPlanHelpers.ts similarity index 100% rename from clients/js/test/instructionPlansDraft/_transactionPlanHelpers.ts rename to clients/js/test/instructionPlans/_transactionPlanHelpers.ts diff --git a/clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts b/clients/js/test/instructionPlans/_transactionPlanResultHelpers.ts similarity index 100% rename from clients/js/test/instructionPlansDraft/_transactionPlanResultHelpers.ts rename to clients/js/test/instructionPlans/_transactionPlanResultHelpers.ts diff --git a/clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts b/clients/js/test/instructionPlans/transactionPlanExecutor.test.ts similarity index 100% rename from clients/js/test/instructionPlansDraft/transactionPlanExecutor.test.ts rename to clients/js/test/instructionPlans/transactionPlanExecutor.test.ts diff --git a/clients/js/test/instructionPlansDraft/transactionPlanner.test.ts b/clients/js/test/instructionPlans/transactionPlanner.test.ts similarity index 100% rename from clients/js/test/instructionPlansDraft/transactionPlanner.test.ts rename to clients/js/test/instructionPlans/transactionPlanner.test.ts