From 70277e0c8841b4cb5c65abd6d42b754842d06800 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 18 May 2026 14:52:57 +0100 Subject: [PATCH] Migrate JS CLI to Kit plugins This PR rebuilds the `clients/js` CLI on top of the Kit plugin client model introduced in the previous PR. `getClient` is now a flat `.use(...)` chain composing the standard `payer`/`identity`/`solanaRpc`/`programMetadataProgram` plugins together with two small CLI-local plugins (`cliConfigs` and `cliRunOrExport`), replacing the bespoke shim layer in `cli/utils.ts` and the `createDefaultTransactionPlannerAndExecutor` / `getPdaDetails` helpers that lived in `internals.ts`. Commands now consume `client.identity` (instead of a CLI-only `client.authority` alias) and call `client.runOrExport(...)` with an `InstructionPlanInput` directly, using `client.programMetadata.instructions.*` wrappers for single-instruction commands and the building-block `getXMetadataInstructionPlan` functions for the one-shots so the `--export` flow can plan-then-branch. The ad-hoc compute-unit-limit stripping helper used during export is replaced with the version-agnostic `setTransactionMessageComputeUnitLimit(undefined, m)` from `@solana/kit`. No public-surface changes: all CLI options, commands, and behaviors are identical. --- clients/js/src/cli/commands/close-buffer.ts | 18 +- clients/js/src/cli/commands/close.ts | 24 +- clients/js/src/cli/commands/create-buffer.ts | 10 +- clients/js/src/cli/commands/create.ts | 8 +- .../js/src/cli/commands/remove-authority.ts | 27 +-- clients/js/src/cli/commands/set-authority.ts | 28 ++- .../src/cli/commands/set-buffer-authority.ts | 17 +- clients/js/src/cli/commands/set-immutable.ts | 22 +- clients/js/src/cli/commands/update-buffer.ts | 8 +- clients/js/src/cli/commands/update.ts | 8 +- clients/js/src/cli/commands/write.ts | 8 +- clients/js/src/cli/utils.ts | 210 +++++++++--------- clients/js/src/internals.ts | 142 +----------- 13 files changed, 191 insertions(+), 339 deletions(-) diff --git a/clients/js/src/cli/commands/close-buffer.ts b/clients/js/src/cli/commands/close-buffer.ts index be04c5b..d3ec5a0 100644 --- a/clients/js/src/cli/commands/close-buffer.ts +++ b/clients/js/src/cli/commands/close-buffer.ts @@ -1,5 +1,5 @@ -import { Address, sequentialInstructionPlan } from '@solana/kit'; -import { fetchMaybeBuffer, getCloseInstruction } from '../../generated'; +import { Address } from '@solana/kit'; +import { fetchMaybeBuffer } from '../../generated'; import { bufferArgument } from '../arguments'; import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions } from '../options'; @@ -31,13 +31,11 @@ export async function doCloseBuffer(buffer: Address, _: Options, cmd: CustomComm logErrorAndExit(`Buffer account not found: "${buffer}"`); } - await client.planAndExecute( - sequentialInstructionPlan([ - getCloseInstruction({ - account: buffer, - authority: client.authority, - destination: options.recipient ?? client.payer.address, - }), - ]), + await client.runOrExport( + client.programMetadata.instructions.close({ + account: buffer, + authority: client.identity, + destination: options.recipient ?? client.payer.address, + }), ); } diff --git a/clients/js/src/cli/commands/close.ts b/clients/js/src/cli/commands/close.ts index 5ff1dc4..6cb5d95 100644 --- a/clients/js/src/cli/commands/close.ts +++ b/clients/js/src/cli/commands/close.ts @@ -1,5 +1,5 @@ -import { Address, sequentialInstructionPlan } from '@solana/kit'; -import { getCloseInstruction, Seed } from '../../generated'; +import { Address } from '@solana/kit'; +import { Seed } from '../../generated'; import { programArgument, seedArgument } from '../arguments'; import { GlobalOptions, NonCanonicalWriteOption, nonCanonicalWriteOption } from '../options'; import { CustomCommand, getClient, getPdaDetailsForWriting } from '../utils'; @@ -25,18 +25,16 @@ async function doClose(seed: Seed, program: Address, _: Options, cmd: CustomComm metadata, program, seed, - authority: isCanonical ? undefined : client.authority.address, + authority: isCanonical ? undefined : client.identity.address, }); - await client.planAndExecute( - sequentialInstructionPlan([ - getCloseInstruction({ - account: metadata, - authority: client.authority, - program, - programData, - destination: client.payer.address, - }), - ]), + await client.runOrExport( + client.programMetadata.instructions.close({ + account: metadata, + authority: client.identity, + program, + programData, + destination: client.payer.address, + }), ); } diff --git a/clients/js/src/cli/commands/create-buffer.ts b/clients/js/src/cli/commands/create-buffer.ts index 5c15f2d..3a4f062 100644 --- a/clients/js/src/cli/commands/create-buffer.ts +++ b/clients/js/src/cli/commands/create-buffer.ts @@ -1,5 +1,4 @@ import { generateKeyPairSigner } from '@solana/kit'; -import { getCreateBufferInstructionPlan } from '../../createBuffer'; import { fileArgument } from '../arguments'; import { GlobalOptions, setWriteOptions, WriteOptions } from '../options'; import { CustomCommand, getClient, getWriteInput } from '../utils'; @@ -22,14 +21,13 @@ export async function doCreateBuffer(file: string | undefined, _: Options, cmd: logCommand(`Creating new buffer and setting authority...`, { buffer: buffer.address, - authority: client.authority.address, + authority: client.identity.address, }); - await client.planAndExecute( - await getCreateBufferInstructionPlan(client, { + await client.runOrExport( + client.programMetadata.instructions.createBuffer({ newBuffer: buffer, - authority: client.authority, - payer: client.payer, + authority: client.identity, sourceBuffer: writeInput.buffer, closeSourceBuffer: writeInput.closeBuffer, data: writeInput.buffer?.data.data ?? writeInput.data, diff --git a/clients/js/src/cli/commands/create.ts b/clients/js/src/cli/commands/create.ts index a6c3858..84fbfbe 100644 --- a/clients/js/src/cli/commands/create.ts +++ b/clients/js/src/cli/commands/create.ts @@ -35,7 +35,7 @@ export async function doCreate(seed: Seed, program: Address, file: string | unde metadata, program, seed, - authority: isCanonical ? undefined : client.authority.address, + authority: isCanonical ? undefined : client.identity.address, }); const [metadataAccount, writeInput] = await Promise.all([ @@ -47,11 +47,11 @@ export async function doCreate(seed: Seed, program: Address, file: string | unde logErrorAndExit(`Metadata account ${picocolors.bold(metadataAccount.address)} already exists.`); } - await client.planAndExecute( - await getCreateMetadataInstructionPlan(client, { + await client.runOrExport( + getCreateMetadataInstructionPlan(client, { ...writeInput, payer: client.payer, - authority: client.authority, + authority: client.identity, program, programData, seed, diff --git a/clients/js/src/cli/commands/remove-authority.ts b/clients/js/src/cli/commands/remove-authority.ts index bb13bf3..6cc3443 100644 --- a/clients/js/src/cli/commands/remove-authority.ts +++ b/clients/js/src/cli/commands/remove-authority.ts @@ -1,9 +1,8 @@ -import { Address, sequentialInstructionPlan } from '@solana/kit'; -import { getSetAuthorityInstruction, Seed } from '../../generated'; -import { getPdaDetails } from '../../internals'; +import { Address } from '@solana/kit'; +import { Seed } from '../../generated'; import { programArgument, seedArgument } from '../arguments'; import { GlobalOptions } from '../options'; -import { CustomCommand, getClient } from '../utils'; +import { CustomCommand, getClient, getPdaDetails } from '../utils'; import { logCommand } from '../logs'; export function setRemoveAuthorityCommand(program: CustomCommand): void { @@ -22,7 +21,7 @@ async function doRemoveAuthority(seed: Seed, program: Address, _: Options, cmd: const { metadata, programData } = await getPdaDetails({ rpc: client.rpc, program, - authority: client.authority, + authority: client.identity, seed, }); @@ -32,15 +31,13 @@ async function doRemoveAuthority(seed: Seed, program: Address, _: Options, cmd: seed, }); - await client.planAndExecute( - sequentialInstructionPlan([ - getSetAuthorityInstruction({ - account: metadata, - authority: client.authority, - newAuthority: null, - program, - programData, - }), - ]), + await client.runOrExport( + client.programMetadata.instructions.setAuthority({ + account: metadata, + authority: client.identity, + newAuthority: null, + program, + programData, + }), ); } diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts index ea6792b..d81a9cc 100644 --- a/clients/js/src/cli/commands/set-authority.ts +++ b/clients/js/src/cli/commands/set-authority.ts @@ -1,10 +1,9 @@ -import { Address, sequentialInstructionPlan } from '@solana/kit'; -import { getSetAuthorityInstruction, Seed } from '../../generated'; -import { getPdaDetails } from '../../internals'; +import { Address } from '@solana/kit'; +import { Seed } from '../../generated'; import { programArgument, seedArgument } from '../arguments'; import { logCommand } from '../logs'; import { GlobalOptions, NewAuthorityOption, newAuthorityOption } from '../options'; -import { CustomCommand, getClient } from '../utils'; +import { CustomCommand, getClient, getPdaDetails } from '../utils'; export function setSetAuthorityCommand(program: CustomCommand): void { program @@ -21,7 +20,8 @@ async function doSetAuthority(seed: Seed, program: Address, _: Options, cmd: Cus const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); const { metadata, programData } = await getPdaDetails({ - ...client, + rpc: client.rpc, + authority: client.identity, program, seed, }); @@ -33,15 +33,13 @@ async function doSetAuthority(seed: Seed, program: Address, _: Options, cmd: Cus seed, }); - await client.planAndExecute( - sequentialInstructionPlan([ - getSetAuthorityInstruction({ - account: metadata, - authority: client.authority, - newAuthority: options.newAuthority, - program, - programData, - }), - ]), + await client.runOrExport( + client.programMetadata.instructions.setAuthority({ + account: metadata, + authority: client.identity, + newAuthority: options.newAuthority, + program, + programData, + }), ); } diff --git a/clients/js/src/cli/commands/set-buffer-authority.ts b/clients/js/src/cli/commands/set-buffer-authority.ts index 5455549..67cf8f0 100644 --- a/clients/js/src/cli/commands/set-buffer-authority.ts +++ b/clients/js/src/cli/commands/set-buffer-authority.ts @@ -1,5 +1,4 @@ -import { Address, sequentialInstructionPlan } from '@solana/kit'; -import { getSetAuthorityInstruction } from '../../generated'; +import { Address } from '@solana/kit'; import { bufferArgument } from '../arguments'; import { logCommand } from '../logs'; import { GlobalOptions, NewAuthorityOption, newAuthorityOption } from '../options'; @@ -24,13 +23,11 @@ export async function doSetBufferAuthority(buffer: Address, _: Options, cmd: Cus 'new authority': options.newAuthority, }); - await client.planAndExecute( - sequentialInstructionPlan([ - getSetAuthorityInstruction({ - account: buffer, - authority: client.authority, - newAuthority: options.newAuthority, - }), - ]), + await client.runOrExport( + client.programMetadata.instructions.setAuthority({ + account: buffer, + authority: client.identity, + newAuthority: options.newAuthority, + }), ); } diff --git a/clients/js/src/cli/commands/set-immutable.ts b/clients/js/src/cli/commands/set-immutable.ts index 2c5f4c7..d97d039 100644 --- a/clients/js/src/cli/commands/set-immutable.ts +++ b/clients/js/src/cli/commands/set-immutable.ts @@ -1,5 +1,5 @@ -import { Address, sequentialInstructionPlan } from '@solana/kit'; -import { getSetImmutableInstruction, Seed } from '../../generated'; +import { Address } from '@solana/kit'; +import { Seed } from '../../generated'; import { programArgument, seedArgument } from '../arguments'; import { logCommand } from '../logs'; import { GlobalOptions, NonCanonicalWriteOption, nonCanonicalWriteOption } from '../options'; @@ -25,17 +25,15 @@ async function doSetImmutable(seed: Seed, program: Address, _: Options, cmd: Cus metadata, program, seed, - authority: isCanonical ? undefined : client.authority.address, + authority: isCanonical ? undefined : client.identity.address, }); - await client.planAndExecute( - sequentialInstructionPlan([ - getSetImmutableInstruction({ - metadata, - authority: client.authority, - program, - programData, - }), - ]), + await client.runOrExport( + client.programMetadata.instructions.setImmutable({ + metadata, + authority: client.identity, + program, + programData, + }), ); } diff --git a/clients/js/src/cli/commands/update-buffer.ts b/clients/js/src/cli/commands/update-buffer.ts index 4848d32..916f1e4 100644 --- a/clients/js/src/cli/commands/update-buffer.ts +++ b/clients/js/src/cli/commands/update-buffer.ts @@ -1,6 +1,5 @@ import { Address } from '@solana/kit'; import { fetchMaybeBuffer } from '../../generated'; -import { getUpdateBufferInstructionPlan } from '../../updateBuffer'; import { bufferArgument, fileArgument } from '../arguments'; import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions, setWriteOptions, WriteOptions } from '../options'; @@ -35,11 +34,10 @@ export async function doUpdateBuffer(buffer: Address, file: string | undefined, const newData = writeInput.buffer?.data.data ?? writeInput.data; const sizeDifference = newData.length - currentData.length; - await client.planAndExecute( - await getUpdateBufferInstructionPlan(client, { + await client.runOrExport( + client.programMetadata.instructions.updateBuffer({ buffer, - authority: client.authority, - payer: client.payer, + authority: client.identity, sizeDifference, sourceBuffer: writeInput.buffer, closeSourceBuffer: writeInput.closeBuffer, diff --git a/clients/js/src/cli/commands/update.ts b/clients/js/src/cli/commands/update.ts index 0d162c6..4644255 100644 --- a/clients/js/src/cli/commands/update.ts +++ b/clients/js/src/cli/commands/update.ts @@ -35,7 +35,7 @@ export async function doWrite(seed: Seed, program: Address, file: string | undef metadata, program, seed, - authority: isCanonical ? undefined : client.authority.address, + authority: isCanonical ? undefined : client.identity.address, }); const [metadataAccount, writeInput] = await Promise.all([ @@ -47,11 +47,11 @@ export async function doWrite(seed: Seed, program: Address, file: string | undef logErrorAndExit(`Metadata account ${picocolors.bold(metadataAccount.address)} does not exist.`); } - await client.planAndExecute( - await getUpdateMetadataInstructionPlan(client, { + await client.runOrExport( + getUpdateMetadataInstructionPlan(client, { ...writeInput, payer: client.payer, - authority: client.authority, + authority: client.identity, program, programData, metadata: metadataAccount, diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts index bf77792..732b5ef 100644 --- a/clients/js/src/cli/commands/write.ts +++ b/clients/js/src/cli/commands/write.ts @@ -34,7 +34,7 @@ export async function doWrite(seed: Seed, program: Address, file: string | undef metadata, program, seed, - authority: isCanonical ? undefined : client.authority.address, + authority: isCanonical ? undefined : client.identity.address, }); const [metadataAccount, writeInput] = await Promise.all([ @@ -42,11 +42,11 @@ export async function doWrite(seed: Seed, program: Address, file: string | undef getWriteInput(client, file, options), ]); - await client.planAndExecute( - await getWriteMetadataInstructionPlan(client, { + await client.runOrExport( + getWriteMetadataInstructionPlan(client, { ...writeInput, payer: client.payer, - authority: client.authority, + authority: client.identity, program, programData, seed, diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index bf4be59..839c5dd 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -7,43 +7,47 @@ import { AccountRole, Address, address, - BASE_ACCOUNT_SIZE, - ClientWithGetMinimumBalance, + ClientWithRpc, ClientWithTransactionPlanning, Commitment, compileTransaction, + createClient, createKeyPairSignerFromBytes, createNoopSigner, createSolanaRpc, createSolanaRpcSubscriptions, + extendClient, flattenTransactionPlan, - GetMinimumBalanceConfig, + GetAccountInfoApi, + GetLatestBlockhashApi, getTransactionEncoder, - InstructionPlan, - Lamports, - lamports, + InstructionPlanInput, MessageSigner, pipe, Rpc, RpcSubscriptions, + setTransactionMessageComputeUnitLimit, setTransactionMessageLifetimeUsingBlockhash, SolanaRpcApi, SolanaRpcSubscriptionsApi, TransactionMessage, - TransactionMessageWithFeePayer, TransactionPlan, TransactionPlanExecutor, - TransactionPlanner, TransactionSigner, } from '@solana/kit'; +import { solanaRpc } from '@solana/kit-plugin-rpc'; +import { identity, payer } from '@solana/kit-plugin-signer'; import { Command } from 'commander'; import picocolors from 'picocolors'; import { parse as parseYaml } from 'yaml'; -import { Buffer, DataSource, Encoding, fetchBuffer, Format, Seed } from '../generated'; -import { createDefaultTransactionPlannerAndExecutor, getPdaDetails, PdaDetails } from '../internals'; + +import { Buffer, DataSource, Encoding, fetchBuffer, findMetadataPda, Format, Seed, SeedArgs } from '../generated'; import { decodeData, packDirectData, PackedData, packExternalData, packUrlData } from '../packData'; +import { getProgramAuthority } from '../utils'; +import { programMetadataProgram } from '../plugin'; import { logErrorAndExit, logExports, logSuccess, logWarning } from './logs'; import { + ExportEncodingOption, ExportOption, GlobalOptions, KeypairOption, @@ -52,11 +56,6 @@ import { RpcOption, WriteOptions, } from './options'; -import { - COMPUTE_BUDGET_PROGRAM_ADDRESS, - ComputeBudgetInstruction, - identifyComputeBudgetInstruction, -} from '@solana-program/compute-budget'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; const DATA_SOURCE_OPTIONS = @@ -73,68 +72,76 @@ export class CustomCommand extends Command { } } -export type Client = ReadonlyClient & - ClientWithGetMinimumBalance & - ClientWithTransactionPlanning & { - authority: TransactionSigner & MessageSigner; - executor: TransactionPlanExecutor; - payer: TransactionSigner & MessageSigner; - planAndExecute: (instructionPlan: InstructionPlan) => Promise; - planner: TransactionPlanner; - }; +/** + * The CLI client returned by {@link getClient}: a Solana RPC client extended + * with the program metadata plugin (`client.programMetadata.*`), plus a few + * CLI-only fields (`configs`, `runOrExport`). + */ +export type Client = Awaited>; -export async function getClient(options: GlobalOptions): Promise { - const readonlyClient = getReadonlyClient(options); - const [authority, payer] = await getKeyPairSigners(options, readonlyClient.configs); - const { planner, executor } = createDefaultTransactionPlannerAndExecutor({ - priorityFees: options.priorityFees, - payer, - rpc: readonlyClient.rpc, - rpcSubscriptions: readonlyClient.rpcSubscriptions, - concurrency: 5, - }); - const planAndExecute = async (instructionPlan: InstructionPlan) => { - const transactionPlan = await planner(instructionPlan); - if (options.export) { - await exportTransactionPlan(transactionPlan, readonlyClient, options); - } else { - await executeTransactionPlan(transactionPlan, executor); - } - }; - const getMinimumBalance = async (space: number, config?: GetMinimumBalanceConfig): Promise => { - if (config?.withoutHeader) { - const headerBalance = await readonlyClient.rpc.getMinimumBalanceForRentExemption(0n).send(); - const lamportsPerByte = BigInt(headerBalance) / BigInt(BASE_ACCOUNT_SIZE); - return lamports(lamportsPerByte * BigInt(space)); - } - return await readonlyClient.rpc.getMinimumBalanceForRentExemption(BigInt(space)).send(); - }; - const planTransaction: ClientWithTransactionPlanning['planTransaction'] = async (input, config) => { - const transactionPlan = await planner(input as InstructionPlan, config); - if (transactionPlan.kind !== 'single') { - throw new Error('Expected a single transaction plan'); - } - return transactionPlan.message; - }; - const planTransactions: ClientWithTransactionPlanning['planTransactions'] = async (input, config) => - await planner(input as InstructionPlan, config); - return { - ...readonlyClient, - authority, - executor, - getMinimumBalance, - payer, - planAndExecute, - planner, - planTransaction, - planTransactions, - }; +export async function getClient(options: GlobalOptions) { + const configs = getSolanaConfigs(); + const rpcUrl = getRpcUrl(options, configs); + const rpcSubscriptionsUrl = getRpcSubscriptionsUrl(rpcUrl, configs); + const [identitySigner, payerSigner] = await getKeyPairSigners(options, configs); + + return createClient() + .use(payer(payerSigner)) + .use(identity(identitySigner)) + .use( + solanaRpc({ + rpcUrl, + rpcSubscriptionsUrl, + transactionConfig: { microLamportsPerComputeUnit: options.priorityFees }, + }), + ) + .use(programMetadataProgram()) + .use(cliConfigs(configs)) + .use(cliRunOrExport(options)); +} + +/** + * Plugin that attaches the parsed `~/.config/solana/cli/config.yml` to the + * client so commands that need to introspect Solana config defaults can read + * them without re-parsing the file. + */ +function cliConfigs(configs: SolanaConfigs) { + return (client: T) => extendClient(client, { configs }); +} + +/** + * Plugin that attaches a `runOrExport` helper to the client. The helper plans + * the given instruction(s) and then either executes them through the client's + * transaction plan executor or exports them as encoded transactions, depending + * on whether `--export` was passed. + */ +function cliRunOrExport(options: ExportOption & ExportEncodingOption) { + return < + T extends ClientWithRpc & + ClientWithTransactionPlanning & { + transactionPlanExecutor: TransactionPlanExecutor; + }, + >( + client: T, + ) => + extendClient(client, { + runOrExport: async (input: InstructionPlanInput | Promise): Promise => { + const transactionPlan = await client.planTransactions(await input); + if (options.export) { + await exportTransactionPlan(transactionPlan, client, options); + } else { + // TODO: progress + error handling. + await client.transactionPlanExecutor(transactionPlan); + logSuccess('Operation executed successfully'); + } + }, + }); } async function exportTransactionPlan( transactionPlan: TransactionPlan, - client: Pick, - options: GlobalOptions, + client: ClientWithRpc, + options: ExportOption & ExportEncodingOption, ) { const singleTransactions = flattenTransactionPlan(transactionPlan); const transactionEncoder = getTransactionEncoder(); @@ -147,7 +154,7 @@ async function exportTransactionPlan( const message = pipe( singleTransactions[i].message, m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), - m => removeComputeUnitLimitInstruction(m), + m => setTransactionMessageComputeUnitLimit(undefined, m), ); const prefix = picocolors.yellow(`[Transaction #${i + 1}]`); if (options.exportEncoding === 'instruction-list') { @@ -183,32 +190,6 @@ function logInstructions(message: TransactionMessage): void { }); } -function removeComputeUnitLimitInstruction< - TTransactionMessage extends TransactionMessage & TransactionMessageWithFeePayer, ->(message: TTransactionMessage): TTransactionMessage { - const index = getSetComputeUnitLimitInstructionIndex(message); - if (index === -1) return message; - return { - ...message, - instructions: message.instructions.filter((_, i) => i !== index), - }; -} - -export function getSetComputeUnitLimitInstructionIndex(transactionMessage: TransactionMessage) { - return transactionMessage.instructions.findIndex(ix => { - return ( - ix.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS && - identifyComputeBudgetInstruction(ix.data as Uint8Array) === ComputeBudgetInstruction.SetComputeUnitLimit - ); - }); -} - -async function executeTransactionPlan(transactionPlan: TransactionPlan, executor: TransactionPlanExecutor) { - // TODO: progress + error handling - await executor(transactionPlan); - logSuccess('Operation executed successfully'); -} - export type ReadonlyClient = { configs: SolanaConfigs; rpc: Rpc; @@ -302,13 +283,42 @@ export function getFormatFromFile(file: string | undefined): Format { } } +export type PdaDetails = { + metadata: Address; + isCanonical: boolean; + programData?: Address; +}; + +/** + * Fetches the on-chain state of `program` to determine whether the metadata + * account is canonical (i.e. the caller's authority matches the program's + * upgrade authority) and returns the resolved metadata PDA along with the + * associated program-data account when applicable. + */ +export async function getPdaDetails(input: { + rpc: Rpc; + program: Address; + authority: TransactionSigner | Address; + seed: SeedArgs; +}): Promise { + const authorityAddress = typeof input.authority === 'string' ? input.authority : input.authority.address; + const { authority, programData } = await getProgramAuthority(input.rpc, input.program); + const isCanonical = !!authority && authority === authorityAddress; + const [metadata] = await findMetadataPda({ + program: input.program, + authority: isCanonical ? null : authorityAddress, + seed: input.seed, + }); + return { metadata, isCanonical, programData }; +} + export async function getPdaDetailsForWriting( client: Client, options: NonCanonicalWriteOption, program: Address, seed: Seed, ): Promise { - const details = await getPdaDetails({ ...client, program, seed }); + const details = await getPdaDetails({ rpc: client.rpc, authority: client.identity, program, seed }); assertValidIsCanonical(details.isCanonical, options); const isCanonical = !options.nonCanonical; return { diff --git a/clients/js/src/internals.ts b/clients/js/src/internals.ts index 20acb23..e2d354f 100644 --- a/clients/js/src/internals.ts +++ b/clients/js/src/internals.ts @@ -1,111 +1,7 @@ -import { - estimateAndUpdateProvisoryComputeUnitLimitFactory, - estimateComputeUnitLimitFactory, - fillProvisorySetComputeUnitLimitInstruction, - setTransactionMessageComputeUnitPrice, -} from '@solana-program/compute-budget'; -import { - Address, - assertIsSendableTransaction, - assertIsTransactionWithBlockhashLifetime, - ClientWithTransactionPlanning, - createTransactionMessage, - createTransactionPlanExecutor, - createTransactionPlanner, - GetAccountInfoApi, - GetLatestBlockhashApi, - InstructionPlan, - MicroLamports, - pipe, - Rpc, - sendAndConfirmTransactionFactory, - setTransactionMessageFeePayerSigner, - setTransactionMessageLifetimeUsingBlockhash, - signTransactionMessageWithSigners, - SimulateTransactionApi, - TransactionPlanExecutorConfig, - TransactionSigner, -} from '@solana/kit'; -import { findMetadataPda, SeedArgs } from './generated'; -import { getProgramAuthority } from './utils'; +import { ClientWithTransactionPlanning, InstructionPlan } from '@solana/kit'; export const REALLOC_LIMIT = 10_240; -export type PdaDetails = { - metadata: Address; - isCanonical: boolean; - programData?: Address; -}; - -/** - * Fetches the on-chain state of `program` to determine whether the metadata - * account is canonical (i.e. the caller's authority matches the program's - * upgrade authority) and returns the resolved metadata PDA along with the - * associated program-data account when applicable. - */ -export async function getPdaDetails(input: { - rpc: Rpc; - program: Address; - authority: TransactionSigner | Address; - seed: SeedArgs; -}): Promise { - const authorityAddress = typeof input.authority === 'string' ? input.authority : input.authority.address; - const { authority, programData } = await getProgramAuthority(input.rpc, input.program); - const isCanonical = !!authority && authority === authorityAddress; - const [metadata] = await findMetadataPda({ - program: input.program, - authority: isCanonical ? null : authorityAddress, - seed: input.seed, - }); - return { metadata, isCanonical, programData }; -} - -export function createDefaultTransactionPlannerAndExecutor(input: { - concurrency?: number; - payer: TransactionSigner; - priorityFees?: MicroLamports; - rpc: Parameters[0]['rpc'] & - Rpc; - rpcSubscriptions: Parameters[0]['rpcSubscriptions']; -}) { - const sendAndConfirmTransaction = sendAndConfirmTransactionFactory(input); - const estimateCULimit = estimateComputeUnitLimitFactory(input); - const estimateAndSetCULimit = estimateAndUpdateProvisoryComputeUnitLimitFactory(estimateCULimit); - - const planner = createTransactionPlanner({ - createTransactionMessage: () => - pipe( - createTransactionMessage({ version: 0 }), - m => setTransactionMessageFeePayerSigner(input.payer, m), - m => fillProvisorySetComputeUnitLimitInstruction(m), - m => (input.priorityFees ? setTransactionMessageComputeUnitPrice(input.priorityFees, m) : m), - ), - }); - - const executor = createTransactionPlanExecutor({ - executeTransactionMessage: limitFunction(async (context, message, config) => { - const { value: latestBlockhash } = await input.rpc.getLatestBlockhash().send(); - const transaction = await pipe( - setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message), - m => (context.message = m), - async m => await estimateAndSetCULimit(m, config), - async m => (context.message = await m), - async m => await signTransactionMessageWithSigners(await m, config), - ); - context.transaction = transaction; - assertIsSendableTransaction(transaction); - assertIsTransactionWithBlockhashLifetime(transaction); - await sendAndConfirmTransaction(transaction, { - ...config, - commitment: 'confirmed', - }); - return transaction; - }, input.concurrency ?? 5), - } as TransactionPlanExecutorConfig); - - return { planner, executor }; -} - /** * Returns `true` if the given instruction plan can be planned by the client's * transaction planner without throwing — i.e. it fits within a single @@ -119,39 +15,3 @@ export async function isValidInstructionPlan(instructionPlan: InstructionPlan, c return false; } } - -function limitFunction( - fn: (...args: TArguments) => PromiseLike, - concurrency: number, -): (...args: TArguments) => Promise { - let running = 0; - const queue: Array<{ - args: TArguments; - resolve: (value: TReturnType) => void; - reject: (reason?: unknown) => void; - }> = []; - - function process() { - // Do nothing if we're still running at max concurrency - // or if there's nothing left to process. - if (running >= concurrency || queue.length === 0) return; - - running++; - const { args, resolve, reject } = queue.shift()!; - - Promise.resolve(fn(...args)) - .then(resolve) - .catch(reject) - .finally(() => { - running--; - process(); - }); - } - - return function (...args) { - return new Promise((resolve, reject) => { - queue.push({ args, resolve, reject }); - process(); - }); - }; -}