Skip to content

Commit

Permalink
[wip]: Add getDefaultCreateMessage helper
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva committed Jan 22, 2025
1 parent 1a2f7e0 commit 0802673
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 49 deletions.
8 changes: 6 additions & 2 deletions clients/js/src/createMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getTransferSolInstruction } from '@solana-program/system';
import { Lamports } from '@solana/web3.js';
import { Lamports, sendAndConfirmTransactionFactory } from '@solana/web3.js';
import {
getAllocateInstruction,
getInitializeInstruction,
getWriteInstruction,
PROGRAM_METADATA_PROGRAM_ADDRESS,
} from './generated';
import {
getDefaultCreateMessage,
getPdaDetails,
InstructionPlan,
PdaDetails,
Expand All @@ -23,7 +24,10 @@ export async function createMetadata(
const pdaDetails = await getPdaDetails(input);
const extendedInput = { ...input, ...pdaDetails };
const plan = await getCreateMetadataInstructions(extendedInput);
await sendInstructionPlan(plan);
const createMessage =
input.createMessage ?? getDefaultCreateMessage(input.rpc, input.payer);
const sendAndConfirm = sendAndConfirmTransactionFactory(input);
await sendInstructionPlan(plan, createMessage, sendAndConfirm);
return { metadata: extendedInput.metadata };
}

Expand Down
73 changes: 37 additions & 36 deletions clients/js/src/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,14 @@ import {
CompilableTransactionMessage,
createTransactionMessage,
GetAccountInfoApi,
GetEpochInfoApi,
GetLatestBlockhashApi,
GetSignatureStatusesApi,
IInstruction,
pipe,
Rpc,
RpcSubscriptions,
sendAndConfirmTransactionFactory,
SendTransactionApi,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
SignatureNotificationsApi,
signTransactionMessageWithSigners,
SlotNotificationsApi,
TransactionMessageWithBlockhashLifetime,
TransactionSigner,
} from '@solana/web3.js';
Expand Down Expand Up @@ -54,39 +48,46 @@ export async function getPdaDetails(input: {
return { metadata, isCanonical, programData };
}

export async function sendInstructionsInSequentialTransactions(input: {
rpc: Rpc<
GetLatestBlockhashApi &
GetEpochInfoApi &
GetSignatureStatusesApi &
SendTransactionApi
>;
rpcSubscriptions: RpcSubscriptions<
SignatureNotificationsApi & SlotNotificationsApi
>;
payer: TransactionSigner;
instructions: IInstruction[][];
}) {
const sendAndConfirm = sendAndConfirmTransactionFactory(input);
for (const instructions of input.instructions) {
await pipe(
await getBaseTransactionMessage(input.rpc, input.payer),
(tx) => appendTransactionMessageInstructions(instructions, tx),
(tx) => signAndSendTransaction(tx, sendAndConfirm)
export function getDefaultCreateMessage(
rpc: Rpc<GetLatestBlockhashApi>,
payer: TransactionSigner
): () => Promise<
CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime
> {
const getBlockhash = getTimedCacheFunction(async () => {
const { value } = await rpc.getLatestBlockhash().send();
return value;
}, 60_000);
return async () => {
const latestBlockhash = await getBlockhash();
return pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(payer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
}
};
}

async function getBaseTransactionMessage(
rpc: Rpc<GetLatestBlockhashApi>,
payer: TransactionSigner
) {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
return pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(payer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
function getTimedCacheFunction<T>(
fn: () => Promise<T>,
timeoutInMilliseconds: number
): () => Promise<T> {
let cache: T | null = null;
let cacheExpiryTimer: NodeJS.Timeout | null = null;
return async () => {
// Cache hit.
if (cache && cacheExpiryTimer) {
return cache;
}

// Cache miss.
cache = await fn();
cacheExpiryTimer = setTimeout(() => {
cache = null;
cacheExpiryTimer = null;
}, timeoutInMilliseconds);
return cache;
};
}

async function signAndSendTransaction(
Expand Down
24 changes: 17 additions & 7 deletions clients/js/src/updateMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getTransferSolInstruction } from '@solana-program/system';
import {
generateKeyPairSigner,
isTransactionSigner,
lamports,
Lamports,
sendAndConfirmTransactionFactory,
TransactionSigner,
} from '@solana/web3.js';
import {
Expand All @@ -12,18 +14,21 @@ import {
getWriteInstruction,
} from './generated';
import {
getDefaultCreateMessage,
getPdaDetails,
InstructionPlan,
MessageInstructionPlan,
PdaDetails,
sendInstructionPlan,
} from './internals';
import { getAccountSize, MetadataInput } from './utils';
import { getAccountSize, MetadataInput, MetadataResponse } from './utils';

const SIZE_THRESHOLD_FOR_UPDATING_WITH_BUFFER = 200;
const WRITE_CHUNK_SIZE = 900;

export async function updateMetadata(input: MetadataInput) {
export async function updateMetadata(
input: MetadataInput
): Promise<MetadataResponse> {
const pdaDetails = await getPdaDetails(input);
const metadataAccount = await fetchMetadata(input.rpc, pdaDetails.metadata);
if (!metadataAccount.data.mutable) {
Expand All @@ -34,9 +39,12 @@ export async function updateMetadata(input: MetadataInput) {
...input,
...pdaDetails,
};
const plan = getUpdateMetadataInstructions(extendedInput);
await sendInstructionPlan(plan);
return extendedInput.metadata;
const plan = await getUpdateMetadataInstructions(extendedInput);
const createMessage =
input.createMessage ?? getDefaultCreateMessage(input.rpc, input.payer);
const sendAndConfirm = sendAndConfirmTransactionFactory(input);
await sendInstructionPlan(plan, createMessage, sendAndConfirm);
return { metadata: extendedInput.metadata };
}

export async function getUpdateMetadataInstructions(
Expand All @@ -62,7 +70,9 @@ export async function getUpdateMetadataInstructions(

const newAccountSize = getAccountSize(newDataLength);
const [buffer, bufferRent] = await Promise.all([
generateKeyPairSigner(),
typeof input.buffer === 'object' && isTransactionSigner(input.buffer)
? Promise.resolve(input.buffer)
: generateKeyPairSigner(),
input.rpc.getMinimumBalanceForRentExemption(newAccountSize).send(),
]);
return getUpdateMetadataInstructionsUsingBuffer({
Expand Down Expand Up @@ -112,7 +122,7 @@ export function getUpdateMetadataInstructionsUsingInstructionData(
}

export function getUpdateMetadataInstructionsUsingBuffer(
input: Omit<MetadataInput, 'rpc' | 'rpcSubscriptions'> &
input: Omit<MetadataInput, 'rpc' | 'rpcSubscriptions' | 'buffer'> &
PdaDetails & {
chunkSize: number;
sizeDifference: bigint;
Expand Down
19 changes: 15 additions & 4 deletions clients/js/src/uploadMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { sendAndConfirmTransactionFactory } from '@solana/web3.js';
import { getCreateMetadataInstructions } from './createMetadata';
import { fetchMaybeMetadata } from './generated';
import { getPdaDetails, sendInstructionPlan } from './internals';
import {
getDefaultCreateMessage,
getPdaDetails,
sendInstructionPlan,
} from './internals';
import { getUpdateMetadataInstructions } from './updateMetadata';
import { MetadataInput } from './utils';

Expand All @@ -15,18 +20,24 @@ export async function uploadMetadata(input: MetadataInput) {
// Create metadata if it doesn't exist.
if (!metadataAccount.exists) {
const plan = await getCreateMetadataInstructions(extendedInput);
await sendInstructionPlan(plan);
const createMessage =
input.createMessage ?? getDefaultCreateMessage(input.rpc, input.payer);
const sendAndConfirm = sendAndConfirmTransactionFactory(input);
await sendInstructionPlan(plan, createMessage, sendAndConfirm);
return { metadata: extendedInput.metadata };
}

// Update metadata if it exists.
if (!metadataAccount.data.mutable) {
throw new Error('Metadata account is immutable');
}
const plan = getUpdateMetadataInstructions({
const plan = await getUpdateMetadataInstructions({
...extendedInput,
currentDataLength: BigInt(metadataAccount.data.data.length),
});
await sendInstructionPlan(plan);
const createMessage =
input.createMessage ?? getDefaultCreateMessage(input.rpc, input.payer);
const sendAndConfirm = sendAndConfirmTransactionFactory(input);
await sendInstructionPlan(plan, createMessage, sendAndConfirm);
return { metadata: extendedInput.metadata };
}

0 comments on commit 0802673

Please sign in to comment.