Skip to content

Commit

Permalink
Introduce InstructionPlan executors in JS client (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva authored Feb 5, 2025
1 parent f71c3af commit c281352
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 200 deletions.
38 changes: 16 additions & 22 deletions clients/js/src/createMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,32 @@ import {
getInitializeInstruction,
PROGRAM_METADATA_PROGRAM_ADDRESS,
} from './generated';
import {
getTransactionMessageFromPlan,
InstructionPlan,
MessageInstructionPlan,
} from './instructionPlans';
import {
calculateMaxChunkSize,
getComputeUnitInstructions,
getDefaultInstructionPlanContext,
getPdaDetails,
getTransactionMessageFromPlan,
getExtendedMetadataInput,
getMetadataInstructionPlanExecutor,
getWriteInstructionPlan,
InstructionPlan,
messageFitsInOneTransaction,
MessageInstructionPlan,
PdaDetails,
sendInstructionPlanAndGetMetadataResponse,
} from './internals';
import { getAccountSize, MetadataInput, MetadataResponse } from './utils';

export async function createMetadata(
input: MetadataInput
): Promise<MetadataResponse> {
const context = getDefaultInstructionPlanContext(input);
const [pdaDetails, defaultMessage] = await Promise.all([
getPdaDetails(input),
context.createMessage(),
]);
const extendedInput = { ...input, ...pdaDetails, defaultMessage };
return await sendInstructionPlanAndGetMetadataResponse(
await getCreateMetadataInstructions(extendedInput),
context,
extendedInput
);
const extendedInput = await getExtendedMetadataInput(input);
const executor = getMetadataInstructionPlanExecutor(extendedInput);
const plan = await getCreateMetadataInstructionPlan(extendedInput);
return await executor(plan);
}

export async function getCreateMetadataInstructions(
export async function getCreateMetadataInstructionPlan(
input: Omit<MetadataInput, 'rpc' | 'rpcSubscriptions'> &
PdaDetails & {
rpc: Rpc<GetMinimumBalanceForRentExemptionApi>;
Expand All @@ -52,7 +46,7 @@ export async function getCreateMetadataInstructions(
.getMinimumBalanceForRentExemption(getAccountSize(input.data.length))
.send();
const planUsingInstructionData =
getCreateMetadataInstructionsUsingInstructionData({ ...input, rent });
getCreateMetadataInstructionPlanUsingInstructionData({ ...input, rent });
const messageUsingInstructionData = getTransactionMessageFromPlan(
input.defaultMessage,
planUsingInstructionData
Expand All @@ -70,14 +64,14 @@ export async function getCreateMetadataInstructions(
...input,
buffer: input.metadata,
});
return getCreateMetadataInstructionsUsingBuffer({
return getCreateMetadataInstructionPlanUsingBuffer({
...input,
chunkSize,
rent,
});
}

export function getCreateMetadataInstructionsUsingInstructionData(
export function getCreateMetadataInstructionPlanUsingInstructionData(
input: Omit<MetadataInput, 'rpc' | 'rpcSubscriptions'> &
PdaDetails & { rent: Lamports }
): MessageInstructionPlan {
Expand All @@ -101,7 +95,7 @@ export function getCreateMetadataInstructionsUsingInstructionData(
};
}

export function getCreateMetadataInstructionsUsingBuffer(
export function getCreateMetadataInstructionPlanUsingBuffer(
input: Omit<MetadataInput, 'rpc' | 'rpcSubscriptions'> &
PdaDetails & { rent: Lamports; chunkSize: number }
): InstructionPlan {
Expand Down
83 changes: 83 additions & 0 deletions clients/js/src/instructionPlans/defaultInstructionPlanExecutor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
appendTransactionMessageInstructions,
CompilableTransactionMessage,
FullySignedTransaction,
pipe,
signTransactionMessageWithSigners,
TransactionMessageWithBlockhashLifetime,
TransactionMessageWithDurableNonceLifetime,
TransactionWithLifetime,
} from '@solana/web3.js';
import { MessageInstructionPlan } from './instructionPlan';
import {
chunkParallelInstructionPlans,
createInstructionPlanExecutor,
InstructionPlanExecutor,
} from './instructionPlanExecutor';

export type DefaultInstructionPlanExecutorConfig = Readonly<{
/**
* 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; // TODO

/**
* 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
)
>;

/**
* Sends and confirms a constructed transaction.
*/
sendAndConfirm: (
transaction: FullySignedTransaction & TransactionWithLifetime,
config?: { abortSignal?: AbortSignal }
) => Promise<void>;
}>;

export function getDefaultInstructionPlanExecutor(
config: DefaultInstructionPlanExecutorConfig
): InstructionPlanExecutor {
const {
getDefaultMessage,
parallelChunkSize: chunkSize,
sendAndConfirm,
} = config;

return async (plan, config) => {
const handleMessage = async (plan: MessageInstructionPlan) => {
const tx = await pipe(
await getDefaultMessage(config),
(tx) => appendTransactionMessageInstructions(plan.instructions, tx),
(tx) => signTransactionMessageWithSigners(tx)
);
await sendAndConfirm(tx, config);
};

const executor = pipe(createInstructionPlanExecutor(handleMessage), (e) =>
chunkSize ? chunkParallelInstructionPlans(e, chunkSize) : e
);

return await executor(plan, config);
};
}
3 changes: 3 additions & 0 deletions clients/js/src/instructionPlans/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './defaultInstructionPlanExecutor';
export * from './instructionPlan';
export * from './instructionPlanExecutor';
39 changes: 39 additions & 0 deletions clients/js/src/instructionPlans/instructionPlan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
appendTransactionMessageInstructions,
BaseTransactionMessage,
IInstruction,
} from '@solana/web3.js';

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
);
}
69 changes: 69 additions & 0 deletions clients/js/src/instructionPlans/instructionPlanExecutor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
InstructionPlan,
MessageInstructionPlan,
ParallelInstructionPlan,
} from './instructionPlan';

export type InstructionPlanExecutor = (
plan: InstructionPlan,
config?: { abortSignal?: AbortSignal }
) => Promise<void>;

export function createInstructionPlanExecutor(
handleMessage: (
plan: MessageInstructionPlan,
config?: { abortSignal?: AbortSignal }
) => Promise<void>
): 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) => {
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);
}
};
}
Loading

0 comments on commit c281352

Please sign in to comment.