Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading