Skip to content

Commit

Permalink
[wip]: Compute write chunk size and buffer strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva committed Jan 23, 2025
1 parent a42905a commit 73b5e48
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 70 deletions.
91 changes: 58 additions & 33 deletions clients/js/src/createMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,90 @@
import { getTransferSolInstruction } from '@solana-program/system';
import { Lamports } from '@solana/web3.js';
import {
CompilableTransactionMessage,
GetMinimumBalanceForRentExemptionApi,
Lamports,
Rpc,
} from '@solana/web3.js';
import {
getAllocateInstruction,
getInitializeInstruction,
getWriteInstruction,
PROGRAM_METADATA_PROGRAM_ADDRESS,
} from './generated';
import {
calculateMaxChunkSize,
getComputeUnitInstructions,
getDefaultInstructionPlanContext,
getPdaDetails,
getTransactionMessageFromPlan,
getWriteInstructionPlan,
InstructionPlan,
messageFitsInOneTransaction,
MessageInstructionPlan,
PdaDetails,
sendInstructionPlan,
} from './internals';
import { getAccountSize, MetadataInput, MetadataResponse } from './utils';

export const SIZE_THRESHOLD_FOR_INITIALIZING_WITH_BUFFER = 200;
const WRITE_CHUNK_SIZE = 900;

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

export async function getCreateMetadataInstructions(
input: Omit<MetadataInput, 'rpcSubscriptions'> & PdaDetails
input: Omit<MetadataInput, 'rpc' | 'rpcSubscriptions'> &
PdaDetails & {
rpc: Rpc<GetMinimumBalanceForRentExemptionApi>;
defaultMessage: CompilableTransactionMessage;
}
): Promise<InstructionPlan> {
const useBuffer =
input.data.length >= SIZE_THRESHOLD_FOR_INITIALIZING_WITH_BUFFER; // TODO: Compute.
const chunkSize = WRITE_CHUNK_SIZE; // TODO: Ask for createMessage to return the chunk size.
const rent = await input.rpc
.getMinimumBalanceForRentExemption(getAccountSize(input.data.length))
.send();
const planUsingInstructionData =
getCreateMetadataInstructionsUsingInstructionData({ ...input, rent });
const messageUsingInstructionData = getTransactionMessageFromPlan(
input.defaultMessage,
planUsingInstructionData
);
const useBuffer =
input.buffer === undefined
? !messageFitsInOneTransaction(messageUsingInstructionData)
: !!input.buffer;

if (!useBuffer) {
return planUsingInstructionData;
}

return useBuffer
? getCreateMetadataInstructionsUsingBuffer({ ...input, chunkSize, rent })
: getCreateMetadataInstructionsUsingInstructionData({ ...input, rent });
const chunkSize = calculateMaxChunkSize(input.defaultMessage, {
...input,
buffer: input.metadata,
});
return getCreateMetadataInstructionsUsingBuffer({
...input,
chunkSize,
rent,
});
}

export function getCreateMetadataInstructionsUsingInstructionData(
input: Omit<MetadataInput, 'rpc' | 'rpcSubscriptions'> &
PdaDetails & { rent: Lamports }
): InstructionPlan {
): MessageInstructionPlan {
return {
kind: 'message',
instructions: [
...getComputeUnitInstructions({
computeUnitPrice: input.priorityFees,
computeUnitLimit: 10_000, // TODO: Add max CU for each instruction.
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
}),
getTransferSolInstruction({
source: input.payer,
Expand All @@ -79,7 +110,7 @@ export function getCreateMetadataInstructionsUsingBuffer(
instructions: [
...getComputeUnitInstructions({
computeUnitPrice: input.priorityFees,
computeUnitLimit: 10_000, // TODO: Add max CU for each instruction.
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
}),
getTransferSolInstruction({
source: input.payer,
Expand All @@ -101,20 +132,14 @@ export function getCreateMetadataInstructionsUsingBuffer(
// TODO: Use parallel plan when the program supports it.
const writePlan: InstructionPlan = { kind: 'sequential', plans: [] };
while (offset < input.data.length) {
writePlan.plans.push({
kind: 'message',
instructions: [
...getComputeUnitInstructions({
computeUnitPrice: input.priorityFees,
computeUnitLimit: 10_000, // TODO: Add max CU for each instruction.
}),
getWriteInstruction({
buffer: input.metadata,
authority: input.authority,
data: input.data.slice(offset, offset + input.chunkSize),
}),
],
});
writePlan.plans.push(
getWriteInstructionPlan({
buffer: input.metadata,
authority: input.authority,
data: input.data.slice(offset, offset + input.chunkSize),
priorityFees: input.priorityFees,
})
);
offset += input.chunkSize;
}
mainPlan.plans.push(writePlan);
Expand All @@ -124,7 +149,7 @@ export function getCreateMetadataInstructionsUsingBuffer(
instructions: [
...getComputeUnitInstructions({
computeUnitPrice: input.priorityFees,
computeUnitLimit: 10_000, // TODO: Add max CU for each instruction.
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
}),
getInitializeInstruction({
...input,
Expand Down
73 changes: 72 additions & 1 deletion clients/js/src/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ import {
appendTransactionMessageInstructions,
Commitment,
CompilableTransactionMessage,
compileTransaction,
createTransactionMessage,
GetAccountInfoApi,
GetEpochInfoApi,
GetLatestBlockhashApi,
GetMinimumBalanceForRentExemptionApi,
GetSignatureStatusesApi,
getTransactionEncoder,
IInstruction,
MicroLamports,
pipe,
ReadonlyUint8Array,
Rpc,
RpcSubscriptions,
sendAndConfirmTransactionFactory,
Expand All @@ -28,9 +31,14 @@ import {
TransactionMessageWithBlockhashLifetime,
TransactionSigner,
} from '@solana/web3.js';
import { findMetadataPda, SeedArgs } from './generated';
import { findMetadataPda, getWriteInstruction, SeedArgs } from './generated';
import { getProgramAuthority } from './utils';

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 PdaDetails = {
metadata: Address;
isCanonical: boolean;
Expand Down Expand Up @@ -199,6 +207,15 @@ async function sendMessageInstructionPlan(
);
}

export function getTransactionMessageFromPlan(
defaultMessage: CompilableTransactionMessage,
plan: MessageInstructionPlan
) {
return pipe(defaultMessage, (tx) =>
appendTransactionMessageInstructions(plan.instructions, tx)
);
}

export function getComputeUnitInstructions(input: {
computeUnitPrice?: MicroLamports;
computeUnitLimit?: number;
Expand All @@ -220,3 +237,57 @@ export function getComputeUnitInstructions(input: {
}
return instructions;
}

export function calculateMaxChunkSize(
defaultMessage: CompilableTransactionMessage,
input: {
buffer: Address;
authority: TransactionSigner;
priorityFees?: MicroLamports;
}
) {
const plan = getWriteInstructionPlan({ ...input, 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 getWriteInstructionPlan(input: {
buffer: Address;
authority: TransactionSigner;
data: ReadonlyUint8Array;
priorityFees?: MicroLamports;
}): MessageInstructionPlan {
return {
kind: 'message',
instructions: [
...getComputeUnitInstructions({
computeUnitPrice: input.priorityFees,
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
}),
getWriteInstruction(input),
],
};
}
Loading

0 comments on commit 73b5e48

Please sign in to comment.