From 7b3b93a6b43a0f9aded3832a12fad3611ca31974 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 11 Apr 2025 14:22:12 +0100 Subject: [PATCH 01/60] Add cli-utils file --- clients/js/src/cli-utils.ts | 326 +++++++++++++++++++++++++++++++++++ clients/js/src/cli.ts | 333 ++---------------------------------- 2 files changed, 340 insertions(+), 319 deletions(-) create mode 100644 clients/js/src/cli-utils.ts diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts new file mode 100644 index 0000000..eb120be --- /dev/null +++ b/clients/js/src/cli-utils.ts @@ -0,0 +1,326 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { + address, + Commitment, + createKeyPairSignerFromBytes, + createSolanaRpc, + createSolanaRpcSubscriptions, + KeyPairSigner, + MicroLamports, + Rpc, + RpcSubscriptions, + SolanaRpcApi, + SolanaRpcSubscriptionsApi, +} from '@solana/kit'; +import chalk from 'chalk'; +import { parse as parseYaml } from 'yaml'; +import { Compression, Encoding, Format } from './generated'; +import { + createDefaultTransactionPlanExecutor, + createDefaultTransactionPlanner, + InstructionPlan, + TransactionPlanExecutor, + TransactionPlanner, + TransactionPlanResult, +} from './instructionPlans'; +import { + packDirectData, + PackedData, + packExternalData, + packUrlData, +} from './packData'; + +const LOCALHOST_URL = 'http://127.0.0.1:8899'; +const LOCALHOST_WEBSOCKET_URL = 'ws://127.0.0.1:8900'; + +export type GlobalOptions = { + keypair?: string; + payer?: string; + rpc?: string; + priorityFees?: MicroLamports; +}; + +export type UploadOptions = GlobalOptions & { + nonCanonical: boolean; + file?: string; + url?: string; + account?: string; + accountOffset?: string; + accountLength?: string; + format?: string; + encoding?: string; + compression?: string; + bufferOnly: boolean; +}; + +export type Client = ReadonlyClient & { + authority: KeyPairSigner; + executor: TransactionPlanExecutor; + payer: KeyPairSigner; + planAndExecute: ( + instructionPlan: InstructionPlan + ) => Promise; + planner: TransactionPlanner; +}; + +export async function getClient(options: { + keypair?: string; + payer?: string; + priorityFees?: MicroLamports; + rpc?: string; +}): Promise { + const readonlyClient = getReadonlyClient(options); + const [authority, payer] = await getKeyPairSigners( + options, + readonlyClient.configs + ); + const planner = createDefaultTransactionPlanner({ + feePayer: payer, + computeUnitPrice: options.priorityFees, + }); + const executor = createDefaultTransactionPlanExecutor({ + rpc: readonlyClient.rpc, + rpcSubscriptions: readonlyClient.rpcSubscriptions, + parallelChunkSize: 5, + }); + const planAndExecute = async ( + instructionPlan: InstructionPlan + ): Promise => { + const transactionPlan = await planner(instructionPlan); + return await executor(transactionPlan); + }; + return { + ...readonlyClient, + authority, + executor, + payer, + planAndExecute, + planner, + }; +} + +export type ReadonlyClient = { + configs: SolanaConfigs; + rpc: Rpc; + rpcSubscriptions: RpcSubscriptions; +}; + +export function getReadonlyClient(options: { rpc?: string }): ReadonlyClient { + const configs = getSolanaConfigs(); + const rpcUrl = getRpcUrl(options, configs); + const rpcSubscriptionsUrl = getRpcSubscriptionsUrl(rpcUrl, configs); + return { + configs, + rpc: createSolanaRpc(rpcUrl), + rpcSubscriptions: createSolanaRpcSubscriptions(rpcSubscriptionsUrl), + }; +} + +function getRpcUrl(options: { rpc?: string }, configs: SolanaConfigs): string { + if (options.rpc) return options.rpc; + if (configs.json_rpc_url) return configs.json_rpc_url; + return LOCALHOST_URL; +} + +function getRpcSubscriptionsUrl( + rpcUrl: string, + configs: SolanaConfigs +): string { + if (configs.websocket_url) return configs.websocket_url; + if (rpcUrl === LOCALHOST_URL) return LOCALHOST_WEBSOCKET_URL; + return rpcUrl.replace(/^http/, 'ws'); +} + +type SolanaConfigs = { + json_rpc_url?: string; + websocket_url?: string; + keypair_path?: string; + commitment?: Commitment; +}; + +function getSolanaConfigs(): SolanaConfigs { + const path = getSolanaConfigPath(); + if (!fs.existsSync(path)) { + logWarning('Solana config file not found'); + return {}; + } + return parseYaml(fs.readFileSync(getSolanaConfigPath(), 'utf8')); +} + +function getSolanaConfigPath(): string { + return path.join(os.homedir(), '.config', 'solana', 'cli', 'config.yml'); +} + +export async function getKeyPairSigners( + options: { keypair?: string; payer?: string }, + configs: SolanaConfigs +): Promise<[KeyPairSigner, KeyPairSigner]> { + const keypairPath = getKeyPairPath(options, configs); + const keypairPromise = getKeyPairSignerFromPath(keypairPath); + const payerPromise = options.payer + ? getKeyPairSignerFromPath(options.payer) + : keypairPromise; + return await Promise.all([keypairPromise, payerPromise]); +} + +function getKeyPairPath( + options: { keypair?: string }, + configs: SolanaConfigs +): string { + if (options.keypair) return options.keypair; + if (configs.keypair_path) return configs.keypair_path; + return path.join(os.homedir(), '.config', 'solana', 'id.json'); +} + +async function getKeyPairSignerFromPath( + keypairPath: string +): Promise { + if (!fs.existsSync(keypairPath)) { + logErrorAndExit(`Keypair file not found at: ${keypairPath}`); + } + const keypairString = fs.readFileSync(keypairPath, 'utf-8'); + const keypairData = new Uint8Array(JSON.parse(keypairString)); + return await createKeyPairSignerFromBytes(keypairData); +} + +function getCompression(options: { compression?: string }): Compression { + switch (options.compression) { + case 'none': + return Compression.None; + case 'gzip': + return Compression.Gzip; + case undefined: + case 'zlib': + return Compression.Zlib; + default: + logErrorAndExit(`Invalid compression option: ${options.compression}`); + } +} + +function getEncoding(options: { encoding?: string }): Encoding { + switch (options.encoding) { + case 'none': + return Encoding.None; + case undefined: + case 'utf8': + return Encoding.Utf8; + case 'base58': + return Encoding.Base58; + case 'base64': + return Encoding.Base64; + default: + logErrorAndExit(`Invalid encoding option: ${options.encoding}`); + } +} + +export function getFormat(options: { format?: string; file?: string }): Format { + switch (options.format) { + case undefined: + return getFormatFromFile(options.file); + case 'none': + return Format.None; + case 'json': + return Format.Json; + case 'yaml': + return Format.Yaml; + case 'toml': + return Format.Toml; + default: + logErrorAndExit(`Invalid format option: ${options.format}`); + } +} + +function getFormatFromFile(file: string | undefined): Format { + if (!file) return Format.None; + const extension = path.extname(file); + switch (extension) { + case '.json': + return Format.Json; + case '.yaml': + case '.yml': + return Format.Yaml; + case '.toml': + return Format.Toml; + default: + return Format.None; + } +} + +export function getPackedData( + content: string | undefined, + options: UploadOptions +): PackedData { + const compression = getCompression(options); + const encoding = getEncoding(options); + let packData: PackedData | null = null; + const assertSingleUse = () => { + if (packData) { + logErrorAndExit( + 'Multiple data sources provided. Use only one of: `[content]`, `--file `, `--url ` or `--account
` to provide data.' + ); + } + }; + + if (content) { + packData = packDirectData({ content, compression, encoding }); + } + if (options.file) { + assertSingleUse(); + if (!fs.existsSync(options.file)) { + logErrorAndExit(`File not found: ${options.file}`); + } + const fileContent = fs.readFileSync(options.file, 'utf-8'); + packData = packDirectData({ content: fileContent, compression, encoding }); + } + if (options.url) { + assertSingleUse(); + packData = packUrlData({ url: options.url, compression, encoding }); + } + if (options.account) { + assertSingleUse(); + packData = packExternalData({ + address: address(options.account), + offset: options.accountOffset + ? parseInt(options.accountOffset) + : undefined, + length: options.accountLength + ? parseInt(options.accountLength) + : undefined, + compression, + encoding, + }); + } + + if (!packData) { + logErrorAndExit( + 'No data provided. Use `[content]`, `--file `, `--url ` or `--account
` to provide data.' + ); + } + + return packData; +} + +export function writeFile(filepath: string, content: string): void { + fs.mkdirSync(path.dirname(filepath), { recursive: true }); + fs.writeFileSync(filepath, content); +} + +export function logSuccess(message: string): void { + console.warn(chalk.green(`[Success] `) + message); +} + +export function logWarning(message: string): void { + console.warn(chalk.yellow(`[Warning] `) + message); +} + +export function logError(message: string): void { + console.error(chalk.red(`[Error] `) + message); +} + +export function logErrorAndExit(message: string): never { + logError(message); + process.exit(1); +} diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 6e8ca71..b1a1c31 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -1,69 +1,41 @@ #!/usr/bin/env node -import fs from 'fs'; -import os from 'os'; -import path from 'path'; - import { Address, address, - Commitment, - createKeyPairSignerFromBytes, - createSolanaRpc, - createSolanaRpcSubscriptions, getBase58Decoder, getBase64Decoder, getTransactionEncoder, isSolanaError, - KeyPairSigner, MicroLamports, - Rpc, - RpcSubscriptions, - SolanaRpcApi, - SolanaRpcSubscriptionsApi, Transaction, } from '@solana/kit'; import chalk from 'chalk'; import { Command, Option } from 'commander'; -import { parse as parseYaml } from 'yaml'; +import { + getClient, + getFormat, + getKeyPairSigners, + getPackedData, + getReadonlyClient, + GlobalOptions, + logErrorAndExit, + logSuccess, + UploadOptions, + writeFile, +} from './cli-utils'; import { downloadMetadata } from './downloadMetadata'; import { - Compression, - Encoding, - Format, getCloseInstruction, getSetAuthorityInstruction, getSetImmutableInstruction, } from './generated'; -import { - createDefaultTransactionPlanExecutor, - createDefaultTransactionPlanner, - InstructionPlan, - sequentialInstructionPlan, - TransactionPlanExecutor, - TransactionPlanner, - TransactionPlanResult, -} from './instructionPlans'; +import { sequentialInstructionPlan } from './instructionPlans'; import { getPdaDetails } from './internals'; -import { - packDirectData, - PackedData, - packExternalData, - packUrlData, -} from './packData'; import { uploadMetadata } from './uploadMetadata'; import { getProgramAuthority } from './utils'; -const LOCALHOST_URL = 'http://127.0.0.1:8899'; -const LOCALHOST_WEBSOCKET_URL = 'ws://127.0.0.1:8900'; - // Define the CLI program. -type GlobalOptions = { - keypair?: string; - payer?: string; - rpc?: string; - priorityFees?: MicroLamports; -}; const program = new Command(); program .name('program-metadata') @@ -90,18 +62,6 @@ program ); // Upload metadata command. -type UploadOptions = GlobalOptions & { - nonCanonical: boolean; - file?: string; - url?: string; - account?: string; - accountOffset?: string; - accountLength?: string; - format?: string; - encoding?: string; - compression?: string; - bufferOnly: boolean; -}; program .command('upload') .argument('', 'Seed for the metadata account') @@ -252,8 +212,7 @@ program authority ); if (options.output) { - fs.mkdirSync(path.dirname(options.output), { recursive: true }); - fs.writeFileSync(options.output, content); + writeFile(options.output, content); logSuccess(`Metadata content saved to ${chalk.bold(options.output)}`); } else { console.log(content); @@ -460,268 +419,4 @@ program // TODO }); -type Client = ReadonlyClient & { - authority: KeyPairSigner; - executor: TransactionPlanExecutor; - payer: KeyPairSigner; - planAndExecute: ( - instructionPlan: InstructionPlan - ) => Promise; - planner: TransactionPlanner; -}; - -async function getClient(options: { - keypair?: string; - payer?: string; - priorityFees?: MicroLamports; - rpc?: string; -}): Promise { - const readonlyClient = getReadonlyClient(options); - const [authority, payer] = await getKeyPairSigners( - options, - readonlyClient.configs - ); - const planner = createDefaultTransactionPlanner({ - feePayer: payer, - computeUnitPrice: options.priorityFees, - }); - const executor = createDefaultTransactionPlanExecutor({ - rpc: readonlyClient.rpc, - rpcSubscriptions: readonlyClient.rpcSubscriptions, - parallelChunkSize: 5, - }); - const planAndExecute = async ( - instructionPlan: InstructionPlan - ): Promise => { - const transactionPlan = await planner(instructionPlan); - return await executor(transactionPlan); - }; - return { - ...readonlyClient, - authority, - executor, - payer, - planAndExecute, - planner, - }; -} - -type ReadonlyClient = { - configs: SolanaConfigs; - rpc: Rpc; - rpcSubscriptions: RpcSubscriptions; -}; - -function getReadonlyClient(options: { rpc?: string }): ReadonlyClient { - const configs = getSolanaConfigs(); - const rpcUrl = getRpcUrl(options, configs); - const rpcSubscriptionsUrl = getRpcSubscriptionsUrl(rpcUrl, configs); - return { - configs, - rpc: createSolanaRpc(rpcUrl), - rpcSubscriptions: createSolanaRpcSubscriptions(rpcSubscriptionsUrl), - }; -} - -function getRpcUrl(options: { rpc?: string }, configs: SolanaConfigs): string { - if (options.rpc) return options.rpc; - if (configs.json_rpc_url) return configs.json_rpc_url; - return LOCALHOST_URL; -} - -function getRpcSubscriptionsUrl( - rpcUrl: string, - configs: SolanaConfigs -): string { - if (configs.websocket_url) return configs.websocket_url; - if (rpcUrl === LOCALHOST_URL) return LOCALHOST_WEBSOCKET_URL; - return rpcUrl.replace(/^http/, 'ws'); -} - -type SolanaConfigs = { - json_rpc_url?: string; - websocket_url?: string; - keypair_path?: string; - commitment?: Commitment; -}; - -function getSolanaConfigs(): SolanaConfigs { - const path = getSolanaConfigPath(); - if (!fs.existsSync(path)) { - logWarning('Solana config file not found'); - return {}; - } - return parseYaml(fs.readFileSync(getSolanaConfigPath(), 'utf8')); -} - -function getSolanaConfigPath(): string { - return path.join(os.homedir(), '.config', 'solana', 'cli', 'config.yml'); -} - -async function getKeyPairSigners( - options: { keypair?: string; payer?: string }, - configs: SolanaConfigs -): Promise<[KeyPairSigner, KeyPairSigner]> { - const keypairPath = getKeyPairPath(options, configs); - const keypairPromise = getKeyPairSignerFromPath(keypairPath); - const payerPromise = options.payer - ? getKeyPairSignerFromPath(options.payer) - : keypairPromise; - return await Promise.all([keypairPromise, payerPromise]); -} - -function getKeyPairPath( - options: { keypair?: string }, - configs: SolanaConfigs -): string { - if (options.keypair) return options.keypair; - if (configs.keypair_path) return configs.keypair_path; - return path.join(os.homedir(), '.config', 'solana', 'id.json'); -} - -async function getKeyPairSignerFromPath( - keypairPath: string -): Promise { - if (!fs.existsSync(keypairPath)) { - logErrorAndExit(`Keypair file not found at: ${keypairPath}`); - } - const keypairString = fs.readFileSync(keypairPath, 'utf-8'); - const keypairData = new Uint8Array(JSON.parse(keypairString)); - return await createKeyPairSignerFromBytes(keypairData); -} - -function getCompression(options: { compression?: string }): Compression { - switch (options.compression) { - case 'none': - return Compression.None; - case 'gzip': - return Compression.Gzip; - case undefined: - case 'zlib': - return Compression.Zlib; - default: - logErrorAndExit(`Invalid compression option: ${options.compression}`); - } -} - -function getEncoding(options: { encoding?: string }): Encoding { - switch (options.encoding) { - case 'none': - return Encoding.None; - case undefined: - case 'utf8': - return Encoding.Utf8; - case 'base58': - return Encoding.Base58; - case 'base64': - return Encoding.Base64; - default: - logErrorAndExit(`Invalid encoding option: ${options.encoding}`); - } -} - -function getFormat(options: { format?: string; file?: string }): Format { - switch (options.format) { - case undefined: - return getFormatFromFile(options.file); - case 'none': - return Format.None; - case 'json': - return Format.Json; - case 'yaml': - return Format.Yaml; - case 'toml': - return Format.Toml; - default: - logErrorAndExit(`Invalid format option: ${options.format}`); - } -} - -function getFormatFromFile(file: string | undefined): Format { - if (!file) return Format.None; - const extension = path.extname(file); - switch (extension) { - case '.json': - return Format.Json; - case '.yaml': - case '.yml': - return Format.Yaml; - case '.toml': - return Format.Toml; - default: - return Format.None; - } -} - -function getPackedData( - content: string | undefined, - options: UploadOptions -): PackedData { - const compression = getCompression(options); - const encoding = getEncoding(options); - let packData: PackedData | null = null; - const assertSingleUse = () => { - if (packData) { - logErrorAndExit( - 'Multiple data sources provided. Use only one of: `[content]`, `--file `, `--url ` or `--account
` to provide data.' - ); - } - }; - - if (content) { - packData = packDirectData({ content, compression, encoding }); - } - if (options.file) { - assertSingleUse(); - if (!fs.existsSync(options.file)) { - logErrorAndExit(`File not found: ${options.file}`); - } - const fileContent = fs.readFileSync(options.file, 'utf-8'); - packData = packDirectData({ content: fileContent, compression, encoding }); - } - if (options.url) { - assertSingleUse(); - packData = packUrlData({ url: options.url, compression, encoding }); - } - if (options.account) { - assertSingleUse(); - packData = packExternalData({ - address: address(options.account), - offset: options.accountOffset - ? parseInt(options.accountOffset) - : undefined, - length: options.accountLength - ? parseInt(options.accountLength) - : undefined, - compression, - encoding, - }); - } - - if (!packData) { - logErrorAndExit( - 'No data provided. Use `[content]`, `--file `, `--url ` or `--account
` to provide data.' - ); - } - - return packData; -} - -function logSuccess(message: string): void { - console.warn(chalk.green(`[Success] `) + message); -} - -function logWarning(message: string): void { - console.warn(chalk.yellow(`[Warning] `) + message); -} - -function logError(message: string): void { - console.error(chalk.red(`[Error] `) + message); -} - -function logErrorAndExit(message: string): never { - logError(message); - process.exit(1); -} - program.parse(); From e3aaa091c0e13db89b818a507cb75a3a417dbe40 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 15:36:06 +0100 Subject: [PATCH 02/60] Add cause upon failed transaction plan execution --- .../transactionPlanExecutorBase.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/clients/js/src/instructionPlans/transactionPlanExecutorBase.ts b/clients/js/src/instructionPlans/transactionPlanExecutorBase.ts index d8068e6..5d60e40 100644 --- a/clients/js/src/instructionPlans/transactionPlanExecutorBase.ts +++ b/clients/js/src/instructionPlans/transactionPlanExecutorBase.ts @@ -48,6 +48,7 @@ export function createBaseTransactionPlanExecutor( result: TransactionPlanResult; }; error.result = result; + error.cause = findErrorFromTransactionPlanResult(result); throw error; } @@ -169,3 +170,17 @@ async function traverseSingle( }; } } + +function findErrorFromTransactionPlanResult( + result: TransactionPlanResult +): Error | undefined { + if (result.kind === 'single') { + return result.status.kind === 'failed' ? result.status.error : undefined; + } + for (const plan of result.plans) { + const error = findErrorFromTransactionPlanResult(plan); + if (error) { + return error; + } + } +} From dd190d2792e4c2d4dbf096c52c2c3b69c6a0e1af Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 15:43:24 +0100 Subject: [PATCH 03/60] Fix RPC to RPC Subscriptions URL logic --- clients/js/src/cli-utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts index eb120be..35eb688 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli-utils.ts @@ -34,7 +34,6 @@ import { } from './packData'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; -const LOCALHOST_WEBSOCKET_URL = 'ws://127.0.0.1:8900'; export type GlobalOptions = { keypair?: string; @@ -130,8 +129,7 @@ function getRpcSubscriptionsUrl( configs: SolanaConfigs ): string { if (configs.websocket_url) return configs.websocket_url; - if (rpcUrl === LOCALHOST_URL) return LOCALHOST_WEBSOCKET_URL; - return rpcUrl.replace(/^http/, 'ws'); + return rpcUrl.replace(/^http/, 'ws').replace(/:8899$/, ':8900'); } type SolanaConfigs = { From 2decb73463bb2945de2551d6da2179c7f4e031d2 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 17:09:38 +0100 Subject: [PATCH 04/60] Extract global options --- clients/js/src/cli-options.ts | 42 +++++++++++++++++++++++++++++++++++ clients/js/src/cli-utils.ts | 8 +------ clients/js/src/cli.ts | 24 +++----------------- 3 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 clients/js/src/cli-options.ts diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts new file mode 100644 index 0000000..a227bfd --- /dev/null +++ b/clients/js/src/cli-options.ts @@ -0,0 +1,42 @@ +import type { MicroLamports } from '@solana/kit'; +import { Command, Option } from 'commander'; + +export type GlobalOptions = KeypairOption & + PayerOption & + PriorityFeesOption & + RpcOption; + +export function setGlobalOptions(command: Command) { + command.addOption(keypairOption); + command.addOption(payerOption); + command.addOption(priorityFeesOption); + command.addOption(rpcOption); +} + +export type KeypairOption = { keypair?: string }; +export const keypairOption = new Option( + '-k, --keypair ', + 'Path to keypair file' +).default(undefined, 'solana config'); + +export type PayerOption = { payer?: string }; +export const payerOption = new Option( + '-p, --payer ', + 'Path to keypair file of transaction fee and storage payer' +).default(undefined, 'keypair option'); + +export type PriorityFeesOption = { priorityFees?: MicroLamports }; +export const priorityFeesOption = new Option( + '--priority-fees ', + 'Priority fees in micro-lamports per compute unit for sending transactions' +) + .default('100000') + .argParser((value: string | undefined) => + value !== undefined ? (BigInt(value) as MicroLamports) : undefined + ); + +export type RpcOption = { rpc?: string }; +export const rpcOption = new Option('--rpc ', 'RPC URL').default( + undefined, + 'solana config or localhost' +); diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts index 35eb688..955b507 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli-utils.ts @@ -32,16 +32,10 @@ import { packExternalData, packUrlData, } from './packData'; +import { GlobalOptions } from './cli-options'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; -export type GlobalOptions = { - keypair?: string; - payer?: string; - rpc?: string; - priorityFees?: MicroLamports; -}; - export type UploadOptions = GlobalOptions & { nonCanonical: boolean; file?: string; diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index b1a1c31..50e7c6c 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -7,18 +7,17 @@ import { getBase64Decoder, getTransactionEncoder, isSolanaError, - MicroLamports, Transaction, } from '@solana/kit'; import chalk from 'chalk'; import { Command, Option } from 'commander'; +import { GlobalOptions, setGlobalOptions } from './cli-options'; import { getClient, getFormat, getKeyPairSigners, getPackedData, getReadonlyClient, - GlobalOptions, logErrorAndExit, logSuccess, UploadOptions, @@ -41,25 +40,8 @@ program .name('program-metadata') .description('CLI to manage Solana program metadata and IDLs') .version(__VERSION__) - .option( - '-k, --keypair ', - 'Path to keypair file. (default: solana config)' - ) - .option( - '-p, --payer ', - 'Path to keypair file of transaction fee and storage payer. (default: keypair)' - ) - .option('--rpc ', 'RPC URL. (default: solana config or localhost)') - .addOption( - new Option( - '--priority-fees ', - 'Priority fees per compute unit for sending transactions' - ) - .default('100000') - .argParser((value: string | undefined) => - value !== undefined ? (BigInt(value) as MicroLamports) : undefined - ) - ); + .configureHelp({ showGlobalOptions: true }); +setGlobalOptions(program); // Upload metadata command. program From f985c59a5f1a2754ca65e155aff4a8a115ccec0f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 17:31:24 +0100 Subject: [PATCH 05/60] wip --- clients/js/src/cli-options.ts | 86 +++++++++++++++++++++++++++++++++++ clients/js/src/cli-utils.ts | 50 ++------------------ clients/js/src/cli.ts | 25 ++++------ 3 files changed, 99 insertions(+), 62 deletions(-) diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index a227bfd..5061b73 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -1,5 +1,7 @@ import type { MicroLamports } from '@solana/kit'; import { Command, Option } from 'commander'; +import { Compression, Encoding, Format } from './generated'; +import { logErrorAndExit } from './cli-utils'; export type GlobalOptions = KeypairOption & PayerOption & @@ -40,3 +42,87 @@ export const rpcOption = new Option('--rpc ', 'RPC URL').default( undefined, 'solana config or localhost' ); + +export type UploadOptions = { + nonCanonical: boolean; + file?: string; + url?: string; + account?: string; + accountOffset?: string; + accountLength?: string; + format?: string; + bufferOnly: boolean; +} & CompressionOption & + EncodingOption; + +export function setUploadOptions(command: Command) { + command.addOption(keypairOption); // TODO +} + +export type CompressionOption = { compression: Compression }; +export const compressionOption = new Option( + '--compression ', + 'Describes how to compress the data' +) + .choices(['none', 'gzip', 'zlib']) + .default('zlib') + .argParser((value: string | undefined): Compression => { + switch (value) { + case 'none': + return Compression.None; + case 'gzip': + return Compression.Gzip; + case undefined: + case 'zlib': + return Compression.Zlib; + default: + logErrorAndExit(`Invalid compression option: ${value}`); + } + }); + +export type EncodingOption = { encoding: Encoding }; +export const encodingOption = new Option( + '--encoding ', + 'Describes how to encode the data' +) + .choices(['none', 'utf8', 'base58', 'base64']) + .default('utf8') + .argParser((value: string | undefined): Encoding => { + switch (value) { + case 'none': + return Encoding.None; + case undefined: + case 'utf8': + return Encoding.Utf8; + case 'base58': + return Encoding.Base58; + case 'base64': + return Encoding.Base64; + default: + logErrorAndExit(`Invalid encoding option: ${value}`); + } + }); + +export type FormatOption = { format?: Format }; +export const formatOption = new Option( + '--format ', + 'The format of the provided data' +) + .choices(['none', 'json', 'yaml', 'toml']) + .default(undefined, 'the file extension or "none"') + .argParser((value: string | undefined): Format | undefined => { + switch (value) { + case undefined: + return undefined; + case 'none': + return Format.None; + case 'json': + return Format.Json; + case 'yaml': + return Format.Yaml; + case 'toml': + return Format.Toml; + default: + logErrorAndExit(`Invalid format option: ${value}`); + } + }); diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts index 955b507..a767335 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli-utils.ts @@ -17,7 +17,7 @@ import { } from '@solana/kit'; import chalk from 'chalk'; import { parse as parseYaml } from 'yaml'; -import { Compression, Encoding, Format } from './generated'; +import { Format } from './generated'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, @@ -32,23 +32,10 @@ import { packExternalData, packUrlData, } from './packData'; -import { GlobalOptions } from './cli-options'; +import { UploadOptions } from './cli-options'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; -export type UploadOptions = GlobalOptions & { - nonCanonical: boolean; - file?: string; - url?: string; - account?: string; - accountOffset?: string; - accountLength?: string; - format?: string; - encoding?: string; - compression?: string; - bufferOnly: boolean; -}; - export type Client = ReadonlyClient & { authority: KeyPairSigner; executor: TransactionPlanExecutor; @@ -178,36 +165,6 @@ async function getKeyPairSignerFromPath( return await createKeyPairSignerFromBytes(keypairData); } -function getCompression(options: { compression?: string }): Compression { - switch (options.compression) { - case 'none': - return Compression.None; - case 'gzip': - return Compression.Gzip; - case undefined: - case 'zlib': - return Compression.Zlib; - default: - logErrorAndExit(`Invalid compression option: ${options.compression}`); - } -} - -function getEncoding(options: { encoding?: string }): Encoding { - switch (options.encoding) { - case 'none': - return Encoding.None; - case undefined: - case 'utf8': - return Encoding.Utf8; - case 'base58': - return Encoding.Base58; - case 'base64': - return Encoding.Base64; - default: - logErrorAndExit(`Invalid encoding option: ${options.encoding}`); - } -} - export function getFormat(options: { format?: string; file?: string }): Format { switch (options.format) { case undefined: @@ -245,8 +202,7 @@ export function getPackedData( content: string | undefined, options: UploadOptions ): PackedData { - const compression = getCompression(options); - const encoding = getEncoding(options); + const { compression, encoding } = options; let packData: PackedData | null = null; const assertSingleUse = () => { if (packData) { diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 50e7c6c..cbc3119 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -11,7 +11,13 @@ import { } from '@solana/kit'; import chalk from 'chalk'; import { Command, Option } from 'commander'; -import { GlobalOptions, setGlobalOptions } from './cli-options'; +import { + compressionOption, + encodingOption, + GlobalOptions, + setGlobalOptions, + UploadOptions, +} from './cli-options'; import { getClient, getFormat, @@ -20,7 +26,6 @@ import { getReadonlyClient, logErrorAndExit, logSuccess, - UploadOptions, writeFile, } from './cli-utils'; import { downloadMetadata } from './downloadMetadata'; @@ -85,18 +90,8 @@ program 'The format of the provided data. (default: the file extension or "none")' ).choices(['none', 'json', 'yaml', 'toml']) ) - .addOption( - new Option( - '--encoding ', - 'Describes how to encode the data. (default: "utf8")' - ).choices(['none', 'utf8', 'base58', 'base64']) - ) - .addOption( - new Option( - '--compression ', - 'Describes how to compress the data. (default: "zlib")' - ).choices(['none', 'gzip', 'zlib']) - ) + .addOption(encodingOption) + .addOption(compressionOption) .option( '--buffer-only', 'Only create the buffer and export the transaction that sets the buffer.', @@ -110,7 +105,7 @@ program _, cmd: Command ) => { - const options = cmd.optsWithGlobals() as UploadOptions; + const options = cmd.optsWithGlobals() as UploadOptions & GlobalOptions; const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( client.rpc, From 53c53ced2349180152ddd994ea7a95fea80b1eff Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 17:55:43 +0100 Subject: [PATCH 06/60] wip --- clients/js/src/cli-options.ts | 28 ++++++++++++++-------------- clients/js/src/cli-utils.ts | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index 5061b73..d49afc1 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -9,10 +9,11 @@ export type GlobalOptions = KeypairOption & RpcOption; export function setGlobalOptions(command: Command) { - command.addOption(keypairOption); - command.addOption(payerOption); - command.addOption(priorityFeesOption); - command.addOption(rpcOption); + return command + .addOption(keypairOption) + .addOption(payerOption) + .addOption(priorityFeesOption) + .addOption(rpcOption); } export type KeypairOption = { keypair?: string }; @@ -56,7 +57,10 @@ export type UploadOptions = { EncodingOption; export function setUploadOptions(command: Command) { - command.addOption(keypairOption); // TODO + return command + .addOption(compressionOption) + .addOption(encodingOption) + .addOption(formatOption); } export type CompressionOption = { compression: Compression }; @@ -65,14 +69,13 @@ export const compressionOption = new Option( 'Describes how to compress the data' ) .choices(['none', 'gzip', 'zlib']) - .default('zlib') - .argParser((value: string | undefined): Compression => { + .default(Compression.Zlib, 'zlib') + .argParser((value: string): Compression => { switch (value) { case 'none': return Compression.None; case 'gzip': return Compression.Gzip; - case undefined: case 'zlib': return Compression.Zlib; default: @@ -86,12 +89,11 @@ export const encodingOption = new Option( 'Describes how to encode the data' ) .choices(['none', 'utf8', 'base58', 'base64']) - .default('utf8') - .argParser((value: string | undefined): Encoding => { + .default(Encoding.Utf8, 'utf8') + .argParser((value: string): Encoding => { switch (value) { case 'none': return Encoding.None; - case undefined: case 'utf8': return Encoding.Utf8; case 'base58': @@ -110,10 +112,8 @@ export const formatOption = new Option( ) .choices(['none', 'json', 'yaml', 'toml']) .default(undefined, 'the file extension or "none"') - .argParser((value: string | undefined): Format | undefined => { + .argParser((value: string): Format => { switch (value) { - case undefined: - return undefined; case 'none': return Format.None; case 'json': diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts index a767335..555c9ac 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli-utils.ts @@ -17,6 +17,7 @@ import { } from '@solana/kit'; import chalk from 'chalk'; import { parse as parseYaml } from 'yaml'; +import { UploadOptions } from './cli-options'; import { Format } from './generated'; import { createDefaultTransactionPlanExecutor, @@ -32,7 +33,6 @@ import { packExternalData, packUrlData, } from './packData'; -import { UploadOptions } from './cli-options'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; From d7d429f8d30c37c804b3ac7286788b3e217f2eea Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 18:00:50 +0100 Subject: [PATCH 07/60] wip --- clients/js/src/cli-options.ts | 4 ++-- clients/js/src/cli-utils.ts | 19 +------------------ clients/js/src/cli.ts | 14 +++++--------- 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index d49afc1..909bcd5 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -51,10 +51,10 @@ export type UploadOptions = { account?: string; accountOffset?: string; accountLength?: string; - format?: string; bufferOnly: boolean; } & CompressionOption & - EncodingOption; + EncodingOption & + FormatOption; export function setUploadOptions(command: Command) { return command diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts index 555c9ac..320235c 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli-utils.ts @@ -165,24 +165,7 @@ async function getKeyPairSignerFromPath( return await createKeyPairSignerFromBytes(keypairData); } -export function getFormat(options: { format?: string; file?: string }): Format { - switch (options.format) { - case undefined: - return getFormatFromFile(options.file); - case 'none': - return Format.None; - case 'json': - return Format.Json; - case 'yaml': - return Format.Yaml; - case 'toml': - return Format.Toml; - default: - logErrorAndExit(`Invalid format option: ${options.format}`); - } -} - -function getFormatFromFile(file: string | undefined): Format { +export function getFormatFromFile(file: string | undefined): Format { if (!file) return Format.None; const extension = path.extname(file); switch (extension) { diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index cbc3119..6de2e09 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -10,17 +10,18 @@ import { Transaction, } from '@solana/kit'; import chalk from 'chalk'; -import { Command, Option } from 'commander'; +import { Command } from 'commander'; import { compressionOption, encodingOption, + formatOption, GlobalOptions, setGlobalOptions, UploadOptions, } from './cli-options'; import { getClient, - getFormat, + getFormatFromFile, getKeyPairSigners, getPackedData, getReadonlyClient, @@ -84,12 +85,7 @@ program '--account-length ', 'The length of the data on the provided account. (default: the rest of the data)' ) - .addOption( - new Option( - '--format ', - 'The format of the provided data. (default: the file extension or "none")' - ).choices(['none', 'json', 'yaml', 'toml']) - ) + .addOption(formatOption) .addOption(encodingOption) .addOption(compressionOption) .option( @@ -126,7 +122,7 @@ program authority: client.authority, program, seed, - format: getFormat(options), + format: options.format ?? getFormatFromFile(options.file), closeBuffer: true, priorityFees: options.priorityFees, }); From 6ce6a983762d7334ceb697225440fa4115f46840 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 18:09:13 +0100 Subject: [PATCH 08/60] wip --- clients/js/src/cli-options.ts | 2 +- clients/js/src/cli-utils.ts | 22 +++++++++++----------- clients/js/src/cli.ts | 14 +++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index 909bcd5..58d1736 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -46,7 +46,7 @@ export const rpcOption = new Option('--rpc ', 'RPC URL').default( export type UploadOptions = { nonCanonical: boolean; - file?: string; + text?: string; url?: string; account?: string; accountOffset?: string; diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts index 320235c..c4e2630 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli-utils.ts @@ -182,7 +182,7 @@ export function getFormatFromFile(file: string | undefined): Format { } export function getPackedData( - content: string | undefined, + file: string | undefined, options: UploadOptions ): PackedData { const { compression, encoding } = options; @@ -190,22 +190,22 @@ export function getPackedData( const assertSingleUse = () => { if (packData) { logErrorAndExit( - 'Multiple data sources provided. Use only one of: `[content]`, `--file `, `--url ` or `--account
` to provide data.' + 'Multiple data sources provided. Use only one of: `[file]`, `--text `, `--url ` or `--account
` to provide data.' ); } }; - if (content) { - packData = packDirectData({ content, compression, encoding }); - } - if (options.file) { - assertSingleUse(); - if (!fs.existsSync(options.file)) { - logErrorAndExit(`File not found: ${options.file}`); + if (file) { + if (!fs.existsSync(file)) { + logErrorAndExit(`File not found: ${file}`); } - const fileContent = fs.readFileSync(options.file, 'utf-8'); + const fileContent = fs.readFileSync(file, 'utf-8'); packData = packDirectData({ content: fileContent, compression, encoding }); } + if (options.text) { + assertSingleUse(); + packData = packDirectData({ content: options.text, compression, encoding }); + } if (options.url) { assertSingleUse(); packData = packUrlData({ url: options.url, compression, encoding }); @@ -227,7 +227,7 @@ export function getPackedData( if (!packData) { logErrorAndExit( - 'No data provided. Use `[content]`, `--file `, `--url ` or `--account
` to provide data.' + 'No data provided. Use `[file]`, `--text `, `--url ` or `--account
` to provide data.' ); } diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 6de2e09..58b516d 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -59,8 +59,8 @@ program address ) .argument( - '[content]', - 'Direct content to upload. See options for other sources such as --file, --url and --account.' + '[file]', + 'The path to the file to upload (creates a "direct" data source). See options for other sources such as --text, --url and --account.' ) .description('Upload metadata') .option( @@ -69,8 +69,8 @@ program false ) .option( - '--file ', - 'The path to the file to upload (creates a "direct" data source).' + '--text ', + 'Direct content to upload (creates a "direct" data source).' ) .option('--url ', 'The url to upload (creates a "url" data source).') .option( @@ -97,7 +97,7 @@ program async ( seed: string, program: Address, - content: string | undefined, + file: string | undefined, _, cmd: Command ) => { @@ -117,12 +117,12 @@ program } await uploadMetadata({ ...client, - ...getPackedData(content, options), + ...getPackedData(file, options), payer: client.payer, authority: client.authority, program, seed, - format: options.format ?? getFormatFromFile(options.file), + format: options.format ?? getFormatFromFile(file), closeBuffer: true, priorityFees: options.priorityFees, }); From 33509f46004d05eaf149beb4f64f33f71bbf3581 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 18:24:58 +0100 Subject: [PATCH 09/60] wip --- clients/js/src/cli-options.ts | 73 +++++++++++++++++++++++++++-------- clients/js/src/cli.ts | 32 +++------------ 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index 58d1736..c32b536 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -19,19 +19,19 @@ export function setGlobalOptions(command: Command) { export type KeypairOption = { keypair?: string }; export const keypairOption = new Option( '-k, --keypair ', - 'Path to keypair file' + 'Path to keypair file.' ).default(undefined, 'solana config'); export type PayerOption = { payer?: string }; export const payerOption = new Option( '-p, --payer ', - 'Path to keypair file of transaction fee and storage payer' + 'Path to keypair file of transaction fee and storage payer.' ).default(undefined, 'keypair option'); export type PriorityFeesOption = { priorityFees?: MicroLamports }; export const priorityFeesOption = new Option( '--priority-fees ', - 'Priority fees in micro-lamports per compute unit for sending transactions' + 'Priority fees in micro-lamports per compute unit for sending transactions.' ) .default('100000') .argParser((value: string | undefined) => @@ -39,34 +39,73 @@ export const priorityFeesOption = new Option( ); export type RpcOption = { rpc?: string }; -export const rpcOption = new Option('--rpc ', 'RPC URL').default( +export const rpcOption = new Option('--rpc ', 'RPC URL.').default( undefined, 'solana config or localhost' ); export type UploadOptions = { nonCanonical: boolean; - text?: string; - url?: string; - account?: string; - accountOffset?: string; - accountLength?: string; bufferOnly: boolean; -} & CompressionOption & +} & TextOption & + UrlOption & + AccountOption & + AccountOffsetOption & + AccountLengthOption & + CompressionOption & EncodingOption & FormatOption; export function setUploadOptions(command: Command) { - return command - .addOption(compressionOption) - .addOption(encodingOption) - .addOption(formatOption); + return ( + command + // Data sources. + .addOption(textOption) + .addOption(urlOption) + .addOption(accountOption) + .addOption(accountOffsetOption) + .addOption(accountLengthOption) + // Enums. + .addOption(compressionOption) + .addOption(encodingOption) + .addOption(formatOption) + ); } +export type TextOption = { text?: string }; +export const textOption = new Option( + '--text ', + 'Direct content to upload (creates a "direct" data source).' +); + +export type UrlOption = { url?: string }; +export const urlOption = new Option( + '--url ', + 'The url to upload (creates a "url" data source).' +); + +export type AccountOption = { account?: string }; +export const accountOption = new Option( + '--account
', + 'The account address to upload (creates an "external" data source). See also: "--account-offset" and "--account-length".' +); + +export type AccountOffsetOption = { accountOffset?: string }; +export const accountOffsetOption = new Option( + '--account-offset ', + 'The offset in which the data start on the provided account. Requires "--account" to be set.' +).default(undefined, '0'); + +export type AccountLengthOption = { accountLength?: string }; +export const accountLengthOption = new Option( + '--account-length ', + 'The length of the data on the provided account. Requires "--account" to be set.' +).default(undefined, 'the rest of the data'); + export type CompressionOption = { compression: Compression }; export const compressionOption = new Option( '--compression ', - 'Describes how to compress the data' + 'Describes how to compress the data.' ) .choices(['none', 'gzip', 'zlib']) .default(Compression.Zlib, 'zlib') @@ -86,7 +125,7 @@ export const compressionOption = new Option( export type EncodingOption = { encoding: Encoding }; export const encodingOption = new Option( '--encoding ', - 'Describes how to encode the data' + 'Describes how to encode the data.' ) .choices(['none', 'utf8', 'base58', 'base64']) .default(Encoding.Utf8, 'utf8') @@ -108,7 +147,7 @@ export const encodingOption = new Option( export type FormatOption = { format?: Format }; export const formatOption = new Option( '--format ', - 'The format of the provided data' + 'The format of the provided data.' ) .choices(['none', 'json', 'yaml', 'toml']) .default(undefined, 'the file extension or "none"') diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 58b516d..033ef7b 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -12,11 +12,9 @@ import { import chalk from 'chalk'; import { Command } from 'commander'; import { - compressionOption, - encodingOption, - formatOption, GlobalOptions, setGlobalOptions, + setUploadOptions, UploadOptions, } from './cli-options'; import { @@ -50,8 +48,9 @@ program setGlobalOptions(program); // Upload metadata command. -program +const uploadCommand = program .command('upload') + .description('Upload metadata') .argument('', 'Seed for the metadata account') .argument( '', @@ -62,32 +61,13 @@ program '[file]', 'The path to the file to upload (creates a "direct" data source). See options for other sources such as --text, --url and --account.' ) - .description('Upload metadata') .option( '--non-canonical', 'When provided, a non-canonical metadata account will be uploaded using the active keypair as the authority.', false - ) - .option( - '--text ', - 'Direct content to upload (creates a "direct" data source).' - ) - .option('--url ', 'The url to upload (creates a "url" data source).') - .option( - '--account
', - 'The account address to upload (creates an "external" data source).' - ) - .option( - '--account-offset ', - 'The offset in which the data start on the provided account. (default: 0)' - ) - .option( - '--account-length ', - 'The length of the data on the provided account. (default: the rest of the data)' - ) - .addOption(formatOption) - .addOption(encodingOption) - .addOption(compressionOption) + ); +setUploadOptions(uploadCommand); +uploadCommand .option( '--buffer-only', 'Only create the buffer and export the transaction that sets the buffer.', From 6e59d1916b0ca81a0af34c2cd2877c49e599a7d6 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 18:27:19 +0100 Subject: [PATCH 10/60] wip --- clients/js/src/cli-options.ts | 7 ++----- clients/js/src/cli-utils.ts | 4 ++-- clients/js/src/cli.ts | 9 +++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index c32b536..058a570 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -44,10 +44,7 @@ export const rpcOption = new Option('--rpc ', 'RPC URL.').default( 'solana config or localhost' ); -export type UploadOptions = { - nonCanonical: boolean; - bufferOnly: boolean; -} & TextOption & +export type WriteOptions = TextOption & UrlOption & AccountOption & AccountOffsetOption & @@ -56,7 +53,7 @@ export type UploadOptions = { EncodingOption & FormatOption; -export function setUploadOptions(command: Command) { +export function setWriteOptions(command: Command) { return ( command // Data sources. diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts index c4e2630..c2171c7 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli-utils.ts @@ -17,7 +17,7 @@ import { } from '@solana/kit'; import chalk from 'chalk'; import { parse as parseYaml } from 'yaml'; -import { UploadOptions } from './cli-options'; +import { WriteOptions } from './cli-options'; import { Format } from './generated'; import { createDefaultTransactionPlanExecutor, @@ -183,7 +183,7 @@ export function getFormatFromFile(file: string | undefined): Format { export function getPackedData( file: string | undefined, - options: UploadOptions + options: WriteOptions ): PackedData { const { compression, encoding } = options; let packData: PackedData | null = null; diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 033ef7b..0e76287 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -14,8 +14,8 @@ import { Command } from 'commander'; import { GlobalOptions, setGlobalOptions, - setUploadOptions, - UploadOptions, + setWriteOptions, + WriteOptions, } from './cli-options'; import { getClient, @@ -66,7 +66,7 @@ const uploadCommand = program 'When provided, a non-canonical metadata account will be uploaded using the active keypair as the authority.', false ); -setUploadOptions(uploadCommand); +setWriteOptions(uploadCommand); uploadCommand .option( '--buffer-only', @@ -81,7 +81,8 @@ uploadCommand _, cmd: Command ) => { - const options = cmd.optsWithGlobals() as UploadOptions & GlobalOptions; + const options = cmd.optsWithGlobals() as WriteOptions & + GlobalOptions & { nonCanonical: boolean; bufferOnly: boolean }; const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( client.rpc, From 2120fbf3651a42fe919ee3a3b068b2ba2eae5a10 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 18:44:06 +0100 Subject: [PATCH 11/60] wip --- clients/js/src/cli-logs.ts | 18 ++++++++++++++ clients/js/src/cli-options.ts | 28 ++++++++++++++++++++-- clients/js/src/cli-utils.ts | 19 +-------------- clients/js/src/cli.ts | 44 +++++++++++++---------------------- 4 files changed, 61 insertions(+), 48 deletions(-) create mode 100644 clients/js/src/cli-logs.ts diff --git a/clients/js/src/cli-logs.ts b/clients/js/src/cli-logs.ts new file mode 100644 index 0000000..6b6086e --- /dev/null +++ b/clients/js/src/cli-logs.ts @@ -0,0 +1,18 @@ +import chalk from 'chalk'; + +export function logSuccess(message: string): void { + console.warn(chalk.green(`[Success] `) + message); +} + +export function logWarning(message: string): void { + console.warn(chalk.yellow(`[Warning] `) + message); +} + +export function logError(message: string): void { + console.error(chalk.red(`[Error] `) + message); +} + +export function logErrorAndExit(message: string): never { + logError(message); + process.exit(1); +} diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index 058a570..dedf38a 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -1,7 +1,7 @@ -import type { MicroLamports } from '@solana/kit'; +import { address, type Address, type MicroLamports } from '@solana/kit'; import { Command, Option } from 'commander'; import { Compression, Encoding, Format } from './generated'; -import { logErrorAndExit } from './cli-utils'; +import { logErrorAndExit } from './cli-logs'; export type GlobalOptions = KeypairOption & PayerOption & @@ -162,3 +162,27 @@ export const formatOption = new Option( logErrorAndExit(`Invalid format option: ${value}`); } }); + +export type NonCanonicalWriteOption = { nonCanonical: boolean }; +export const nonCanonicalWriteOption = new Option( + '--non-canonical', + 'When provided, a non-canonical metadata account will be closed using the active keypair as the authority.' +).default(false); + +export type NonCanonicalReadOption = { nonCanonical: Address | boolean }; +export const nonCanonicalReadOption = new Option( + '--non-canonical [address]', + 'When provided, a non-canonical metadata account will be downloaded using the provided address or the active keypair as the authority.' +) + .default(false) + .argParser((value: string | undefined): Address | boolean => { + return value === undefined ? true : addressParser(value); + }); + +function addressParser(value: string): Address { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid address: "${value}"`); + } +} diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli-utils.ts index c2171c7..a8666a2 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli-utils.ts @@ -15,7 +15,6 @@ import { SolanaRpcApi, SolanaRpcSubscriptionsApi, } from '@solana/kit'; -import chalk from 'chalk'; import { parse as parseYaml } from 'yaml'; import { WriteOptions } from './cli-options'; import { Format } from './generated'; @@ -33,6 +32,7 @@ import { packExternalData, packUrlData, } from './packData'; +import { logErrorAndExit, logWarning } from './cli-logs'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; @@ -238,20 +238,3 @@ export function writeFile(filepath: string, content: string): void { fs.mkdirSync(path.dirname(filepath), { recursive: true }); fs.writeFileSync(filepath, content); } - -export function logSuccess(message: string): void { - console.warn(chalk.green(`[Success] `) + message); -} - -export function logWarning(message: string): void { - console.warn(chalk.yellow(`[Warning] `) + message); -} - -export function logError(message: string): void { - console.error(chalk.red(`[Error] `) + message); -} - -export function logErrorAndExit(message: string): never { - logError(message); - process.exit(1); -} diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index 0e76287..e238098 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -11,8 +11,13 @@ import { } from '@solana/kit'; import chalk from 'chalk'; import { Command } from 'commander'; +import { logErrorAndExit, logSuccess } from './cli-logs'; import { GlobalOptions, + NonCanonicalReadOption, + nonCanonicalReadOption, + NonCanonicalWriteOption, + nonCanonicalWriteOption, setGlobalOptions, setWriteOptions, WriteOptions, @@ -23,8 +28,6 @@ import { getKeyPairSigners, getPackedData, getReadonlyClient, - logErrorAndExit, - logSuccess, writeFile, } from './cli-utils'; import { downloadMetadata } from './downloadMetadata'; @@ -61,11 +64,7 @@ const uploadCommand = program '[file]', 'The path to the file to upload (creates a "direct" data source). See options for other sources such as --text, --url and --account.' ) - .option( - '--non-canonical', - 'When provided, a non-canonical metadata account will be uploaded using the active keypair as the authority.', - false - ); + .addOption(nonCanonicalWriteOption); setWriteOptions(uploadCommand); uploadCommand .option( @@ -82,7 +81,8 @@ uploadCommand cmd: Command ) => { const options = cmd.optsWithGlobals() as WriteOptions & - GlobalOptions & { nonCanonical: boolean; bufferOnly: boolean }; + GlobalOptions & + NonCanonicalWriteOption & { bufferOnly: boolean }; const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( client.rpc, @@ -130,25 +130,19 @@ uploadCommand ); // Download metadata command. -type DownloadOptions = GlobalOptions & { - output?: string; - nonCanonical?: string | true; -}; +type DownloadOptions = GlobalOptions & + NonCanonicalReadOption & { output?: string }; program .command('download') - .description('Download IDL to file') + .description('Download metadata to file') .argument('', 'Seed for the metadata account') .argument( '', 'Program associated with the metadata account', address ) - .option('-o, --output ', 'Path to save the IDL file') - .option( - '--non-canonical [address]', - 'When provided, a non-canonical metadata account will be downloaded using the provided address or the active keypair as the authority.', - false - ) + .option('-o, --output ', 'Path to save the metadata file') + .addOption(nonCanonicalReadOption) .action(async (seed: string, program: Address, _, cmd: Command) => { const options = cmd.optsWithGlobals() as DownloadOptions; const client = getReadonlyClient(options); @@ -156,7 +150,7 @@ program options.nonCanonical === true ? (await getKeyPairSigners(options, client.configs))[0].address : options.nonCanonical - ? address(options.nonCanonical) + ? options.nonCanonical : undefined; try { const content = await downloadMetadata( @@ -254,9 +248,7 @@ program logSuccess('Additional authority successfully removed'); }); -type SetImmutableOptions = GlobalOptions & { - nonCanonical: boolean; -}; +type SetImmutableOptions = GlobalOptions & NonCanonicalWriteOption; program .command('set-immutable') .description( @@ -268,11 +260,7 @@ program 'Program associated with the metadata account', address ) - .option( - '--non-canonical', - 'When provided, a non-canonical metadata account will be updated using the active keypair as the authority.', - false - ) + .addOption(nonCanonicalWriteOption) .action(async (seed: string, program: Address, _, cmd: Command) => { const options = cmd.optsWithGlobals() as SetImmutableOptions; const client = await getClient(options); From 516cd9f711011813c04fe638a72d7cc95c2355da Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 18:49:33 +0100 Subject: [PATCH 12/60] wip --- clients/js/src/cli-options.ts | 6 ++++++ clients/js/src/cli.ts | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index dedf38a..2569108 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -179,6 +179,12 @@ export const nonCanonicalReadOption = new Option( return value === undefined ? true : addressParser(value); }); +export type OutputOption = { output?: string }; +export const outputOption = new Option( + '-o, --output ', + 'Path to save the retrieved data.' +); + function addressParser(value: string): Address { try { return address(value); diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index e238098..d0a485d 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -18,6 +18,8 @@ import { nonCanonicalReadOption, NonCanonicalWriteOption, nonCanonicalWriteOption, + OutputOption, + outputOption, setGlobalOptions, setWriteOptions, WriteOptions, @@ -130,8 +132,6 @@ uploadCommand ); // Download metadata command. -type DownloadOptions = GlobalOptions & - NonCanonicalReadOption & { output?: string }; program .command('download') .description('Download metadata to file') @@ -141,10 +141,12 @@ program 'Program associated with the metadata account', address ) - .option('-o, --output ', 'Path to save the metadata file') + .addOption(outputOption) .addOption(nonCanonicalReadOption) .action(async (seed: string, program: Address, _, cmd: Command) => { - const options = cmd.optsWithGlobals() as DownloadOptions; + const options = cmd.optsWithGlobals() as GlobalOptions & + NonCanonicalReadOption & + OutputOption; const client = getReadonlyClient(options); const authority = options.nonCanonical === true From 376517ac0f931c5942392b32a0b7c7be57e21192 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 19:01:51 +0100 Subject: [PATCH 13/60] wip --- clients/js/src/cli-arguments.ts | 19 +++++++ clients/js/src/cli-options.ts | 19 ++++--- clients/js/src/cli.ts | 96 ++++++++++++--------------------- 3 files changed, 63 insertions(+), 71 deletions(-) create mode 100644 clients/js/src/cli-arguments.ts diff --git a/clients/js/src/cli-arguments.ts b/clients/js/src/cli-arguments.ts new file mode 100644 index 0000000..cd812eb --- /dev/null +++ b/clients/js/src/cli-arguments.ts @@ -0,0 +1,19 @@ +import { Argument } from 'commander'; +import { address, Address } from '@solana/kit'; +import { logErrorAndExit } from './cli-logs'; + +export const seedArgument = new Argument( + '', + 'Seed of the metadata account (e.g. "idl" for program IDLs).' +); + +export const programArgument = new Argument( + '', + 'Program associated with the metadata account.' +).argParser((value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid program address: "${value}"`); + } +}); diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli-options.ts index 2569108..ff56a15 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli-options.ts @@ -1,7 +1,7 @@ import { address, type Address, type MicroLamports } from '@solana/kit'; import { Command, Option } from 'commander'; -import { Compression, Encoding, Format } from './generated'; import { logErrorAndExit } from './cli-logs'; +import { Compression, Encoding, Format } from './generated'; export type GlobalOptions = KeypairOption & PayerOption & @@ -176,7 +176,14 @@ export const nonCanonicalReadOption = new Option( ) .default(false) .argParser((value: string | undefined): Address | boolean => { - return value === undefined ? true : addressParser(value); + if (value === undefined) { + return true; + } + try { + return address(value); + } catch { + logErrorAndExit(`Invalid non-canonical address: "${value}"`); + } }); export type OutputOption = { output?: string }; @@ -184,11 +191,3 @@ export const outputOption = new Option( '-o, --output ', 'Path to save the retrieved data.' ); - -function addressParser(value: string): Address { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid address: "${value}"`); - } -} diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index d0a485d..d89d9d0 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -37,11 +37,13 @@ import { getCloseInstruction, getSetAuthorityInstruction, getSetImmutableInstruction, + Seed, } from './generated'; import { sequentialInstructionPlan } from './instructionPlans'; import { getPdaDetails } from './internals'; import { uploadMetadata } from './uploadMetadata'; import { getProgramAuthority } from './utils'; +import { programArgument, seedArgument } from './cli-arguments'; // Define the CLI program. const program = new Command(); @@ -55,13 +57,9 @@ setGlobalOptions(program); // Upload metadata command. const uploadCommand = program .command('upload') - .description('Upload metadata') - .argument('', 'Seed for the metadata account') - .argument( - '', - 'Program associated with the metadata account', - address - ) + .description('Upload metadata.') + .addArgument(seedArgument) + .addArgument(programArgument) .argument( '[file]', 'The path to the file to upload (creates a "direct" data source). See options for other sources such as --text, --url and --account.' @@ -76,7 +74,7 @@ uploadCommand ) .action( async ( - seed: string, + seed: Seed, program: Address, file: string | undefined, _, @@ -134,16 +132,12 @@ uploadCommand // Download metadata command. program .command('download') - .description('Download metadata to file') - .argument('', 'Seed for the metadata account') - .argument( - '', - 'Program associated with the metadata account', - address - ) + .description('Download metadata to file.') + .addArgument(seedArgument) + .addArgument(programArgument) .addOption(outputOption) .addOption(nonCanonicalReadOption) - .action(async (seed: string, program: Address, _, cmd: Command) => { + .action(async (seed: Seed, program: Address, _, cmd: Command) => { const options = cmd.optsWithGlobals() as GlobalOptions & NonCanonicalReadOption & OutputOption; @@ -176,18 +170,14 @@ program program .command('set-authority') .description( - 'Set or update an additional authority on canonical metadata accounts' + 'Set or update an additional authority on canonical metadata accounts.' ) - .argument('', 'Seed for the metadata account') - .argument( - '', - 'Program associated with the metadata account', - address - ) - .argument('', 'The new authority to set', address) + .addArgument(seedArgument) + .addArgument(programArgument) + .argument('', 'The new authority to set', address) // TODO: Make it a mandatory option to be explicit. .action( async ( - seed: string, + seed: Seed, program: Address, newAuthority: string, _, @@ -220,14 +210,12 @@ program program .command('remove-authority') - .argument('', 'Seed for the metadata account') - .argument( - '', - 'Program associated with the metadata account', - address + .description( + 'Remove the additional authority on canonical metadata accounts.' ) - .description('Remove the additional authority on canonical metadata accounts') - .action(async (seed: string, program: Address, _, cmd: Command) => { + .addArgument(seedArgument) + .addArgument(programArgument) + .action(async (seed: Seed, program: Address, _, cmd: Command) => { const options = cmd.optsWithGlobals() as GlobalOptions; const client = await getClient(options); const { metadata, programData } = await getPdaDetails({ @@ -250,21 +238,17 @@ program logSuccess('Additional authority successfully removed'); }); -type SetImmutableOptions = GlobalOptions & NonCanonicalWriteOption; program .command('set-immutable') .description( - 'Make the metadata account immutable, preventing any further updates' - ) - .argument('', 'Seed for the metadata account') - .argument( - '', - 'Program associated with the metadata account', - address + 'Make the metadata account immutable, preventing any further updates.' ) + .addArgument(seedArgument) + .addArgument(programArgument) .addOption(nonCanonicalWriteOption) - .action(async (seed: string, program: Address, _, cmd: Command) => { - const options = cmd.optsWithGlobals() as SetImmutableOptions; + .action(async (seed: Seed, program: Address, _, cmd: Command) => { + const options = cmd.optsWithGlobals() as GlobalOptions & + NonCanonicalWriteOption; const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( client.rpc, @@ -297,25 +281,15 @@ program logSuccess('Metadata account successfully set as immutable'); }); -type CloseOptions = GlobalOptions & { - nonCanonical: boolean; -}; program .command('close') - .description('Close metadata account and recover rent') - .argument('', 'Seed for the metadata account') - .argument( - '', - 'Program associated with the metadata account', - address - ) - .option( - '--non-canonical', - 'When provided, a non-canonical metadata account will be closed using the active keypair as the authority.', - false - ) - .action(async (seed: string, program: Address, _, cmd: Command) => { - const options = cmd.optsWithGlobals() as CloseOptions; + .description('Close metadata account and recover rent.') + .addArgument(seedArgument) + .addArgument(programArgument) + .addOption(nonCanonicalWriteOption) + .action(async (seed: Seed, program: Address, _, cmd: Command) => { + const options = cmd.optsWithGlobals() as GlobalOptions & + NonCanonicalWriteOption; const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( client.rpc, @@ -351,14 +325,14 @@ program program .command('list-buffers') - .description('List all buffer accounts owned by an authority') + .description('List all buffer accounts owned by an authority.') .action(async () => { // TODO }); program .command('list') - .description('List all metadata accounts owned by an authority') + .description('List all metadata accounts owned by an authority.') .action(async () => { // TODO }); From f0735e1217be8d2224ba8b8113db130551f94aa2 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 14 Apr 2025 19:04:07 +0100 Subject: [PATCH 14/60] wip --- clients/js/src/cli-arguments.ts | 5 +++++ clients/js/src/cli.ts | 7 ++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/clients/js/src/cli-arguments.ts b/clients/js/src/cli-arguments.ts index cd812eb..bb2ef17 100644 --- a/clients/js/src/cli-arguments.ts +++ b/clients/js/src/cli-arguments.ts @@ -17,3 +17,8 @@ export const programArgument = new Argument( logErrorAndExit(`Invalid program address: "${value}"`); } }); + +export const fileArgument = new Argument( + '[file]', + 'Filepath of the data to upload (creates a "direct" data source). See options for other sources such as --text, --url and --account.' +); diff --git a/clients/js/src/cli.ts b/clients/js/src/cli.ts index d89d9d0..026c0b4 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli.ts @@ -43,7 +43,7 @@ import { sequentialInstructionPlan } from './instructionPlans'; import { getPdaDetails } from './internals'; import { uploadMetadata } from './uploadMetadata'; import { getProgramAuthority } from './utils'; -import { programArgument, seedArgument } from './cli-arguments'; +import { fileArgument, programArgument, seedArgument } from './cli-arguments'; // Define the CLI program. const program = new Command(); @@ -60,10 +60,7 @@ const uploadCommand = program .description('Upload metadata.') .addArgument(seedArgument) .addArgument(programArgument) - .argument( - '[file]', - 'The path to the file to upload (creates a "direct" data source). See options for other sources such as --text, --url and --account.' - ) + .addArgument(fileArgument) .addOption(nonCanonicalWriteOption); setWriteOptions(uploadCommand); uploadCommand From f3775e281ff1ff7f5803e93c9ed3b50770ad0eb1 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 10:09:46 +0100 Subject: [PATCH 15/60] wip --- .../{cli-arguments.ts => cli/arguments.ts} | 2 +- clients/js/src/cli/index.ts | 1 + clients/js/src/{cli-logs.ts => cli/logs.ts} | 0 .../js/src/{cli-options.ts => cli/options.ts} | 4 ++-- clients/js/src/{cli.ts => cli/program.ts} | 20 +++++++++---------- clients/js/src/{cli-utils.ts => cli/utils.ts} | 10 +++++----- clients/js/tsup.config.ts | 2 +- 7 files changed, 20 insertions(+), 19 deletions(-) rename clients/js/src/{cli-arguments.ts => cli/arguments.ts} (93%) create mode 100644 clients/js/src/cli/index.ts rename clients/js/src/{cli-logs.ts => cli/logs.ts} (100%) rename clients/js/src/{cli-options.ts => cli/options.ts} (98%) rename clients/js/src/{cli.ts => cli/program.ts} (95%) rename clients/js/src/{cli-utils.ts => cli/utils.ts} (97%) diff --git a/clients/js/src/cli-arguments.ts b/clients/js/src/cli/arguments.ts similarity index 93% rename from clients/js/src/cli-arguments.ts rename to clients/js/src/cli/arguments.ts index bb2ef17..02e0fd2 100644 --- a/clients/js/src/cli-arguments.ts +++ b/clients/js/src/cli/arguments.ts @@ -1,6 +1,6 @@ import { Argument } from 'commander'; import { address, Address } from '@solana/kit'; -import { logErrorAndExit } from './cli-logs'; +import { logErrorAndExit } from './logs'; export const seedArgument = new Argument( '', diff --git a/clients/js/src/cli/index.ts b/clients/js/src/cli/index.ts new file mode 100644 index 0000000..2851943 --- /dev/null +++ b/clients/js/src/cli/index.ts @@ -0,0 +1 @@ +export * from './program'; diff --git a/clients/js/src/cli-logs.ts b/clients/js/src/cli/logs.ts similarity index 100% rename from clients/js/src/cli-logs.ts rename to clients/js/src/cli/logs.ts diff --git a/clients/js/src/cli-options.ts b/clients/js/src/cli/options.ts similarity index 98% rename from clients/js/src/cli-options.ts rename to clients/js/src/cli/options.ts index ff56a15..b0f4656 100644 --- a/clients/js/src/cli-options.ts +++ b/clients/js/src/cli/options.ts @@ -1,7 +1,7 @@ import { address, type Address, type MicroLamports } from '@solana/kit'; import { Command, Option } from 'commander'; -import { logErrorAndExit } from './cli-logs'; -import { Compression, Encoding, Format } from './generated'; +import { logErrorAndExit } from './logs'; +import { Compression, Encoding, Format } from '../generated'; export type GlobalOptions = KeypairOption & PayerOption & diff --git a/clients/js/src/cli.ts b/clients/js/src/cli/program.ts similarity index 95% rename from clients/js/src/cli.ts rename to clients/js/src/cli/program.ts index 026c0b4..b6bc841 100644 --- a/clients/js/src/cli.ts +++ b/clients/js/src/cli/program.ts @@ -11,7 +11,7 @@ import { } from '@solana/kit'; import chalk from 'chalk'; import { Command } from 'commander'; -import { logErrorAndExit, logSuccess } from './cli-logs'; +import { logErrorAndExit, logSuccess } from './logs'; import { GlobalOptions, NonCanonicalReadOption, @@ -23,7 +23,7 @@ import { setGlobalOptions, setWriteOptions, WriteOptions, -} from './cli-options'; +} from './options'; import { getClient, getFormatFromFile, @@ -31,19 +31,19 @@ import { getPackedData, getReadonlyClient, writeFile, -} from './cli-utils'; -import { downloadMetadata } from './downloadMetadata'; +} from './utils'; +import { downloadMetadata } from '../downloadMetadata'; import { getCloseInstruction, getSetAuthorityInstruction, getSetImmutableInstruction, Seed, -} from './generated'; -import { sequentialInstructionPlan } from './instructionPlans'; -import { getPdaDetails } from './internals'; -import { uploadMetadata } from './uploadMetadata'; -import { getProgramAuthority } from './utils'; -import { fileArgument, programArgument, seedArgument } from './cli-arguments'; +} from '../generated'; +import { sequentialInstructionPlan } from '../instructionPlans'; +import { getPdaDetails } from '../internals'; +import { uploadMetadata } from '../uploadMetadata'; +import { getProgramAuthority } from '../utils'; +import { fileArgument, programArgument, seedArgument } from './arguments'; // Define the CLI program. const program = new Command(); diff --git a/clients/js/src/cli-utils.ts b/clients/js/src/cli/utils.ts similarity index 97% rename from clients/js/src/cli-utils.ts rename to clients/js/src/cli/utils.ts index a8666a2..4d5f1e1 100644 --- a/clients/js/src/cli-utils.ts +++ b/clients/js/src/cli/utils.ts @@ -16,8 +16,8 @@ import { SolanaRpcSubscriptionsApi, } from '@solana/kit'; import { parse as parseYaml } from 'yaml'; -import { WriteOptions } from './cli-options'; -import { Format } from './generated'; +import { WriteOptions } from './options'; +import { Format } from '../generated'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, @@ -25,14 +25,14 @@ import { TransactionPlanExecutor, TransactionPlanner, TransactionPlanResult, -} from './instructionPlans'; +} from '../instructionPlans'; import { packDirectData, PackedData, packExternalData, packUrlData, -} from './packData'; -import { logErrorAndExit, logWarning } from './cli-logs'; +} from '../packData'; +import { logErrorAndExit, logWarning } from './logs'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; diff --git a/clients/js/tsup.config.ts b/clients/js/tsup.config.ts index 96695b2..8bcc7a0 100644 --- a/clients/js/tsup.config.ts +++ b/clients/js/tsup.config.ts @@ -16,7 +16,7 @@ export default defineConfig(() => [ { ...SHARED_OPTIONS, format: 'esm' }, // CLI. - { ...SHARED_OPTIONS, format: 'cjs', entry: ['./src/cli.ts'] }, + { ...SHARED_OPTIONS, format: 'cjs', entry: { cli: './src/cli/index.ts' } }, // Tests. { From 20f40cb52e855bf0a044b6926cf601493f85f92c Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 10:23:30 +0100 Subject: [PATCH 16/60] wip --- clients/js/src/cli/program.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/clients/js/src/cli/program.ts b/clients/js/src/cli/program.ts index b6bc841..128e354 100644 --- a/clients/js/src/cli/program.ts +++ b/clients/js/src/cli/program.ts @@ -54,16 +54,16 @@ program .configureHelp({ showGlobalOptions: true }); setGlobalOptions(program); -// Upload metadata command. -const uploadCommand = program - .command('upload') - .description('Upload metadata.') +// Write metadata command. +const writeCommand = program + .command('write') + .description('Create or update a metadata account for a given program.') .addArgument(seedArgument) .addArgument(programArgument) .addArgument(fileArgument) .addOption(nonCanonicalWriteOption); -setWriteOptions(uploadCommand); -uploadCommand +setWriteOptions(writeCommand); +writeCommand .option( '--buffer-only', 'Only create the buffer and export the transaction that sets the buffer.', @@ -90,7 +90,7 @@ uploadCommand client.authority.address !== programAuthority ) { logErrorAndExit( - 'You must be the program authority to upload a canonical metadata account. Use `--non-canonical` option to upload as a third party.' + 'You must be the program authority to write to a canonical metadata account. Use `--non-canonical` option to write as a third party.' ); } await uploadMetadata({ From fba998e21a2c359ea2c6a80345fb4bef4857f652 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 10:24:45 +0100 Subject: [PATCH 17/60] wip --- clients/js/src/cli/program.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/js/src/cli/program.ts b/clients/js/src/cli/program.ts index 128e354..d35b77c 100644 --- a/clients/js/src/cli/program.ts +++ b/clients/js/src/cli/program.ts @@ -126,10 +126,10 @@ writeCommand } ); -// Download metadata command. +// Fetch metadata command. program - .command('download') - .description('Download metadata to file.') + .command('fetch') + .description('Fetch the content of a metadata account for a given program.') .addArgument(seedArgument) .addArgument(programArgument) .addOption(outputOption) From 2aa1fc323a1199af792c05251115b0e7c84bf03b Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 10:28:38 +0100 Subject: [PATCH 18/60] wip --- clients/js/src/cli/program.ts | 8 ++++---- .../{downloadMetadata.ts => fetchMetadataContent.ts} | 4 ++-- clients/js/src/index.ts | 4 ++-- .../js/src/{uploadMetadata.ts => writeMetadata.ts} | 2 +- ...Metadata.test.ts => fetchMetadataContent.test.ts} | 12 ++++++------ ...{uploadMetadata.test.ts => writeMetadata.test.ts} | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) rename clients/js/src/{downloadMetadata.ts => fetchMetadataContent.ts} (92%) rename clients/js/src/{uploadMetadata.ts => writeMetadata.ts} (98%) rename clients/js/test/{downloadMetadata.test.ts => fetchMetadataContent.test.ts} (89%) rename clients/js/test/{uploadMetadata.test.ts => writeMetadata.test.ts} (97%) diff --git a/clients/js/src/cli/program.ts b/clients/js/src/cli/program.ts index d35b77c..018f0cc 100644 --- a/clients/js/src/cli/program.ts +++ b/clients/js/src/cli/program.ts @@ -32,7 +32,7 @@ import { getReadonlyClient, writeFile, } from './utils'; -import { downloadMetadata } from '../downloadMetadata'; +import { fetchMetadataContent } from '../fetchMetadataContent'; import { getCloseInstruction, getSetAuthorityInstruction, @@ -41,7 +41,7 @@ import { } from '../generated'; import { sequentialInstructionPlan } from '../instructionPlans'; import { getPdaDetails } from '../internals'; -import { uploadMetadata } from '../uploadMetadata'; +import { writeMetadata } from '../writeMetadata'; import { getProgramAuthority } from '../utils'; import { fileArgument, programArgument, seedArgument } from './arguments'; @@ -93,7 +93,7 @@ writeCommand 'You must be the program authority to write to a canonical metadata account. Use `--non-canonical` option to write as a third party.' ); } - await uploadMetadata({ + await writeMetadata({ ...client, ...getPackedData(file, options), payer: client.payer, @@ -146,7 +146,7 @@ program ? options.nonCanonical : undefined; try { - const content = await downloadMetadata( + const content = await fetchMetadataContent( client.rpc, program, seed, diff --git a/clients/js/src/downloadMetadata.ts b/clients/js/src/fetchMetadataContent.ts similarity index 92% rename from clients/js/src/downloadMetadata.ts rename to clients/js/src/fetchMetadataContent.ts index 671c42a..fbc7e21 100644 --- a/clients/js/src/downloadMetadata.ts +++ b/clients/js/src/fetchMetadataContent.ts @@ -4,7 +4,7 @@ import { parse as parseYaml } from 'yaml'; import { fetchMetadataFromSeeds, Format, SeedArgs } from './generated'; import { unpackAndFetchData } from './packData'; -export async function downloadMetadata( +export async function fetchMetadataContent( rpc: Rpc, program: Address, seed: SeedArgs, @@ -18,7 +18,7 @@ export async function downloadMetadata( return await unpackAndFetchData({ rpc, ...account.data }); } -export async function downloadAndParseMetadata( +export async function fetchAndParseMetadataContent( rpc: Rpc, program: Address, seed: SeedArgs, diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index 5ce6031..ea72bca 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -2,8 +2,8 @@ export * from './generated'; export * from './instructionPlans'; export * from './createMetadata'; -export * from './downloadMetadata'; +export * from './fetchMetadataContent'; export * from './packData'; export * from './updateMetadata'; -export * from './uploadMetadata'; export * from './utils'; +export * from './writeMetadata'; diff --git a/clients/js/src/uploadMetadata.ts b/clients/js/src/writeMetadata.ts similarity index 98% rename from clients/js/src/uploadMetadata.ts rename to clients/js/src/writeMetadata.ts index ff965a8..f08b01c 100644 --- a/clients/js/src/uploadMetadata.ts +++ b/clients/js/src/writeMetadata.ts @@ -21,7 +21,7 @@ import { } from './updateMetadata'; import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; -export async function uploadMetadata( +export async function writeMetadata( input: MetadataInput & { rpc: Rpc & Parameters[0]['rpc']; diff --git a/clients/js/test/downloadMetadata.test.ts b/clients/js/test/fetchMetadataContent.test.ts similarity index 89% rename from clients/js/test/downloadMetadata.test.ts rename to clients/js/test/fetchMetadataContent.test.ts index c8b96da..3f36480 100644 --- a/clients/js/test/downloadMetadata.test.ts +++ b/clients/js/test/fetchMetadataContent.test.ts @@ -1,10 +1,10 @@ import { address } from '@solana/kit'; import test from 'ava'; import { - downloadAndParseMetadata, + fetchAndParseMetadataContent, Format, packDirectData, - uploadMetadata, + writeMetadata, } from '../src'; import { createDefaultSolanaClient, @@ -20,7 +20,7 @@ test('it fetches and parses direct IDLs from canonical metadata accounts', async // And given the following IDL exists for the program. const idl = '{"kind":"rootNode","standard":"codama","version":"1.0.0"}'; - await uploadMetadata({ + await writeMetadata({ ...client, ...packDirectData({ content: idl }), payer: authority, @@ -31,7 +31,7 @@ test('it fetches and parses direct IDLs from canonical metadata accounts', async }); // When we fetch the canonical IDL for the program. - const result = await downloadAndParseMetadata(client.rpc, program, 'idl'); + const result = await fetchAndParseMetadataContent(client.rpc, program, 'idl'); // Then we expect the following IDL to be fetched and parsed. t.deepEqual(result, { @@ -49,7 +49,7 @@ test('it fetches and parses direct IDLs from non-canonical metadata accounts', a // And given the following IDL exists for the program. const idl = '{"kind":"rootNode","standard":"codama","version":"1.0.0"}'; - await uploadMetadata({ + await writeMetadata({ ...client, ...packDirectData({ content: idl }), payer: authority, @@ -60,7 +60,7 @@ test('it fetches and parses direct IDLs from non-canonical metadata accounts', a }); // When we fetch the non-canonical IDL for the program. - const result = await downloadAndParseMetadata( + const result = await fetchAndParseMetadataContent( client.rpc, program, 'idl', diff --git a/clients/js/test/uploadMetadata.test.ts b/clients/js/test/writeMetadata.test.ts similarity index 97% rename from clients/js/test/uploadMetadata.test.ts rename to clients/js/test/writeMetadata.test.ts index 92e6251..71962be 100644 --- a/clients/js/test/uploadMetadata.test.ts +++ b/clients/js/test/writeMetadata.test.ts @@ -10,7 +10,7 @@ import { findCanonicalPda, Format, Metadata, - uploadMetadata, + writeMetadata, } from '../src'; import { createDefaultSolanaClient, @@ -30,7 +30,7 @@ test('it creates a new metadata account if it does not exist', async (t) => { // When we upload this canonical metadata account. const data = getUtf8Encoder().encode('Some data'); - await uploadMetadata({ + await writeMetadata({ ...client, payer: authority, authority, @@ -83,7 +83,7 @@ test('it updates a metadata account if it exists', async (t) => { // When we upload this canonical metadata account with different data. const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); - const { metadata } = await uploadMetadata({ + const { metadata } = await writeMetadata({ ...client, payer: authority, authority, From fc35ed881e9851dda0b67015813fc5d601d35077 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 10:41:44 +0100 Subject: [PATCH 19/60] wip --- clients/js/src/cli/commands/fetch.ts | 55 ++++++++++++++++++++ clients/js/src/cli/commands/index.ts | 7 +++ clients/js/src/cli/program.ts | 75 +++++----------------------- 3 files changed, 75 insertions(+), 62 deletions(-) create mode 100644 clients/js/src/cli/commands/fetch.ts create mode 100644 clients/js/src/cli/commands/index.ts diff --git a/clients/js/src/cli/commands/fetch.ts b/clients/js/src/cli/commands/fetch.ts new file mode 100644 index 0000000..2ca2b74 --- /dev/null +++ b/clients/js/src/cli/commands/fetch.ts @@ -0,0 +1,55 @@ +import { Address, isSolanaError } from '@solana/kit'; +import chalk from 'chalk'; +import { Command } from 'commander'; +import { fetchMetadataContent } from '../../fetchMetadataContent'; +import { logErrorAndExit, logSuccess } from '../logs'; +import { + GlobalOptions, + nonCanonicalReadOption, + NonCanonicalReadOption, + outputOption, + OutputOption, +} from '../options'; +import { getKeyPairSigners, getReadonlyClient, writeFile } from '../utils'; +import { Seed } from '../../generated'; +import { programArgument, seedArgument } from '../arguments'; + +export function setFetchCommand(program: Command): void { + program + .command('fetch') + .description('Fetch the content of a metadata account for a given program.') + .addArgument(seedArgument) + .addArgument(programArgument) + .addOption(nonCanonicalReadOption) + .addOption(outputOption) + .action(doFetch); +} + +type Options = NonCanonicalReadOption & OutputOption; +async function doFetch(seed: Seed, program: Address, _: Options, cmd: Command) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = getReadonlyClient(options); + const authority = + options.nonCanonical === true + ? (await getKeyPairSigners(options, client.configs))[0].address + : options.nonCanonical + ? options.nonCanonical + : undefined; + try { + const content = await fetchMetadataContent( + client.rpc, + program, + seed, + authority + ); + if (options.output) { + writeFile(options.output, content); + logSuccess(`Metadata content saved to ${chalk.bold(options.output)}`); + } else { + console.log(content); + } + } catch (error) { + if (isSolanaError(error)) logErrorAndExit(error.message); + throw error; + } +} diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts new file mode 100644 index 0000000..7750305 --- /dev/null +++ b/clients/js/src/cli/commands/index.ts @@ -0,0 +1,7 @@ +import { Command } from 'commander'; +import { setFetchCommand } from './fetch'; + +export function setCommands(program: Command): void { + // setWriteCommand(program); + setFetchCommand(program); +} diff --git a/clients/js/src/cli/program.ts b/clients/js/src/cli/program.ts index 018f0cc..d8fda52 100644 --- a/clients/js/src/cli/program.ts +++ b/clients/js/src/cli/program.ts @@ -6,33 +6,10 @@ import { getBase58Decoder, getBase64Decoder, getTransactionEncoder, - isSolanaError, Transaction, } from '@solana/kit'; import chalk from 'chalk'; import { Command } from 'commander'; -import { logErrorAndExit, logSuccess } from './logs'; -import { - GlobalOptions, - NonCanonicalReadOption, - nonCanonicalReadOption, - NonCanonicalWriteOption, - nonCanonicalWriteOption, - OutputOption, - outputOption, - setGlobalOptions, - setWriteOptions, - WriteOptions, -} from './options'; -import { - getClient, - getFormatFromFile, - getKeyPairSigners, - getPackedData, - getReadonlyClient, - writeFile, -} from './utils'; -import { fetchMetadataContent } from '../fetchMetadataContent'; import { getCloseInstruction, getSetAuthorityInstruction, @@ -41,9 +18,20 @@ import { } from '../generated'; import { sequentialInstructionPlan } from '../instructionPlans'; import { getPdaDetails } from '../internals'; -import { writeMetadata } from '../writeMetadata'; import { getProgramAuthority } from '../utils'; +import { writeMetadata } from '../writeMetadata'; import { fileArgument, programArgument, seedArgument } from './arguments'; +import { setCommands } from './commands'; +import { logErrorAndExit, logSuccess } from './logs'; +import { + GlobalOptions, + NonCanonicalWriteOption, + nonCanonicalWriteOption, + setGlobalOptions, + setWriteOptions, + WriteOptions, +} from './options'; +import { getClient, getFormatFromFile, getPackedData } from './utils'; // Define the CLI program. const program = new Command(); @@ -53,6 +41,7 @@ program .version(__VERSION__) .configureHelp({ showGlobalOptions: true }); setGlobalOptions(program); +setCommands(program); // Write metadata command. const writeCommand = program @@ -126,44 +115,6 @@ writeCommand } ); -// Fetch metadata command. -program - .command('fetch') - .description('Fetch the content of a metadata account for a given program.') - .addArgument(seedArgument) - .addArgument(programArgument) - .addOption(outputOption) - .addOption(nonCanonicalReadOption) - .action(async (seed: Seed, program: Address, _, cmd: Command) => { - const options = cmd.optsWithGlobals() as GlobalOptions & - NonCanonicalReadOption & - OutputOption; - const client = getReadonlyClient(options); - const authority = - options.nonCanonical === true - ? (await getKeyPairSigners(options, client.configs))[0].address - : options.nonCanonical - ? options.nonCanonical - : undefined; - try { - const content = await fetchMetadataContent( - client.rpc, - program, - seed, - authority - ); - if (options.output) { - writeFile(options.output, content); - logSuccess(`Metadata content saved to ${chalk.bold(options.output)}`); - } else { - console.log(content); - } - } catch (error) { - if (isSolanaError(error)) logErrorAndExit(error.message); - throw error; - } - }); - program .command('set-authority') .description( From acb38b95cb6bbe72427e2cc066f740ce9dc56e70 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 10:47:59 +0100 Subject: [PATCH 20/60] wip --- clients/js/src/cli/commands/index.ts | 3 +- clients/js/src/cli/commands/write.ts | 82 ++++++++++++++++++++++++++ clients/js/src/cli/program.ts | 88 +--------------------------- 3 files changed, 87 insertions(+), 86 deletions(-) create mode 100644 clients/js/src/cli/commands/write.ts diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index 7750305..bb8ab39 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -1,7 +1,8 @@ import { Command } from 'commander'; import { setFetchCommand } from './fetch'; +import { setWriteCommand } from './write'; export function setCommands(program: Command): void { - // setWriteCommand(program); + setWriteCommand(program); setFetchCommand(program); } diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts new file mode 100644 index 0000000..b12dd29 --- /dev/null +++ b/clients/js/src/cli/commands/write.ts @@ -0,0 +1,82 @@ +import chalk from 'chalk'; +import { Command } from 'commander'; +import { getClient, getFormatFromFile, getPackedData } from '../utils'; +import { writeMetadata } from '../../writeMetadata'; +import { Seed } from '../../generated'; +import { + Address, + getBase58Decoder, + getBase64Decoder, + getTransactionEncoder, + Transaction, +} from '@solana/kit'; +import { + GlobalOptions, + nonCanonicalWriteOption, + NonCanonicalWriteOption, + setWriteOptions, + WriteOptions, +} from '../options'; +import { getProgramAuthority } from '../../utils'; +import { logErrorAndExit, logSuccess } from '../logs'; +import { fileArgument, programArgument, seedArgument } from '../arguments'; + +export function setWriteCommand(program: Command): void { + const writeCommand = program + .command('write') + .description('Create or update a metadata account for a given program.') + .addArgument(seedArgument) + .addArgument(programArgument) + .addArgument(fileArgument); + setWriteOptions(writeCommand); + writeCommand.addOption(nonCanonicalWriteOption); +} + +type Options = WriteOptions & NonCanonicalWriteOption; +export async function doWrite( + seed: Seed, + program: Address, + file: string | undefined, + _: Options, + cmd: Command +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const { authority: programAuthority } = await getProgramAuthority( + client.rpc, + program + ); + if (!options.nonCanonical && client.authority.address !== programAuthority) { + logErrorAndExit( + 'You must be the program authority to write to a canonical metadata account. Use `--non-canonical` option to write as a third party.' + ); + } + await writeMetadata({ + ...client, + ...getPackedData(file, options), + payer: client.payer, + authority: client.authority, + program, + seed, + format: options.format ?? getFormatFromFile(file), + closeBuffer: true, + priorityFees: options.priorityFees, + }); + const exportTransaction = false; // TODO: Option + if (exportTransaction) { + const transactionBytes = getTransactionEncoder().encode({} as Transaction); + const base64EncodedTransaction = + getBase64Decoder().decode(transactionBytes); + const base58EncodedTransaction = + getBase58Decoder().decode(transactionBytes); + logSuccess( + `Buffer successfully created for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"!\n` + + `Use the following transaction data to apply the buffer:\n\n` + + `[base64]\n${base64EncodedTransaction}\n\n[base58]\n${base58EncodedTransaction}` + ); + } else { + logSuccess( + `Metadata uploaded successfully for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"!` + ); + } +} diff --git a/clients/js/src/cli/program.ts b/clients/js/src/cli/program.ts index d8fda52..8fa7a6c 100644 --- a/clients/js/src/cli/program.ts +++ b/clients/js/src/cli/program.ts @@ -1,13 +1,6 @@ #!/usr/bin/env node -import { - Address, - address, - getBase58Decoder, - getBase64Decoder, - getTransactionEncoder, - Transaction, -} from '@solana/kit'; +import { Address, address } from '@solana/kit'; import chalk from 'chalk'; import { Command } from 'commander'; import { @@ -19,8 +12,7 @@ import { import { sequentialInstructionPlan } from '../instructionPlans'; import { getPdaDetails } from '../internals'; import { getProgramAuthority } from '../utils'; -import { writeMetadata } from '../writeMetadata'; -import { fileArgument, programArgument, seedArgument } from './arguments'; +import { programArgument, seedArgument } from './arguments'; import { setCommands } from './commands'; import { logErrorAndExit, logSuccess } from './logs'; import { @@ -28,10 +20,8 @@ import { NonCanonicalWriteOption, nonCanonicalWriteOption, setGlobalOptions, - setWriteOptions, - WriteOptions, } from './options'; -import { getClient, getFormatFromFile, getPackedData } from './utils'; +import { getClient } from './utils'; // Define the CLI program. const program = new Command(); @@ -43,78 +33,6 @@ program setGlobalOptions(program); setCommands(program); -// Write metadata command. -const writeCommand = program - .command('write') - .description('Create or update a metadata account for a given program.') - .addArgument(seedArgument) - .addArgument(programArgument) - .addArgument(fileArgument) - .addOption(nonCanonicalWriteOption); -setWriteOptions(writeCommand); -writeCommand - .option( - '--buffer-only', - 'Only create the buffer and export the transaction that sets the buffer.', - false - ) - .action( - async ( - seed: Seed, - program: Address, - file: string | undefined, - _, - cmd: Command - ) => { - const options = cmd.optsWithGlobals() as WriteOptions & - GlobalOptions & - NonCanonicalWriteOption & { bufferOnly: boolean }; - const client = await getClient(options); - const { authority: programAuthority } = await getProgramAuthority( - client.rpc, - program - ); - if ( - !options.nonCanonical && - client.authority.address !== programAuthority - ) { - logErrorAndExit( - 'You must be the program authority to write to a canonical metadata account. Use `--non-canonical` option to write as a third party.' - ); - } - await writeMetadata({ - ...client, - ...getPackedData(file, options), - payer: client.payer, - authority: client.authority, - program, - seed, - format: options.format ?? getFormatFromFile(file), - closeBuffer: true, - priorityFees: options.priorityFees, - }); - const exportTransaction = false; // TODO: Option - if (exportTransaction) { - const transactionBytes = getTransactionEncoder().encode( - {} as Transaction - ); - const base64EncodedTransaction = - getBase64Decoder().decode(transactionBytes); - const base58EncodedTransaction = - getBase58Decoder().decode(transactionBytes); - logSuccess( - `Buffer successfully created for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"!\n` + - `Use the following transaction data to apply the buffer:\n\n` + - `[base64]\n${base64EncodedTransaction}\n\n[base58]\n${base58EncodedTransaction}` - ); - } else { - logSuccess( - `Metadata uploaded successfully for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"!` - ); - } - } - ); - program .command('set-authority') .description( From 088a9c1c75adaed8726fbb2ccd9e0abf9db3203d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 11:03:58 +0100 Subject: [PATCH 21/60] wip --- clients/js/src/cli/commands/close.ts | 57 ++++++ clients/js/src/cli/commands/index.ts | 13 ++ .../js/src/cli/commands/remove-authority.ts | 49 +++++ clients/js/src/cli/commands/set-authority.ts | 54 +++++ clients/js/src/cli/commands/set-immutable.ts | 63 ++++++ clients/js/src/cli/program.ts | 192 +----------------- 6 files changed, 238 insertions(+), 190 deletions(-) create mode 100644 clients/js/src/cli/commands/close.ts create mode 100644 clients/js/src/cli/commands/remove-authority.ts create mode 100644 clients/js/src/cli/commands/set-authority.ts create mode 100644 clients/js/src/cli/commands/set-immutable.ts diff --git a/clients/js/src/cli/commands/close.ts b/clients/js/src/cli/commands/close.ts new file mode 100644 index 0000000..541bf67 --- /dev/null +++ b/clients/js/src/cli/commands/close.ts @@ -0,0 +1,57 @@ +import { Address } from '@solana/kit'; +import { Command } from 'commander'; +import { getCloseInstruction, Seed } from '../../generated'; +import { sequentialInstructionPlan } from '../../instructionPlans'; +import { getPdaDetails } from '../../internals'; +import { getProgramAuthority } from '../../utils'; +import { programArgument, seedArgument } from '../arguments'; +import { logErrorAndExit, logSuccess } from '../logs'; +import { + GlobalOptions, + NonCanonicalWriteOption, + nonCanonicalWriteOption, +} from '../options'; +import { getClient } from '../utils'; + +export function setCloseCommand(program: Command): void { + program + .command('close') + .description('Close metadata account and recover rent.') + .addArgument(seedArgument) + .addArgument(programArgument) + .addOption(nonCanonicalWriteOption) + .action(doClose); +} + +type Options = NonCanonicalWriteOption; +async function doClose(seed: Seed, program: Address, _: Options, cmd: Command) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const { authority: programAuthority } = await getProgramAuthority( + client.rpc, + program + ); + if (!options.nonCanonical && client.authority.address !== programAuthority) { + logErrorAndExit( + 'You must be the program authority to close a canonical metadata account. Use `--non-canonical` option to close as a third party.' + ); + } + const { metadata, programData } = await getPdaDetails({ + rpc: client.rpc, + program, + authority: client.authority, + seed, + }); + await client.planAndExecute( + sequentialInstructionPlan([ + getCloseInstruction({ + account: metadata, + authority: client.authority, + program, + programData, + destination: client.payer.address, + }), + ]) + ); + logSuccess('Account successfully closed and rent recovered'); +} diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index bb8ab39..b5e2496 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -1,8 +1,21 @@ import { Command } from 'commander'; import { setFetchCommand } from './fetch'; import { setWriteCommand } from './write'; +import { setCloseCommand } from './close'; +import { setSetAuthorityCommand } from './set-authority'; +import { setRemoveAuthorityCommand } from './remove-authority'; +import { setSetImmutableCommand } from './set-immutable'; export function setCommands(program: Command): void { + // Metadata commands. setWriteCommand(program); setFetchCommand(program); + setSetAuthorityCommand(program); + setRemoveAuthorityCommand(program); + setSetImmutableCommand(program); + setCloseCommand(program); + // TODO: list: List all metadata accounts owned by an authority. + + // Buffer commands. + // TODO: list-buffers: List all buffer accounts owned by an authority. } diff --git a/clients/js/src/cli/commands/remove-authority.ts b/clients/js/src/cli/commands/remove-authority.ts new file mode 100644 index 0000000..d16e7d1 --- /dev/null +++ b/clients/js/src/cli/commands/remove-authority.ts @@ -0,0 +1,49 @@ +import { Address } from '@solana/kit'; +import { Command } from 'commander'; +import { getSetAuthorityInstruction, Seed } from '../../generated'; +import { sequentialInstructionPlan } from '../../instructionPlans'; +import { getPdaDetails } from '../../internals'; +import { programArgument, seedArgument } from '../arguments'; +import { logSuccess } from '../logs'; +import { GlobalOptions } from '../options'; +import { getClient } from '../utils'; + +export function setRemoveAuthorityCommand(program: Command): void { + program + .command('remove-authority') + .description( + 'Remove the additional authority on canonical metadata accounts.' + ) + .addArgument(seedArgument) + .addArgument(programArgument) + .action(doRemoveAuthority); +} + +type Options = {}; +async function doRemoveAuthority( + seed: Seed, + program: Address, + _: Options, + cmd: Command +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const { metadata, programData } = await getPdaDetails({ + rpc: client.rpc, + program, + authority: client.authority, + seed, + }); + await client.planAndExecute( + sequentialInstructionPlan([ + getSetAuthorityInstruction({ + account: metadata, + authority: client.authority, + newAuthority: null, + program, + programData, + }), + ]) + ); + logSuccess('Additional authority successfully removed'); +} diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts new file mode 100644 index 0000000..23e8c56 --- /dev/null +++ b/clients/js/src/cli/commands/set-authority.ts @@ -0,0 +1,54 @@ +import { address, Address } from '@solana/kit'; +import chalk from 'chalk'; +import { Command } from 'commander'; +import { getSetAuthorityInstruction, Seed } from '../../generated'; +import { sequentialInstructionPlan } from '../../instructionPlans'; +import { getPdaDetails } from '../../internals'; +import { programArgument, seedArgument } from '../arguments'; +import { logSuccess } from '../logs'; +import { GlobalOptions } from '../options'; +import { getClient } from '../utils'; + +export function setSetAuthorityCommand(program: Command): void { + program + .command('set-authority') + .description( + 'Set or update an additional authority on canonical metadata accounts.' + ) + .addArgument(seedArgument) + .addArgument(programArgument) + .argument('', 'The new authority to set', address) // TODO: Make it a mandatory option to be explicit. + .action(doSetAuthority); +} + +type Options = {}; +async function doSetAuthority( + seed: Seed, + program: Address, + newAuthority: Address, + _: Options, + cmd: Command +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const { metadata, programData } = await getPdaDetails({ + rpc: client.rpc, + program, + authority: client.authority, + seed, + }); + await client.planAndExecute( + sequentialInstructionPlan([ + getSetAuthorityInstruction({ + account: metadata, + authority: client.authority, + newAuthority: address(newAuthority), + program, + programData, + }), + ]) + ); + logSuccess( + `Additional authority successfully set to ${chalk.bold(newAuthority)}` + ); +} diff --git a/clients/js/src/cli/commands/set-immutable.ts b/clients/js/src/cli/commands/set-immutable.ts new file mode 100644 index 0000000..9bc7574 --- /dev/null +++ b/clients/js/src/cli/commands/set-immutable.ts @@ -0,0 +1,63 @@ +import { Address } from '@solana/kit'; +import { Command } from 'commander'; +import { getSetImmutableInstruction, Seed } from '../../generated'; +import { sequentialInstructionPlan } from '../../instructionPlans'; +import { getPdaDetails } from '../../internals'; +import { getProgramAuthority } from '../../utils'; +import { programArgument, seedArgument } from '../arguments'; +import { logErrorAndExit, logSuccess } from '../logs'; +import { + GlobalOptions, + NonCanonicalWriteOption, + nonCanonicalWriteOption, +} from '../options'; +import { getClient } from '../utils'; + +export function setSetImmutableCommand(program: Command): void { + program + .command('set-immutable') + .description( + 'Make the metadata account immutable, preventing any further updates.' + ) + .addArgument(seedArgument) + .addArgument(programArgument) + .addOption(nonCanonicalWriteOption) + .action(doSetImmutable); +} + +type Options = NonCanonicalWriteOption; +async function doSetImmutable( + seed: Seed, + program: Address, + _: Options, + cmd: Command +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const { authority: programAuthority } = await getProgramAuthority( + client.rpc, + program + ); + if (!options.nonCanonical && client.authority.address !== programAuthority) { + logErrorAndExit( + 'You must be the program authority to update a canonical metadata account. Use the `--non-canonical` option to update as a third party.' + ); + } + const { metadata, programData } = await getPdaDetails({ + rpc: client.rpc, + program, + authority: client.authority, + seed, + }); + await client.planAndExecute( + sequentialInstructionPlan([ + getSetImmutableInstruction({ + metadata, + authority: client.authority, + program, + programData, + }), + ]) + ); + logSuccess('Metadata account successfully set as immutable'); +} diff --git a/clients/js/src/cli/program.ts b/clients/js/src/cli/program.ts index 8fa7a6c..07dd85b 100644 --- a/clients/js/src/cli/program.ts +++ b/clients/js/src/cli/program.ts @@ -1,27 +1,8 @@ #!/usr/bin/env node -import { Address, address } from '@solana/kit'; -import chalk from 'chalk'; import { Command } from 'commander'; -import { - getCloseInstruction, - getSetAuthorityInstruction, - getSetImmutableInstruction, - Seed, -} from '../generated'; -import { sequentialInstructionPlan } from '../instructionPlans'; -import { getPdaDetails } from '../internals'; -import { getProgramAuthority } from '../utils'; -import { programArgument, seedArgument } from './arguments'; import { setCommands } from './commands'; -import { logErrorAndExit, logSuccess } from './logs'; -import { - GlobalOptions, - NonCanonicalWriteOption, - nonCanonicalWriteOption, - setGlobalOptions, -} from './options'; -import { getClient } from './utils'; +import { setGlobalOptions } from './options'; // Define the CLI program. const program = new Command(); @@ -30,177 +11,8 @@ program .description('CLI to manage Solana program metadata and IDLs') .version(__VERSION__) .configureHelp({ showGlobalOptions: true }); + setGlobalOptions(program); setCommands(program); -program - .command('set-authority') - .description( - 'Set or update an additional authority on canonical metadata accounts.' - ) - .addArgument(seedArgument) - .addArgument(programArgument) - .argument('', 'The new authority to set', address) // TODO: Make it a mandatory option to be explicit. - .action( - async ( - seed: Seed, - program: Address, - newAuthority: string, - _, - cmd: Command - ) => { - const options = cmd.optsWithGlobals() as GlobalOptions; - const client = await getClient(options); - const { metadata, programData } = await getPdaDetails({ - rpc: client.rpc, - program, - authority: client.authority, - seed, - }); - await client.planAndExecute( - sequentialInstructionPlan([ - getSetAuthorityInstruction({ - account: metadata, - authority: client.authority, - newAuthority: address(newAuthority), - program, - programData, - }), - ]) - ); - logSuccess( - `Additional authority successfully set to ${chalk.bold(newAuthority)}` - ); - } - ); - -program - .command('remove-authority') - .description( - 'Remove the additional authority on canonical metadata accounts.' - ) - .addArgument(seedArgument) - .addArgument(programArgument) - .action(async (seed: Seed, program: Address, _, cmd: Command) => { - const options = cmd.optsWithGlobals() as GlobalOptions; - const client = await getClient(options); - const { metadata, programData } = await getPdaDetails({ - rpc: client.rpc, - program, - authority: client.authority, - seed, - }); - await client.planAndExecute( - sequentialInstructionPlan([ - getSetAuthorityInstruction({ - account: metadata, - authority: client.authority, - newAuthority: null, - program, - programData, - }), - ]) - ); - logSuccess('Additional authority successfully removed'); - }); - -program - .command('set-immutable') - .description( - 'Make the metadata account immutable, preventing any further updates.' - ) - .addArgument(seedArgument) - .addArgument(programArgument) - .addOption(nonCanonicalWriteOption) - .action(async (seed: Seed, program: Address, _, cmd: Command) => { - const options = cmd.optsWithGlobals() as GlobalOptions & - NonCanonicalWriteOption; - const client = await getClient(options); - const { authority: programAuthority } = await getProgramAuthority( - client.rpc, - program - ); - if ( - !options.nonCanonical && - client.authority.address !== programAuthority - ) { - logErrorAndExit( - 'You must be the program authority to update a canonical metadata account. Use `--non-canonical` option to update as a third party.' - ); - } - const { metadata, programData } = await getPdaDetails({ - rpc: client.rpc, - program, - authority: client.authority, - seed, - }); - await client.planAndExecute( - sequentialInstructionPlan([ - getSetImmutableInstruction({ - metadata, - authority: client.authority, - program, - programData, - }), - ]) - ); - logSuccess('Metadata account successfully set as immutable'); - }); - -program - .command('close') - .description('Close metadata account and recover rent.') - .addArgument(seedArgument) - .addArgument(programArgument) - .addOption(nonCanonicalWriteOption) - .action(async (seed: Seed, program: Address, _, cmd: Command) => { - const options = cmd.optsWithGlobals() as GlobalOptions & - NonCanonicalWriteOption; - const client = await getClient(options); - const { authority: programAuthority } = await getProgramAuthority( - client.rpc, - program - ); - if ( - !options.nonCanonical && - client.authority.address !== programAuthority - ) { - logErrorAndExit( - 'You must be the program authority to close a canonical metadata account. Use `--non-canonical` option to close as a third party.' - ); - } - const { metadata, programData } = await getPdaDetails({ - rpc: client.rpc, - program, - authority: client.authority, - seed, - }); - await client.planAndExecute( - sequentialInstructionPlan([ - getCloseInstruction({ - account: metadata, - authority: client.authority, - program, - programData, - destination: client.payer.address, - }), - ]) - ); - logSuccess('Account successfully closed and rent recovered'); - }); - -program - .command('list-buffers') - .description('List all buffer accounts owned by an authority.') - .action(async () => { - // TODO - }); - -program - .command('list') - .description('List all metadata accounts owned by an authority.') - .action(async () => { - // TODO - }); - program.parse(); From f57cf56ad072eb32f3605dc3e35dfcb556dacc86 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 11:17:42 +0100 Subject: [PATCH 22/60] wip --- clients/js/src/cli/commands/close.ts | 12 ++++--- clients/js/src/cli/commands/fetch.ts | 21 +++++++---- clients/js/src/cli/commands/index.ts | 33 ++++++++--------- .../js/src/cli/commands/remove-authority.ts | 7 ++-- clients/js/src/cli/commands/set-authority.ts | 7 ++-- clients/js/src/cli/commands/set-immutable.ts | 7 ++-- clients/js/src/cli/commands/write.ts | 33 +++++++++-------- clients/js/src/cli/options.ts | 35 +++++++++---------- clients/js/src/cli/program.ts | 14 ++++---- clients/js/src/cli/utils.ts | 12 +++++++ 10 files changed, 103 insertions(+), 78 deletions(-) diff --git a/clients/js/src/cli/commands/close.ts b/clients/js/src/cli/commands/close.ts index 541bf67..9c984f3 100644 --- a/clients/js/src/cli/commands/close.ts +++ b/clients/js/src/cli/commands/close.ts @@ -1,5 +1,4 @@ import { Address } from '@solana/kit'; -import { Command } from 'commander'; import { getCloseInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; @@ -11,9 +10,9 @@ import { NonCanonicalWriteOption, nonCanonicalWriteOption, } from '../options'; -import { getClient } from '../utils'; +import { CustomCommand, getClient } from '../utils'; -export function setCloseCommand(program: Command): void { +export function setCloseCommand(program: CustomCommand): void { program .command('close') .description('Close metadata account and recover rent.') @@ -24,7 +23,12 @@ export function setCloseCommand(program: Command): void { } type Options = NonCanonicalWriteOption; -async function doClose(seed: Seed, program: Address, _: Options, cmd: Command) { +async function doClose( + seed: Seed, + program: Address, + _: Options, + cmd: CustomCommand +) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); const { authority: programAuthority } = await getProgramAuthority( diff --git a/clients/js/src/cli/commands/fetch.ts b/clients/js/src/cli/commands/fetch.ts index 2ca2b74..c7518df 100644 --- a/clients/js/src/cli/commands/fetch.ts +++ b/clients/js/src/cli/commands/fetch.ts @@ -1,7 +1,8 @@ import { Address, isSolanaError } from '@solana/kit'; import chalk from 'chalk'; -import { Command } from 'commander'; import { fetchMetadataContent } from '../../fetchMetadataContent'; +import { Seed } from '../../generated'; +import { programArgument, seedArgument } from '../arguments'; import { logErrorAndExit, logSuccess } from '../logs'; import { GlobalOptions, @@ -10,11 +11,14 @@ import { outputOption, OutputOption, } from '../options'; -import { getKeyPairSigners, getReadonlyClient, writeFile } from '../utils'; -import { Seed } from '../../generated'; -import { programArgument, seedArgument } from '../arguments'; +import { + CustomCommand, + getKeyPairSigners, + getReadonlyClient, + writeFile, +} from '../utils'; -export function setFetchCommand(program: Command): void { +export function setFetchCommand(program: CustomCommand): void { program .command('fetch') .description('Fetch the content of a metadata account for a given program.') @@ -26,7 +30,12 @@ export function setFetchCommand(program: Command): void { } type Options = NonCanonicalReadOption & OutputOption; -async function doFetch(seed: Seed, program: Address, _: Options, cmd: Command) { +async function doFetch( + seed: Seed, + program: Address, + _: Options, + cmd: CustomCommand +) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = getReadonlyClient(options); const authority = diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index b5e2496..6f640a7 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -1,21 +1,22 @@ -import { Command } from 'commander'; -import { setFetchCommand } from './fetch'; -import { setWriteCommand } from './write'; +import { CustomCommand } from '../utils'; import { setCloseCommand } from './close'; -import { setSetAuthorityCommand } from './set-authority'; +import { setFetchCommand } from './fetch'; import { setRemoveAuthorityCommand } from './remove-authority'; +import { setSetAuthorityCommand } from './set-authority'; import { setSetImmutableCommand } from './set-immutable'; +import { setWriteCommand } from './write'; -export function setCommands(program: Command): void { - // Metadata commands. - setWriteCommand(program); - setFetchCommand(program); - setSetAuthorityCommand(program); - setRemoveAuthorityCommand(program); - setSetImmutableCommand(program); - setCloseCommand(program); - // TODO: list: List all metadata accounts owned by an authority. - - // Buffer commands. - // TODO: list-buffers: List all buffer accounts owned by an authority. +export function setCommands(program: CustomCommand): void { + program + // Metadata commands. + .tap(setWriteCommand) + .tap(setFetchCommand) + .tap(setSetAuthorityCommand) + .tap(setRemoveAuthorityCommand) + .tap(setSetImmutableCommand) + .tap(setCloseCommand) + // TODO: list: List all metadata accounts owned by an authority. + // Buffer commands. + // TODO: list-buffers: List all buffer accounts owned by an authority. + .tap(() => {}); } diff --git a/clients/js/src/cli/commands/remove-authority.ts b/clients/js/src/cli/commands/remove-authority.ts index d16e7d1..8b11882 100644 --- a/clients/js/src/cli/commands/remove-authority.ts +++ b/clients/js/src/cli/commands/remove-authority.ts @@ -1,14 +1,13 @@ import { Address } from '@solana/kit'; -import { Command } from 'commander'; import { getSetAuthorityInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; import { logSuccess } from '../logs'; import { GlobalOptions } from '../options'; -import { getClient } from '../utils'; +import { CustomCommand, getClient } from '../utils'; -export function setRemoveAuthorityCommand(program: Command): void { +export function setRemoveAuthorityCommand(program: CustomCommand): void { program .command('remove-authority') .description( @@ -24,7 +23,7 @@ async function doRemoveAuthority( seed: Seed, program: Address, _: Options, - cmd: Command + cmd: CustomCommand ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts index 23e8c56..3da265f 100644 --- a/clients/js/src/cli/commands/set-authority.ts +++ b/clients/js/src/cli/commands/set-authority.ts @@ -1,15 +1,14 @@ import { address, Address } from '@solana/kit'; import chalk from 'chalk'; -import { Command } from 'commander'; import { getSetAuthorityInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; import { logSuccess } from '../logs'; import { GlobalOptions } from '../options'; -import { getClient } from '../utils'; +import { CustomCommand, getClient } from '../utils'; -export function setSetAuthorityCommand(program: Command): void { +export function setSetAuthorityCommand(program: CustomCommand): void { program .command('set-authority') .description( @@ -27,7 +26,7 @@ async function doSetAuthority( program: Address, newAuthority: Address, _: Options, - cmd: Command + cmd: CustomCommand ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); diff --git a/clients/js/src/cli/commands/set-immutable.ts b/clients/js/src/cli/commands/set-immutable.ts index 9bc7574..2bbc42c 100644 --- a/clients/js/src/cli/commands/set-immutable.ts +++ b/clients/js/src/cli/commands/set-immutable.ts @@ -1,5 +1,4 @@ import { Address } from '@solana/kit'; -import { Command } from 'commander'; import { getSetImmutableInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; @@ -11,9 +10,9 @@ import { NonCanonicalWriteOption, nonCanonicalWriteOption, } from '../options'; -import { getClient } from '../utils'; +import { CustomCommand, getClient } from '../utils'; -export function setSetImmutableCommand(program: Command): void { +export function setSetImmutableCommand(program: CustomCommand): void { program .command('set-immutable') .description( @@ -30,7 +29,7 @@ async function doSetImmutable( seed: Seed, program: Address, _: Options, - cmd: Command + cmd: CustomCommand ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts index b12dd29..f02401a 100644 --- a/clients/js/src/cli/commands/write.ts +++ b/clients/js/src/cli/commands/write.ts @@ -1,8 +1,3 @@ -import chalk from 'chalk'; -import { Command } from 'commander'; -import { getClient, getFormatFromFile, getPackedData } from '../utils'; -import { writeMetadata } from '../../writeMetadata'; -import { Seed } from '../../generated'; import { Address, getBase58Decoder, @@ -10,6 +5,12 @@ import { getTransactionEncoder, Transaction, } from '@solana/kit'; +import chalk from 'chalk'; +import { Seed } from '../../generated'; +import { getProgramAuthority } from '../../utils'; +import { writeMetadata } from '../../writeMetadata'; +import { fileArgument, programArgument, seedArgument } from '../arguments'; +import { logErrorAndExit, logSuccess } from '../logs'; import { GlobalOptions, nonCanonicalWriteOption, @@ -17,19 +18,23 @@ import { setWriteOptions, WriteOptions, } from '../options'; -import { getProgramAuthority } from '../../utils'; -import { logErrorAndExit, logSuccess } from '../logs'; -import { fileArgument, programArgument, seedArgument } from '../arguments'; +import { + CustomCommand, + getClient, + getFormatFromFile, + getPackedData, +} from '../utils'; -export function setWriteCommand(program: Command): void { - const writeCommand = program +export function setWriteCommand(program: CustomCommand): void { + program .command('write') .description('Create or update a metadata account for a given program.') .addArgument(seedArgument) .addArgument(programArgument) - .addArgument(fileArgument); - setWriteOptions(writeCommand); - writeCommand.addOption(nonCanonicalWriteOption); + .addArgument(fileArgument) + .tap(setWriteOptions) + .addOption(nonCanonicalWriteOption) + .action(doWrite); } type Options = WriteOptions & NonCanonicalWriteOption; @@ -38,7 +43,7 @@ export async function doWrite( program: Address, file: string | undefined, _: Options, - cmd: Command + cmd: CustomCommand ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index b0f4656..268c1b0 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -1,15 +1,16 @@ import { address, type Address, type MicroLamports } from '@solana/kit'; -import { Command, Option } from 'commander'; -import { logErrorAndExit } from './logs'; +import { Option } from 'commander'; import { Compression, Encoding, Format } from '../generated'; +import { logErrorAndExit } from './logs'; +import { CustomCommand } from './utils'; export type GlobalOptions = KeypairOption & PayerOption & PriorityFeesOption & RpcOption; -export function setGlobalOptions(command: Command) { - return command +export function setGlobalOptions(command: CustomCommand) { + command .addOption(keypairOption) .addOption(payerOption) .addOption(priorityFeesOption) @@ -53,20 +54,18 @@ export type WriteOptions = TextOption & EncodingOption & FormatOption; -export function setWriteOptions(command: Command) { - return ( - command - // Data sources. - .addOption(textOption) - .addOption(urlOption) - .addOption(accountOption) - .addOption(accountOffsetOption) - .addOption(accountLengthOption) - // Enums. - .addOption(compressionOption) - .addOption(encodingOption) - .addOption(formatOption) - ); +export function setWriteOptions(command: CustomCommand) { + command + // Data sources. + .addOption(textOption) + .addOption(urlOption) + .addOption(accountOption) + .addOption(accountOffsetOption) + .addOption(accountLengthOption) + // Enums. + .addOption(compressionOption) + .addOption(encodingOption) + .addOption(formatOption); } export type TextOption = { text?: string }; diff --git a/clients/js/src/cli/program.ts b/clients/js/src/cli/program.ts index 07dd85b..88b57df 100644 --- a/clients/js/src/cli/program.ts +++ b/clients/js/src/cli/program.ts @@ -1,18 +1,16 @@ #!/usr/bin/env node -import { Command } from 'commander'; import { setCommands } from './commands'; import { setGlobalOptions } from './options'; +import { CustomCommand } from './utils'; // Define the CLI program. -const program = new Command(); +const program = new CustomCommand(); program .name('program-metadata') .description('CLI to manage Solana program metadata and IDLs') .version(__VERSION__) - .configureHelp({ showGlobalOptions: true }); - -setGlobalOptions(program); -setCommands(program); - -program.parse(); + .configureHelp({ showGlobalOptions: true }) + .tap(setGlobalOptions) + .tap(setCommands) + .parse(); diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index 4d5f1e1..82a3b2a 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -33,9 +33,21 @@ import { packUrlData, } from '../packData'; import { logErrorAndExit, logWarning } from './logs'; +import { Command } from 'commander'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; +export class CustomCommand extends Command { + createCommand(name: string) { + return new CustomCommand(name); + } + + tap(fn: (command: CustomCommand) => void) { + fn(this); + return this; + } +} + export type Client = ReadonlyClient & { authority: KeyPairSigner; executor: TransactionPlanExecutor; From 8afc2e54c8cbe9914babe6bd80096e715d0be6f2 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 11:42:19 +0100 Subject: [PATCH 23/60] wip --- clients/js/src/cli/commands/close.ts | 4 +- .../js/src/cli/commands/remove-authority.ts | 3 +- clients/js/src/cli/commands/set-authority.ts | 5 +- clients/js/src/cli/commands/set-immutable.ts | 4 +- clients/js/src/cli/options.ts | 25 ++++++-- clients/js/src/cli/utils.ts | 58 +++++++++++-------- 6 files changed, 61 insertions(+), 38 deletions(-) diff --git a/clients/js/src/cli/commands/close.ts b/clients/js/src/cli/commands/close.ts index 9c984f3..7b6c783 100644 --- a/clients/js/src/cli/commands/close.ts +++ b/clients/js/src/cli/commands/close.ts @@ -4,7 +4,7 @@ import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { getProgramAuthority } from '../../utils'; import { programArgument, seedArgument } from '../arguments'; -import { logErrorAndExit, logSuccess } from '../logs'; +import { logErrorAndExit } from '../logs'; import { GlobalOptions, NonCanonicalWriteOption, @@ -47,6 +47,7 @@ async function doClose( seed, }); await client.planAndExecute( + 'Close metadata account and recover rent', sequentialInstructionPlan([ getCloseInstruction({ account: metadata, @@ -57,5 +58,4 @@ async function doClose( }), ]) ); - logSuccess('Account successfully closed and rent recovered'); } diff --git a/clients/js/src/cli/commands/remove-authority.ts b/clients/js/src/cli/commands/remove-authority.ts index 8b11882..e4526cb 100644 --- a/clients/js/src/cli/commands/remove-authority.ts +++ b/clients/js/src/cli/commands/remove-authority.ts @@ -3,7 +3,6 @@ import { getSetAuthorityInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; -import { logSuccess } from '../logs'; import { GlobalOptions } from '../options'; import { CustomCommand, getClient } from '../utils'; @@ -34,6 +33,7 @@ async function doRemoveAuthority( seed, }); await client.planAndExecute( + 'Remove additional authority from metadata account', sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, @@ -44,5 +44,4 @@ async function doRemoveAuthority( }), ]) ); - logSuccess('Additional authority successfully removed'); } diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts index 3da265f..48efeb7 100644 --- a/clients/js/src/cli/commands/set-authority.ts +++ b/clients/js/src/cli/commands/set-authority.ts @@ -4,7 +4,6 @@ import { getSetAuthorityInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; -import { logSuccess } from '../logs'; import { GlobalOptions } from '../options'; import { CustomCommand, getClient } from '../utils'; @@ -37,6 +36,7 @@ async function doSetAuthority( seed, }); await client.planAndExecute( + `Set additional authority on metadata account to ${chalk.bold(newAuthority)}`, sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, @@ -47,7 +47,4 @@ async function doSetAuthority( }), ]) ); - logSuccess( - `Additional authority successfully set to ${chalk.bold(newAuthority)}` - ); } diff --git a/clients/js/src/cli/commands/set-immutable.ts b/clients/js/src/cli/commands/set-immutable.ts index 2bbc42c..ca3a750 100644 --- a/clients/js/src/cli/commands/set-immutable.ts +++ b/clients/js/src/cli/commands/set-immutable.ts @@ -4,7 +4,7 @@ import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { getProgramAuthority } from '../../utils'; import { programArgument, seedArgument } from '../arguments'; -import { logErrorAndExit, logSuccess } from '../logs'; +import { logErrorAndExit } from '../logs'; import { GlobalOptions, NonCanonicalWriteOption, @@ -49,6 +49,7 @@ async function doSetImmutable( seed, }); await client.planAndExecute( + 'Make metadata account immutable', sequentialInstructionPlan([ getSetImmutableInstruction({ metadata, @@ -58,5 +59,4 @@ async function doSetImmutable( }), ]) ); - logSuccess('Metadata account successfully set as immutable'); } diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index 268c1b0..790d3b8 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -7,14 +7,16 @@ import { CustomCommand } from './utils'; export type GlobalOptions = KeypairOption & PayerOption & PriorityFeesOption & - RpcOption; + RpcOption & + ExportOption; export function setGlobalOptions(command: CustomCommand) { command .addOption(keypairOption) .addOption(payerOption) .addOption(priorityFeesOption) - .addOption(rpcOption); + .addOption(rpcOption) + .addOption(exportOption); } export type KeypairOption = { keypair?: string }; @@ -45,6 +47,21 @@ export const rpcOption = new Option('--rpc ', 'RPC URL.').default( 'solana config or localhost' ); +export type ExportOption = { export: Address | boolean }; +export const exportOption = new Option( + '--export [address]', + 'When provided, export transactions instead of running them. An optional address can be provided to override the local keypair as the authority.' +) + .default(false) + .argParser((value: string | undefined): Address | boolean => { + if (value === undefined) return true; + try { + return address(value); + } catch { + logErrorAndExit(`Invalid export address: "${value}"`); + } + }); + export type WriteOptions = TextOption & UrlOption & AccountOption & @@ -175,9 +192,7 @@ export const nonCanonicalReadOption = new Option( ) .default(false) .argParser((value: string | undefined): Address | boolean => { - if (value === undefined) { - return true; - } + if (value === undefined) return true; try { return address(value); } catch { diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index 82a3b2a..8032124 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -6,17 +6,18 @@ import { address, Commitment, createKeyPairSignerFromBytes, + createNoopSigner, createSolanaRpc, createSolanaRpcSubscriptions, - KeyPairSigner, - MicroLamports, + MessageSigner, Rpc, RpcSubscriptions, SolanaRpcApi, SolanaRpcSubscriptionsApi, + TransactionSigner, } from '@solana/kit'; +import { Command } from 'commander'; import { parse as parseYaml } from 'yaml'; -import { WriteOptions } from './options'; import { Format } from '../generated'; import { createDefaultTransactionPlanExecutor, @@ -32,8 +33,15 @@ import { packExternalData, packUrlData, } from '../packData'; -import { logErrorAndExit, logWarning } from './logs'; -import { Command } from 'commander'; +import { logErrorAndExit, logSuccess, logWarning } from './logs'; +import { + ExportOption, + GlobalOptions, + KeypairOption, + PayerOption, + RpcOption, + WriteOptions, +} from './options'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; @@ -49,21 +57,17 @@ export class CustomCommand extends Command { } export type Client = ReadonlyClient & { - authority: KeyPairSigner; + authority: TransactionSigner & MessageSigner; executor: TransactionPlanExecutor; - payer: KeyPairSigner; + payer: TransactionSigner & MessageSigner; planAndExecute: ( + description: string, instructionPlan: InstructionPlan ) => Promise; planner: TransactionPlanner; }; -export async function getClient(options: { - keypair?: string; - payer?: string; - priorityFees?: MicroLamports; - rpc?: string; -}): Promise { +export async function getClient(options: GlobalOptions): Promise { const readonlyClient = getReadonlyClient(options); const [authority, payer] = await getKeyPairSigners( options, @@ -79,10 +83,14 @@ export async function getClient(options: { parallelChunkSize: 5, }); const planAndExecute = async ( + description: string, instructionPlan: InstructionPlan ): Promise => { + console.log(description); const transactionPlan = await planner(instructionPlan); - return await executor(transactionPlan); + const result = await executor(transactionPlan); + logSuccess('Operation executed successfully'); + return result; }; return { ...readonlyClient, @@ -100,7 +108,7 @@ export type ReadonlyClient = { rpcSubscriptions: RpcSubscriptions; }; -export function getReadonlyClient(options: { rpc?: string }): ReadonlyClient { +export function getReadonlyClient(options: RpcOption): ReadonlyClient { const configs = getSolanaConfigs(); const rpcUrl = getRpcUrl(options, configs); const rpcSubscriptionsUrl = getRpcSubscriptionsUrl(rpcUrl, configs); @@ -111,7 +119,7 @@ export function getReadonlyClient(options: { rpc?: string }): ReadonlyClient { }; } -function getRpcUrl(options: { rpc?: string }, configs: SolanaConfigs): string { +function getRpcUrl(options: RpcOption, configs: SolanaConfigs): string { if (options.rpc) return options.rpc; if (configs.json_rpc_url) return configs.json_rpc_url; return LOCALHOST_URL; @@ -146,19 +154,25 @@ function getSolanaConfigPath(): string { } export async function getKeyPairSigners( - options: { keypair?: string; payer?: string }, + options: KeypairOption & PayerOption & ExportOption, configs: SolanaConfigs -): Promise<[KeyPairSigner, KeyPairSigner]> { +): Promise< + [TransactionSigner & MessageSigner, TransactionSigner & MessageSigner] +> { const keypairPath = getKeyPairPath(options, configs); const keypairPromise = getKeyPairSignerFromPath(keypairPath); const payerPromise = options.payer ? getKeyPairSignerFromPath(options.payer) : keypairPromise; - return await Promise.all([keypairPromise, payerPromise]); + const [keypair, payer] = await Promise.all([keypairPromise, payerPromise]); + if (typeof options.export === 'string') { + return [createNoopSigner(options.export), payer]; + } + return [keypair, payer]; } function getKeyPairPath( - options: { keypair?: string }, + options: KeypairOption, configs: SolanaConfigs ): string { if (options.keypair) return options.keypair; @@ -166,9 +180,7 @@ function getKeyPairPath( return path.join(os.homedir(), '.config', 'solana', 'id.json'); } -async function getKeyPairSignerFromPath( - keypairPath: string -): Promise { +async function getKeyPairSignerFromPath(keypairPath: string) { if (!fs.existsSync(keypairPath)) { logErrorAndExit(`Keypair file not found at: ${keypairPath}`); } From fe338971f5c43ed01fd988e0c1bd1311b94c76a0 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 13:30:26 +0100 Subject: [PATCH 24/60] wip --- clients/js/src/createMetadata.ts | 55 +++++++++++++++ .../instructionPlans/transactionPlanner.ts | 12 ++++ clients/js/src/updateMetadata.ts | 70 ++++++++++++++++--- clients/js/src/writeMetadata.ts | 2 +- 4 files changed, 128 insertions(+), 11 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 86a108e..c616a9a 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -1,5 +1,7 @@ import { getTransferSolInstruction } from '@solana-program/system'; import { + Address, + GetAccountInfoApi, GetMinimumBalanceForRentExemptionApi, Lamports, ReadonlyUint8Array, @@ -15,8 +17,11 @@ import { import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, + InstructionPlan, + isValidInstructionPlan, parallelInstructionPlan, sequentialInstructionPlan, + TransactionPlanner, } from './instructionPlans'; import { getExtendInstructionPlan, @@ -68,6 +73,56 @@ export async function createMetadata( return { metadata, result }; } +export async function getCreateMetadataInstructionPlanAlt( + input: MetadataInput & { + planner: TransactionPlanner; + rpc: Rpc; + } +): Promise { + const planner = input.planner; + const [{ programData, isCanonical, metadata }, rent] = await Promise.all([ + getPdaDetails(input), + input.rpc + .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .send(), + ]); + const extendedInput = { + ...input, + programData: isCanonical ? programData : undefined, + metadata, + rent, + }; + + const plan = + getCreateMetadataInstructionPlanUsingInstructionData(extendedInput); + const validPlan = await isValidInstructionPlan(plan, planner); + return validPlan + ? plan + : getCreateMetadataInstructionPlanUsingBuffer(extendedInput); +} + +export async function getCreateMetadataInstructionPlan( + input: Omit & { + buffer?: Address; // TODO + data: ReadonlyUint8Array; + payer: TransactionSigner; + planner: TransactionPlanner; + rpc: Rpc; + } +): Promise { + const rent = await input.rpc + .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .send(); + const extendedInput = { ...input, rent }; + + const plan = + getCreateMetadataInstructionPlanUsingInstructionData(extendedInput); + const validPlan = await isValidInstructionPlan(plan, input.planner); + return validPlan + ? plan + : getCreateMetadataInstructionPlanUsingBuffer(extendedInput); +} + export function getCreateMetadataInstructionPlanUsingInstructionData( input: InitializeInput & { payer: TransactionSigner; rent: Lamports } ) { diff --git a/clients/js/src/instructionPlans/transactionPlanner.ts b/clients/js/src/instructionPlans/transactionPlanner.ts index f960d73..17e71ce 100644 --- a/clients/js/src/instructionPlans/transactionPlanner.ts +++ b/clients/js/src/instructionPlans/transactionPlanner.ts @@ -5,3 +5,15 @@ export type TransactionPlanner = ( instructionPlan: InstructionPlan, config?: { abortSignal?: AbortSignal } ) => Promise; + +export async function isValidInstructionPlan( + instructionPlan: InstructionPlan, + planner: TransactionPlanner +) { + try { + await planner(instructionPlan); + return true; + } catch { + return false; + } +} diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index ac47487..93232a7 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -3,6 +3,7 @@ import { getTransferSolInstruction, } from '@solana-program/system'; import { + Account, generateKeyPairSigner, GetAccountInfoApi, GetMinimumBalanceForRentExemptionApi, @@ -19,14 +20,18 @@ import { getSetAuthorityInstruction, getSetDataInstruction, getTrimInstruction, + Metadata, PROGRAM_METADATA_PROGRAM_ADDRESS, SetDataInput, } from './generated'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, + InstructionPlan, + isValidInstructionPlan, parallelInstructionPlan, sequentialInstructionPlan, + TransactionPlanner, } from './instructionPlans'; import { getExtendInstructionPlan, @@ -55,13 +60,12 @@ export async function updateMetadata( parallelChunkSize: 5, }); - const [{ programData, isCanonical, metadata }, bufferRent] = - await Promise.all([ - getPdaDetails(input), - input.rpc - .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) - .send(), - ]); + const [{ programData, isCanonical, metadata }, fullRent] = await Promise.all([ + getPdaDetails(input), + input.rpc + .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .send(), + ]); const metadataAccount = await fetchMetadata(input.rpc, metadata); if (!metadataAccount.data.mutable) { @@ -84,7 +88,7 @@ export async function updateMetadata( programData: isCanonical ? programData : undefined, metadata, buffer, - bufferRent, + fullRent, extraRent, sizeDifference, }; @@ -99,6 +103,52 @@ export async function updateMetadata( return { metadata, result }; } +export async function getUpdateMetadataInstructionPlan( + input: Omit & { + metadata: Account; + // TODO: from buffer. + data: ReadonlyUint8Array; + payer: TransactionSigner; + planner: TransactionPlanner; + rpc: Rpc; + } +): Promise { + if (!input.metadata.data.mutable) { + throw new Error('Metadata account is immutable'); + } + + const fullRentPromise = input.rpc + .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .send(); + const sizeDifference = + BigInt(input.data.length) - BigInt(input.metadata.data.data.length); + const extraRentPromise = + sizeDifference > 0 + ? input.rpc.getMinimumBalanceForRentExemption(sizeDifference).send() + : Promise.resolve(lamports(0n)); + const [fullRent, extraRent, buffer] = await Promise.all([ + fullRentPromise, + extraRentPromise, + generateKeyPairSigner(), + ]); + + const extendedInput = { + ...input, + metadata: input.metadata.address, + buffer, + fullRent, + extraRent, + sizeDifference, + }; + + const plan = + getUpdateMetadataInstructionPlanUsingInstructionData(extendedInput); + const validPlan = await isValidInstructionPlan(plan, input.planner); + return validPlan + ? plan + : getUpdateMetadataInstructionPlanUsingBuffer(extendedInput); +} + export function getUpdateMetadataInstructionPlanUsingInstructionData( input: Omit & { extraRent: Lamports; @@ -134,10 +184,10 @@ export function getUpdateMetadataInstructionPlanUsingInstructionData( export function getUpdateMetadataInstructionPlanUsingBuffer( input: Omit & { buffer: TransactionSigner; - bufferRent: Lamports; closeBuffer?: boolean; data: ReadonlyUint8Array; extraRent: Lamports; + fullRent: Lamports; payer: TransactionSigner; sizeDifference: number | bigint; } @@ -155,7 +205,7 @@ export function getUpdateMetadataInstructionPlanUsingBuffer( getCreateAccountInstruction({ payer: input.payer, newAccount: input.buffer, - lamports: input.bufferRent, + lamports: input.fullRent, space: getAccountSize(input.data.length), programAddress: PROGRAM_METADATA_PROGRAM_ADDRESS, }), diff --git a/clients/js/src/writeMetadata.ts b/clients/js/src/writeMetadata.ts index f08b01c..2709c0d 100644 --- a/clients/js/src/writeMetadata.ts +++ b/clients/js/src/writeMetadata.ts @@ -87,7 +87,7 @@ export async function writeMetadata( programData: isCanonical ? programData : undefined, metadata, buffer, - bufferRent: rent, + fullRent: rent, extraRent, sizeDifference, }; From 36c6873c59a99f0d083171fd11858660af15f4bf Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 13:46:12 +0100 Subject: [PATCH 25/60] wip --- clients/js/src/createMetadata.ts | 49 ++++---------------------------- clients/js/src/updateMetadata.ts | 40 ++++---------------------- 2 files changed, 11 insertions(+), 78 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index c616a9a..1f9b243 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -1,7 +1,6 @@ import { getTransferSolInstruction } from '@solana-program/system'; import { Address, - GetAccountInfoApi, GetMinimumBalanceForRentExemptionApi, Lamports, ReadonlyUint8Array, @@ -50,57 +49,19 @@ export async function createMetadata( parallelChunkSize: 5, }); - const [{ programData, isCanonical, metadata }, rent] = await Promise.all([ - getPdaDetails(input), - input.rpc - .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) - .send(), - ]); - const extendedInput = { + const { programData, isCanonical, metadata } = await getPdaDetails(input); + const instructionPlan = await getCreateMetadataInstructionPlan({ ...input, programData: isCanonical ? programData : undefined, metadata, - rent, - }; - - const transactionPlan = await planner( - getCreateMetadataInstructionPlanUsingInstructionData(extendedInput) - ).catch(() => - planner(getCreateMetadataInstructionPlanUsingBuffer(extendedInput)) - ); + planner, + }); + const transactionPlan = await planner(instructionPlan); const result = await executor(transactionPlan); return { metadata, result }; } -export async function getCreateMetadataInstructionPlanAlt( - input: MetadataInput & { - planner: TransactionPlanner; - rpc: Rpc; - } -): Promise { - const planner = input.planner; - const [{ programData, isCanonical, metadata }, rent] = await Promise.all([ - getPdaDetails(input), - input.rpc - .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) - .send(), - ]); - const extendedInput = { - ...input, - programData: isCanonical ? programData : undefined, - metadata, - rent, - }; - - const plan = - getCreateMetadataInstructionPlanUsingInstructionData(extendedInput); - const validPlan = await isValidInstructionPlan(plan, planner); - return validPlan - ? plan - : getCreateMetadataInstructionPlanUsingBuffer(extendedInput); -} - export async function getCreateMetadataInstructionPlan( input: Omit & { buffer?: Address; // TODO diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index 93232a7..7957e16 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -60,45 +60,17 @@ export async function updateMetadata( parallelChunkSize: 5, }); - const [{ programData, isCanonical, metadata }, fullRent] = await Promise.all([ - getPdaDetails(input), - input.rpc - .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) - .send(), - ]); - + const { programData, isCanonical, metadata } = await getPdaDetails(input); const metadataAccount = await fetchMetadata(input.rpc, metadata); - if (!metadataAccount.data.mutable) { - throw new Error('Metadata account is immutable'); - } - const sizeDifference = - BigInt(input.data.length) - BigInt(metadataAccount.data.data.length); - const extraRentPromise = - sizeDifference > 0 - ? input.rpc.getMinimumBalanceForRentExemption(sizeDifference).send() - : Promise.resolve(lamports(0n)); - const [extraRent, buffer] = await Promise.all([ - extraRentPromise, - generateKeyPairSigner(), - ]); - - const extendedInput = { + const instructionPlan = await getUpdateMetadataInstructionPlan({ ...input, programData: isCanonical ? programData : undefined, - metadata, - buffer, - fullRent, - extraRent, - sizeDifference, - }; - - const transactionPlan = await planner( - getUpdateMetadataInstructionPlanUsingInstructionData(extendedInput) - ).catch(() => - planner(getUpdateMetadataInstructionPlanUsingBuffer(extendedInput)) - ); + metadata: metadataAccount, + planner, + }); + const transactionPlan = await planner(instructionPlan); const result = await executor(transactionPlan); return { metadata, result }; } From fbc0d830a4866ca0f7b3ace2ae781c4158afd5d1 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 13:51:38 +0100 Subject: [PATCH 26/60] wip --- clients/js/src/writeMetadata.ts | 74 ++++++--------------------------- 1 file changed, 12 insertions(+), 62 deletions(-) diff --git a/clients/js/src/writeMetadata.ts b/clients/js/src/writeMetadata.ts index 2709c0d..6beed74 100644 --- a/clients/js/src/writeMetadata.ts +++ b/clients/js/src/writeMetadata.ts @@ -1,25 +1,17 @@ import { - generateKeyPairSigner, GetAccountInfoApi, GetMinimumBalanceForRentExemptionApi, - lamports, Rpc, } from '@solana/kit'; -import { - getCreateMetadataInstructionPlanUsingBuffer, - getCreateMetadataInstructionPlanUsingInstructionData, -} from './createMetadata'; +import { getCreateMetadataInstructionPlan } from './createMetadata'; import { fetchMaybeMetadata } from './generated'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, } from './instructionPlans'; import { getPdaDetails } from './internals'; -import { - getUpdateMetadataInstructionPlanUsingBuffer, - getUpdateMetadataInstructionPlanUsingInstructionData, -} from './updateMetadata'; -import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; +import { getUpdateMetadataInstructionPlan } from './updateMetadata'; +import { MetadataInput, MetadataResponse } from './utils'; export async function writeMetadata( input: MetadataInput & { @@ -40,64 +32,22 @@ export async function writeMetadata( parallelChunkSize: 5, }); - const [{ programData, isCanonical, metadata }, rent] = await Promise.all([ - getPdaDetails(input), - input.rpc - .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) - .send(), - ]); - + const { programData, isCanonical, metadata } = await getPdaDetails(input); const metadataAccount = await fetchMaybeMetadata(input.rpc, metadata); - - if (!metadataAccount.exists) { - const extendedInput = { - ...input, - programData: isCanonical ? programData : undefined, - metadata, - rent, - }; - - const transactionPlan = await planner( - getCreateMetadataInstructionPlanUsingInstructionData(extendedInput) - ).catch(() => - planner(getCreateMetadataInstructionPlanUsingBuffer(extendedInput)) - ); - - const result = await executor(transactionPlan); - return { metadata, result }; - } - - if (!metadataAccount.data.mutable) { - throw new Error('Metadata account is immutable'); - } - - const sizeDifference = - BigInt(input.data.length) - BigInt(metadataAccount.data.data.length); - const extraRentPromise = - sizeDifference > 0 - ? input.rpc.getMinimumBalanceForRentExemption(sizeDifference).send() - : Promise.resolve(lamports(0n)); - const [extraRent, buffer] = await Promise.all([ - extraRentPromise, - generateKeyPairSigner(), - ]); - const extendedInput = { ...input, programData: isCanonical ? programData : undefined, - metadata, - buffer, - fullRent: rent, - extraRent, - sizeDifference, + planner, }; - const transactionPlan = await planner( - getUpdateMetadataInstructionPlanUsingInstructionData(extendedInput) - ).catch(() => - planner(getUpdateMetadataInstructionPlanUsingBuffer(extendedInput)) - ); + const instructionPlan = metadataAccount.exists + ? await getUpdateMetadataInstructionPlan({ + ...extendedInput, + metadata: metadataAccount, + }) + : await getCreateMetadataInstructionPlan({ ...extendedInput, metadata }); + const transactionPlan = await planner(instructionPlan); const result = await executor(transactionPlan); return { metadata, result }; } From 2832faeb2baef4257d25d3bae0c42786337b88a9 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 13:54:03 +0100 Subject: [PATCH 27/60] wip --- clients/js/src/createMetadata.ts | 9 +++--- clients/js/src/internals.ts | 50 +------------------------------- clients/js/src/updateMetadata.ts | 9 +++--- clients/js/src/utils.ts | 46 ++++++++++++++++++++++++++++- 4 files changed, 56 insertions(+), 58 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 1f9b243..37c7534 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -22,13 +22,14 @@ import { sequentialInstructionPlan, TransactionPlanner, } from './instructionPlans'; +import { getPdaDetails, REALLOC_LIMIT } from './internals'; import { + getAccountSize, getExtendInstructionPlan, - getPdaDetails, getWriteInstructionPlan, - REALLOC_LIMIT, -} from './internals'; -import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; + MetadataInput, + MetadataResponse, +} from './utils'; export async function createMetadata( input: MetadataInput & { diff --git a/clients/js/src/internals.ts b/clients/js/src/internals.ts index 127c8c6..532177a 100644 --- a/clients/js/src/internals.ts +++ b/clients/js/src/internals.ts @@ -1,21 +1,10 @@ import { Address, GetAccountInfoApi, - ReadonlyUint8Array, Rpc, TransactionSigner, } from '@solana/kit'; -import { - findMetadataPda, - getExtendInstruction, - getWriteInstruction, - SeedArgs, -} from './generated'; -import { - getLinearIterableInstructionPlan, - getReallocIterableInstructionPlan, - IterableInstructionPlan, -} from './instructionPlans'; +import { findMetadataPda, SeedArgs } from './generated'; import { getProgramAuthority } from './utils'; export const REALLOC_LIMIT = 10_240; @@ -48,40 +37,3 @@ export async function getPdaDetails(input: { }); return { metadata, isCanonical, programData }; } - -export function getExtendInstructionPlan(input: { - account: Address; - authority: TransactionSigner; - extraLength: number; - program?: Address; - programData?: Address; -}): IterableInstructionPlan { - return getReallocIterableInstructionPlan({ - totalSize: input.extraLength, - getInstruction: (size) => - getExtendInstruction({ - account: input.account, - authority: input.authority, - length: size, - program: input.program, - programData: input.programData, - }), - }); -} - -export function getWriteInstructionPlan(input: { - buffer: Address; - authority: TransactionSigner; - data: ReadonlyUint8Array; -}): IterableInstructionPlan { - return getLinearIterableInstructionPlan({ - totalLength: input.data.length, - getInstruction: (offset, length) => - getWriteInstruction({ - buffer: input.buffer, - authority: input.authority, - offset, - data: input.data.slice(offset, offset + length), - }), - }); -} diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index 7957e16..d73e214 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -33,13 +33,14 @@ import { sequentialInstructionPlan, TransactionPlanner, } from './instructionPlans'; +import { getPdaDetails, REALLOC_LIMIT } from './internals'; import { + getAccountSize, getExtendInstructionPlan, - getPdaDetails, getWriteInstructionPlan, - REALLOC_LIMIT, -} from './internals'; -import { getAccountSize, MetadataInput, MetadataResponse } from './utils'; + MetadataInput, + MetadataResponse, +} from './utils'; export async function updateMetadata( input: MetadataInput & { diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 9a22f40..342ec11 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -22,9 +22,16 @@ import { DataSourceArgs, EncodingArgs, FormatArgs, + getExtendInstruction, + getWriteInstruction, SeedArgs, } from './generated'; -import { TransactionPlanResult } from './instructionPlans'; +import { + getLinearIterableInstructionPlan, + getReallocIterableInstructionPlan, + IterableInstructionPlan, + TransactionPlanResult, +} from './instructionPlans'; export const ACCOUNT_HEADER_LENGTH = 96; @@ -162,3 +169,40 @@ function getLoaderV3Decoders() { ]), ] as const; } + +export function getExtendInstructionPlan(input: { + account: Address; + authority: TransactionSigner; + extraLength: number; + program?: Address; + programData?: Address; +}): IterableInstructionPlan { + return getReallocIterableInstructionPlan({ + totalSize: input.extraLength, + getInstruction: (size) => + getExtendInstruction({ + account: input.account, + authority: input.authority, + length: size, + program: input.program, + programData: input.programData, + }), + }); +} + +export function getWriteInstructionPlan(input: { + buffer: Address; + authority: TransactionSigner; + data: ReadonlyUint8Array; +}): IterableInstructionPlan { + return getLinearIterableInstructionPlan({ + totalLength: input.data.length, + getInstruction: (offset, length) => + getWriteInstruction({ + buffer: input.buffer, + authority: input.authority, + offset, + data: input.data.slice(offset, offset + length), + }), + }); +} From 52e662cb1868cc6e4472e7649f573f8d2213c783 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 13:57:49 +0100 Subject: [PATCH 28/60] wip --- clients/js/src/createMetadata.ts | 18 ++++++------------ clients/js/src/internals.ts | 25 +++++++++++++++++++++++++ clients/js/src/updateMetadata.ts | 18 ++++++------------ clients/js/src/writeMetadata.ts | 19 +++++-------------- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 37c7534..b68e453 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -15,14 +15,17 @@ import { } from './generated'; import { createDefaultTransactionPlanExecutor, - createDefaultTransactionPlanner, InstructionPlan, isValidInstructionPlan, parallelInstructionPlan, sequentialInstructionPlan, TransactionPlanner, } from './instructionPlans'; -import { getPdaDetails, REALLOC_LIMIT } from './internals'; +import { + getDefaultTransactionPlannerAndExecutor, + getPdaDetails, + REALLOC_LIMIT, +} from './internals'; import { getAccountSize, getExtendInstructionPlan, @@ -40,16 +43,7 @@ export async function createMetadata( >[0]['rpcSubscriptions']; } ): Promise { - const planner = createDefaultTransactionPlanner({ - feePayer: input.payer, - computeUnitPrice: input.priorityFees, - }); - const executor = createDefaultTransactionPlanExecutor({ - rpc: input.rpc, - rpcSubscriptions: input.rpcSubscriptions, - parallelChunkSize: 5, - }); - + const { planner, executor } = getDefaultTransactionPlannerAndExecutor(input); const { programData, isCanonical, metadata } = await getPdaDetails(input); const instructionPlan = await getCreateMetadataInstructionPlan({ ...input, diff --git a/clients/js/src/internals.ts b/clients/js/src/internals.ts index 532177a..56c5436 100644 --- a/clients/js/src/internals.ts +++ b/clients/js/src/internals.ts @@ -1,11 +1,16 @@ import { Address, GetAccountInfoApi, + MicroLamports, Rpc, TransactionSigner, } from '@solana/kit'; import { findMetadataPda, SeedArgs } from './generated'; import { getProgramAuthority } from './utils'; +import { + createDefaultTransactionPlanExecutor, + createDefaultTransactionPlanner, +} from './instructionPlans'; export const REALLOC_LIMIT = 10_240; @@ -37,3 +42,23 @@ export async function getPdaDetails(input: { }); return { metadata, isCanonical, programData }; } + +export function getDefaultTransactionPlannerAndExecutor(input: { + payer: TransactionSigner; + priorityFees?: MicroLamports; + rpc: Parameters[0]['rpc']; + rpcSubscriptions: Parameters< + typeof createDefaultTransactionPlanExecutor + >[0]['rpcSubscriptions']; +}) { + const planner = createDefaultTransactionPlanner({ + feePayer: input.payer, + computeUnitPrice: input.priorityFees, + }); + const executor = createDefaultTransactionPlanExecutor({ + rpc: input.rpc, + rpcSubscriptions: input.rpcSubscriptions, + parallelChunkSize: 5, + }); + return { planner, executor }; +} diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index d73e214..84f6ed1 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -26,14 +26,17 @@ import { } from './generated'; import { createDefaultTransactionPlanExecutor, - createDefaultTransactionPlanner, InstructionPlan, isValidInstructionPlan, parallelInstructionPlan, sequentialInstructionPlan, TransactionPlanner, } from './instructionPlans'; -import { getPdaDetails, REALLOC_LIMIT } from './internals'; +import { + getDefaultTransactionPlannerAndExecutor, + getPdaDetails, + REALLOC_LIMIT, +} from './internals'; import { getAccountSize, getExtendInstructionPlan, @@ -51,16 +54,7 @@ export async function updateMetadata( >[0]['rpcSubscriptions']; } ): Promise { - const planner = createDefaultTransactionPlanner({ - feePayer: input.payer, - computeUnitPrice: input.priorityFees, - }); - const executor = createDefaultTransactionPlanExecutor({ - rpc: input.rpc, - rpcSubscriptions: input.rpcSubscriptions, - parallelChunkSize: 5, - }); - + const { planner, executor } = getDefaultTransactionPlannerAndExecutor(input); const { programData, isCanonical, metadata } = await getPdaDetails(input); const metadataAccount = await fetchMetadata(input.rpc, metadata); diff --git a/clients/js/src/writeMetadata.ts b/clients/js/src/writeMetadata.ts index 6beed74..c615455 100644 --- a/clients/js/src/writeMetadata.ts +++ b/clients/js/src/writeMetadata.ts @@ -5,11 +5,11 @@ import { } from '@solana/kit'; import { getCreateMetadataInstructionPlan } from './createMetadata'; import { fetchMaybeMetadata } from './generated'; +import { createDefaultTransactionPlanExecutor } from './instructionPlans'; import { - createDefaultTransactionPlanExecutor, - createDefaultTransactionPlanner, -} from './instructionPlans'; -import { getPdaDetails } from './internals'; + getDefaultTransactionPlannerAndExecutor, + getPdaDetails, +} from './internals'; import { getUpdateMetadataInstructionPlan } from './updateMetadata'; import { MetadataInput, MetadataResponse } from './utils'; @@ -22,16 +22,7 @@ export async function writeMetadata( >[0]['rpcSubscriptions']; } ): Promise { - const planner = createDefaultTransactionPlanner({ - feePayer: input.payer, - computeUnitPrice: input.priorityFees, - }); - const executor = createDefaultTransactionPlanExecutor({ - rpc: input.rpc, - rpcSubscriptions: input.rpcSubscriptions, - parallelChunkSize: 5, - }); - + const { planner, executor } = getDefaultTransactionPlannerAndExecutor(input); const { programData, isCanonical, metadata } = await getPdaDetails(input); const metadataAccount = await fetchMaybeMetadata(input.rpc, metadata); const extendedInput = { From 82d60bda01a064fa4c28d839a8b16d6febfa9990 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 14:05:12 +0100 Subject: [PATCH 29/60] wip --- clients/js/src/writeMetadata.ts | 40 ++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/clients/js/src/writeMetadata.ts b/clients/js/src/writeMetadata.ts index c615455..75a1301 100644 --- a/clients/js/src/writeMetadata.ts +++ b/clients/js/src/writeMetadata.ts @@ -1,11 +1,15 @@ import { GetAccountInfoApi, GetMinimumBalanceForRentExemptionApi, + MaybeAccount, Rpc, } from '@solana/kit'; import { getCreateMetadataInstructionPlan } from './createMetadata'; -import { fetchMaybeMetadata } from './generated'; -import { createDefaultTransactionPlanExecutor } from './instructionPlans'; +import { fetchMaybeMetadata, Metadata } from './generated'; +import { + createDefaultTransactionPlanExecutor, + InstructionPlan, +} from './instructionPlans'; import { getDefaultTransactionPlannerAndExecutor, getPdaDetails, @@ -25,20 +29,34 @@ export async function writeMetadata( const { planner, executor } = getDefaultTransactionPlannerAndExecutor(input); const { programData, isCanonical, metadata } = await getPdaDetails(input); const metadataAccount = await fetchMaybeMetadata(input.rpc, metadata); - const extendedInput = { + + const instructionPlan = await getWriteMetadataInstructionPlan({ ...input, + metadata: metadataAccount, programData: isCanonical ? programData : undefined, planner, - }; - - const instructionPlan = metadataAccount.exists - ? await getUpdateMetadataInstructionPlan({ - ...extendedInput, - metadata: metadataAccount, - }) - : await getCreateMetadataInstructionPlan({ ...extendedInput, metadata }); + }); const transactionPlan = await planner(instructionPlan); const result = await executor(transactionPlan); return { metadata, result }; } + +export async function getWriteMetadataInstructionPlan( + input: Omit< + Parameters[0], + 'metadata' + > & { + metadata: MaybeAccount; + } +): Promise { + return input.metadata.exists + ? await getUpdateMetadataInstructionPlan({ + ...input, + metadata: input.metadata, + }) + : await getCreateMetadataInstructionPlan({ + ...input, + metadata: input.metadata.address, + }); +} From 84f7b92a73dbba46e3301dcdfad5d2c12904e63a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 14:23:33 +0100 Subject: [PATCH 30/60] wip --- clients/js/src/createMetadata.ts | 90 ++++++++++++++++++++++++++++---- clients/js/src/updateMetadata.ts | 1 + clients/js/src/utils.ts | 3 +- clients/js/src/writeMetadata.ts | 11 +++- 4 files changed, 93 insertions(+), 12 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index b68e453..c7e4b1a 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -1,6 +1,6 @@ import { getTransferSolInstruction } from '@solana-program/system'; import { - Address, + Account, GetMinimumBalanceForRentExemptionApi, Lamports, ReadonlyUint8Array, @@ -8,8 +8,11 @@ import { TransactionSigner, } from '@solana/kit'; import { + Buffer, + fetchBuffer, getAllocateInstruction, getInitializeInstruction, + getWriteInstruction, InitializeInput, PROGRAM_METADATA_PROGRAM_ADDRESS, } from './generated'; @@ -45,8 +48,12 @@ export async function createMetadata( ): Promise { const { planner, executor } = getDefaultTransactionPlannerAndExecutor(input); const { programData, isCanonical, metadata } = await getPdaDetails(input); + const buffer = input.buffer + ? await fetchBuffer(input.rpc, input.buffer) + : undefined; const instructionPlan = await getCreateMetadataInstructionPlan({ ...input, + buffer, programData: isCanonical ? programData : undefined, metadata, planner, @@ -59,24 +66,43 @@ export async function createMetadata( export async function getCreateMetadataInstructionPlan( input: Omit & { - buffer?: Address; // TODO - data: ReadonlyUint8Array; + buffer?: Account; + data?: ReadonlyUint8Array; payer: TransactionSigner; planner: TransactionPlanner; rpc: Rpc; } ): Promise { + if (!input.buffer && !input.data) { + throw new Error( + 'Either `buffer` or `data` must be provided to create a new metadata account.' + ); + } + + const data = ( + input.buffer ? input.buffer.data.data : input.data + ) as ReadonlyUint8Array; const rent = await input.rpc - .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .getMinimumBalanceForRentExemption(getAccountSize(data.length)) .send(); - const extendedInput = { ...input, rent }; - const plan = - getCreateMetadataInstructionPlanUsingInstructionData(extendedInput); + if (input.buffer) { + return getCreateMetadataInstructionPlanUsingExistingBuffer({ + ...input, + buffer: input.buffer, + rent, + }); + } + + const plan = getCreateMetadataInstructionPlanUsingInstructionData({ + ...input, + rent, + data, + }); const validPlan = await isValidInstructionPlan(plan, input.planner); return validPlan ? plan - : getCreateMetadataInstructionPlanUsingBuffer(extendedInput); + : getCreateMetadataInstructionPlanUsingNewBuffer({ ...input, rent, data }); } export function getCreateMetadataInstructionPlanUsingInstructionData( @@ -92,7 +118,7 @@ export function getCreateMetadataInstructionPlanUsingInstructionData( ]); } -export function getCreateMetadataInstructionPlanUsingBuffer( +export function getCreateMetadataInstructionPlanUsingNewBuffer( input: Omit & { data: ReadonlyUint8Array; payer: TransactionSigner; @@ -137,3 +163,49 @@ export function getCreateMetadataInstructionPlanUsingBuffer( }), ]); } + +export function getCreateMetadataInstructionPlanUsingExistingBuffer( + input: Omit & { + buffer: Account; + payer: TransactionSigner; + rent: Lamports; + } +) { + const data = input.buffer.data.data; + return sequentialInstructionPlan([ + getTransferSolInstruction({ + source: input.payer, + destination: input.metadata, + amount: input.rent, + }), + getAllocateInstruction({ + buffer: input.metadata, + authority: input.authority, + program: input.program, + programData: input.programData, + seed: input.seed, + }), + ...(data.length > REALLOC_LIMIT + ? [ + getExtendInstructionPlan({ + account: input.metadata, + authority: input.authority, + extraLength: data.length, + program: input.program, + programData: input.programData, + }), + ] + : []), + getWriteInstruction({ + buffer: input.metadata, + authority: input.authority, + sourceBuffer: input.buffer.address, + offset: 0, + }), + getInitializeInstruction({ + ...input, + system: PROGRAM_METADATA_PROGRAM_ADDRESS, + data: undefined, + }), + ]); +} diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index 84f6ed1..62513af 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -63,6 +63,7 @@ export async function updateMetadata( programData: isCanonical ? programData : undefined, metadata: metadataAccount, planner, + data: input.data!, // TODO: Temporary. }); const transactionPlan = await planner(instructionPlan); diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 342ec11..2fe69eb 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -53,7 +53,8 @@ export type MetadataInput = { compression: CompressionArgs; format: FormatArgs; dataSource: DataSourceArgs; - data: ReadonlyUint8Array; + data?: ReadonlyUint8Array; + buffer?: Address; /** * Extra fees to pay in microlamports per CU. * Defaults to no extra fees. diff --git a/clients/js/src/writeMetadata.ts b/clients/js/src/writeMetadata.ts index 75a1301..7155780 100644 --- a/clients/js/src/writeMetadata.ts +++ b/clients/js/src/writeMetadata.ts @@ -5,7 +5,7 @@ import { Rpc, } from '@solana/kit'; import { getCreateMetadataInstructionPlan } from './createMetadata'; -import { fetchMaybeMetadata, Metadata } from './generated'; +import { fetchBuffer, fetchMaybeMetadata, Metadata } from './generated'; import { createDefaultTransactionPlanExecutor, InstructionPlan, @@ -28,10 +28,16 @@ export async function writeMetadata( ): Promise { const { planner, executor } = getDefaultTransactionPlannerAndExecutor(input); const { programData, isCanonical, metadata } = await getPdaDetails(input); - const metadataAccount = await fetchMaybeMetadata(input.rpc, metadata); + const [metadataAccount, buffer] = await Promise.all([ + fetchMaybeMetadata(input.rpc, metadata), + input.buffer + ? fetchBuffer(input.rpc, input.buffer) + : Promise.resolve(undefined), + ]); const instructionPlan = await getWriteMetadataInstructionPlan({ ...input, + buffer, metadata: metadataAccount, programData: isCanonical ? programData : undefined, planner, @@ -54,6 +60,7 @@ export async function getWriteMetadataInstructionPlan( ? await getUpdateMetadataInstructionPlan({ ...input, metadata: input.metadata, + data: input.data!, // TODO: Temporary. }) : await getCreateMetadataInstructionPlan({ ...input, From 0cfe9b4d3bbba4f10931e0d4b885658a55196908 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 14:30:18 +0100 Subject: [PATCH 31/60] wip --- clients/js/test/createMetadata.test.ts | 111 +++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/clients/js/test/createMetadata.test.ts b/clients/js/test/createMetadata.test.ts index 0c70f40..22b00f5 100644 --- a/clients/js/test/createMetadata.test.ts +++ b/clients/js/test/createMetadata.test.ts @@ -13,6 +13,7 @@ import { import { createDefaultSolanaClient, createDeployedProgram, + createKeypairBuffer, generateKeyPairSignerWithSol, } from './_setup'; @@ -94,6 +95,48 @@ test('it creates a canonical metadata account with data larger than a transactio }); }); +test('it creates a canonical metadata account using an existing buffer', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const [program] = await createDeployedProgram(client, authority); + + // And an existing buffer with the following data. + const data = getUtf8Encoder().encode('{"standard":"dummyIdl"}'); + const buffer = await createKeypairBuffer(client, { payer: authority, data }); + + // When we create a canonical metadata account using the existing buffer. + const { metadata } = await createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + buffer: buffer.address, + }); + + // Then we expect the following metadata account to be created. + const account = await fetchMetadata(client.rpc, metadata); + t.like(account.data, { + discriminator: AccountDiscriminator.Metadata, + program, + authority: none(), + mutable: true, + canonical: true, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + format: Format.Json, + dataSource: DataSource.Direct, + dataLength: data.length, + data, + }); +}); + test('it creates a non-canonical metadata account', async (t) => { // Given the following authority and deployed program. const client = createDefaultSolanaClient(); @@ -171,3 +214,71 @@ test('it creates a non-canonical metadata account with data larger than a transa data: largeData, }); }); + +test('it creates a non-canonical metadata account using an existing buffer', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const program = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + // And an existing buffer with the following data. + const data = getUtf8Encoder().encode('{"standard":"dummyIdl"}'); + const buffer = await createKeypairBuffer(client, { payer: authority, data }); + + // When we create a non-canonical metadata account using the existing buffer. + const { metadata } = await createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + buffer: buffer.address, + }); + + // Then we expect the following metadata account to be created. + const account = await fetchMetadata(client.rpc, metadata); + t.like(account.data, { + discriminator: AccountDiscriminator.Metadata, + program, + authority: some(authority.address), + mutable: true, + canonical: false, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + format: Format.Json, + dataSource: DataSource.Direct, + dataLength: data.length, + data, + }); +}); + +test('it cannot create a metadata account if no data or buffer is provided', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const program = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + // When we try to create a metadata account without providing data or buffer. + const promise = createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + }); + + // Then we expect the following error to be thrown. + await t.throwsAsync(promise, { + message: + 'Either `buffer` or `data` must be provided to create a new metadata account.', + }); +}); From f3372fca9dd8db038958cee4af21d05a132dbd77 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 14:55:46 +0100 Subject: [PATCH 32/60] wip --- clients/js/src/createMetadata.ts | 49 ++++++---- clients/js/src/updateMetadata.ts | 118 ++++++++++++++++++++++--- clients/js/src/writeMetadata.ts | 1 - clients/js/test/createMetadata.test.ts | 4 + clients/js/test/updateMetadata.test.ts | 8 ++ 5 files changed, 151 insertions(+), 29 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index c7e4b1a..10e5d1a 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -1,6 +1,7 @@ import { getTransferSolInstruction } from '@solana-program/system'; import { Account, + Address, GetMinimumBalanceForRentExemptionApi, Lamports, ReadonlyUint8Array, @@ -11,6 +12,7 @@ import { Buffer, fetchBuffer, getAllocateInstruction, + getCloseInstruction, getInitializeInstruction, getWriteInstruction, InitializeInput, @@ -47,16 +49,18 @@ export async function createMetadata( } ): Promise { const { planner, executor } = getDefaultTransactionPlannerAndExecutor(input); - const { programData, isCanonical, metadata } = await getPdaDetails(input); - const buffer = input.buffer - ? await fetchBuffer(input.rpc, input.buffer) - : undefined; + const [{ programData, isCanonical, metadata }, buffer] = await Promise.all([ + getPdaDetails(input), + input.buffer + ? fetchBuffer(input.rpc, input.buffer) + : Promise.resolve(undefined), + ]); const instructionPlan = await getCreateMetadataInstructionPlan({ ...input, buffer, - programData: isCanonical ? programData : undefined, metadata, planner, + programData: isCanonical ? programData : undefined, }); const transactionPlan = await planner(instructionPlan); @@ -89,20 +93,19 @@ export async function getCreateMetadataInstructionPlan( if (input.buffer) { return getCreateMetadataInstructionPlanUsingExistingBuffer({ ...input, - buffer: input.buffer, + buffer: input.buffer.address, + dataLength: data.length, rent, }); } - const plan = getCreateMetadataInstructionPlanUsingInstructionData({ - ...input, - rent, - data, - }); + const extendedInput = { ...input, rent, data }; + const plan = + getCreateMetadataInstructionPlanUsingInstructionData(extendedInput); const validPlan = await isValidInstructionPlan(plan, input.planner); return validPlan ? plan - : getCreateMetadataInstructionPlanUsingNewBuffer({ ...input, rent, data }); + : getCreateMetadataInstructionPlanUsingNewBuffer(extendedInput); } export function getCreateMetadataInstructionPlanUsingInstructionData( @@ -166,12 +169,13 @@ export function getCreateMetadataInstructionPlanUsingNewBuffer( export function getCreateMetadataInstructionPlanUsingExistingBuffer( input: Omit & { - buffer: Account; + buffer: Address; + dataLength: number; payer: TransactionSigner; rent: Lamports; + closeBuffer?: boolean; } ) { - const data = input.buffer.data.data; return sequentialInstructionPlan([ getTransferSolInstruction({ source: input.payer, @@ -185,12 +189,12 @@ export function getCreateMetadataInstructionPlanUsingExistingBuffer( programData: input.programData, seed: input.seed, }), - ...(data.length > REALLOC_LIMIT + ...(input.dataLength > REALLOC_LIMIT ? [ getExtendInstructionPlan({ account: input.metadata, authority: input.authority, - extraLength: data.length, + extraLength: input.dataLength, program: input.program, programData: input.programData, }), @@ -199,7 +203,7 @@ export function getCreateMetadataInstructionPlanUsingExistingBuffer( getWriteInstruction({ buffer: input.metadata, authority: input.authority, - sourceBuffer: input.buffer.address, + sourceBuffer: input.buffer, offset: 0, }), getInitializeInstruction({ @@ -207,5 +211,16 @@ export function getCreateMetadataInstructionPlanUsingExistingBuffer( system: PROGRAM_METADATA_PROGRAM_ADDRESS, data: undefined, }), + ...(input.closeBuffer + ? [ + getCloseInstruction({ + account: input.buffer, + authority: input.authority, + destination: input.payer.address, + program: input.program, + programData: input.programData, + }), + ] + : []), ]); } diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index 62513af..bca6512 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -4,6 +4,7 @@ import { } from '@solana-program/system'; import { Account, + Address, generateKeyPairSigner, GetAccountInfoApi, GetMinimumBalanceForRentExemptionApi, @@ -14,12 +15,15 @@ import { TransactionSigner, } from '@solana/kit'; import { + Buffer, + fetchBuffer, fetchMetadata, getAllocateInstruction, getCloseInstruction, getSetAuthorityInstruction, getSetDataInstruction, getTrimInstruction, + getWriteInstruction, Metadata, PROGRAM_METADATA_PROGRAM_ADDRESS, SetDataInput, @@ -56,14 +60,19 @@ export async function updateMetadata( ): Promise { const { planner, executor } = getDefaultTransactionPlannerAndExecutor(input); const { programData, isCanonical, metadata } = await getPdaDetails(input); - const metadataAccount = await fetchMetadata(input.rpc, metadata); + const [metadataAccount, buffer] = await Promise.all([ + fetchMetadata(input.rpc, metadata), + input.buffer + ? fetchBuffer(input.rpc, input.buffer) + : Promise.resolve(undefined), + ]); const instructionPlan = await getUpdateMetadataInstructionPlan({ ...input, - programData: isCanonical ? programData : undefined, + buffer, metadata: metadataAccount, planner, - data: input.data!, // TODO: Temporary. + programData: isCanonical ? programData : undefined, }); const transactionPlan = await planner(instructionPlan); @@ -74,22 +83,30 @@ export async function updateMetadata( export async function getUpdateMetadataInstructionPlan( input: Omit & { metadata: Account; - // TODO: from buffer. - data: ReadonlyUint8Array; + buffer?: Account; + data?: ReadonlyUint8Array; payer: TransactionSigner; planner: TransactionPlanner; rpc: Rpc; } ): Promise { + if (!input.buffer && !input.data) { + throw new Error( + 'Either `buffer` or `data` must be provided to update a new metadata account.' + ); + } if (!input.metadata.data.mutable) { throw new Error('Metadata account is immutable'); } + const data = ( + input.buffer ? input.buffer.data.data : input.data + ) as ReadonlyUint8Array; const fullRentPromise = input.rpc - .getMinimumBalanceForRentExemption(getAccountSize(input.data.length)) + .getMinimumBalanceForRentExemption(getAccountSize(data.length)) .send(); const sizeDifference = - BigInt(input.data.length) - BigInt(input.metadata.data.data.length); + BigInt(data.length) - BigInt(input.metadata.data.data.length); const extraRentPromise = sizeDifference > 0 ? input.rpc.getMinimumBalanceForRentExemption(sizeDifference).send() @@ -100,12 +117,24 @@ export async function getUpdateMetadataInstructionPlan( generateKeyPairSigner(), ]); + if (input.buffer) { + return getUpdateMetadataInstructionPlanUsingExistingBuffer({ + ...input, + buffer: input.buffer.address, + extraRent, + metadata: input.metadata.address, + fullRent, + sizeDifference, + }); + } + const extendedInput = { ...input, - metadata: input.metadata.address, buffer, - fullRent, + data, extraRent, + fullRent, + metadata: input.metadata.address, sizeDifference, }; @@ -114,7 +143,7 @@ export async function getUpdateMetadataInstructionPlan( const validPlan = await isValidInstructionPlan(plan, input.planner); return validPlan ? plan - : getUpdateMetadataInstructionPlanUsingBuffer(extendedInput); + : getUpdateMetadataInstructionPlanUsingNewBuffer(extendedInput); } export function getUpdateMetadataInstructionPlanUsingInstructionData( @@ -149,7 +178,7 @@ export function getUpdateMetadataInstructionPlanUsingInstructionData( ]); } -export function getUpdateMetadataInstructionPlanUsingBuffer( +export function getUpdateMetadataInstructionPlanUsingNewBuffer( input: Omit & { buffer: TransactionSigner; closeBuffer?: boolean; @@ -233,3 +262,70 @@ export function getUpdateMetadataInstructionPlanUsingBuffer( : []), ]); } + +export function getUpdateMetadataInstructionPlanUsingExistingBuffer( + input: Omit & { + buffer: Address; + closeBuffer?: boolean; + extraRent: Lamports; + fullRent: Lamports; + payer: TransactionSigner; + sizeDifference: number | bigint; + } +) { + return sequentialInstructionPlan([ + ...(input.sizeDifference > 0 + ? [ + getTransferSolInstruction({ + source: input.payer, + destination: input.metadata, + amount: input.extraRent, + }), + ] + : []), + ...(input.sizeDifference > REALLOC_LIMIT + ? [ + getExtendInstructionPlan({ + account: input.metadata, + authority: input.authority, + extraLength: Number(input.sizeDifference), + program: input.program, + programData: input.programData, + }), + ] + : []), + getWriteInstruction({ + buffer: input.buffer, + authority: input.authority, + sourceBuffer: input.buffer, + offset: 0, + }), + getSetDataInstruction({ + ...input, + buffer: input.buffer, + data: undefined, + }), + ...(input.closeBuffer + ? [ + getCloseInstruction({ + account: input.buffer, + authority: input.authority, + destination: input.payer.address, + program: input.program, + programData: input.programData, + }), + ] + : []), + ...(input.sizeDifference < 0 + ? [ + getTrimInstruction({ + account: input.metadata, + authority: input.authority, + destination: input.payer.address, + program: input.program, + programData: input.programData, + }), + ] + : []), + ]); +} diff --git a/clients/js/src/writeMetadata.ts b/clients/js/src/writeMetadata.ts index 7155780..749d723 100644 --- a/clients/js/src/writeMetadata.ts +++ b/clients/js/src/writeMetadata.ts @@ -60,7 +60,6 @@ export async function getWriteMetadataInstructionPlan( ? await getUpdateMetadataInstructionPlan({ ...input, metadata: input.metadata, - data: input.data!, // TODO: Temporary. }) : await getCreateMetadataInstructionPlan({ ...input, diff --git a/clients/js/test/createMetadata.test.ts b/clients/js/test/createMetadata.test.ts index 22b00f5..6b62f42 100644 --- a/clients/js/test/createMetadata.test.ts +++ b/clients/js/test/createMetadata.test.ts @@ -282,3 +282,7 @@ test('it cannot create a metadata account if no data or buffer is provided', asy 'Either `buffer` or `data` must be provided to create a new metadata account.', }); }); + +test.todo( + 'it can close an existing buffer after using it to create a new metadata account' +); diff --git a/clients/js/test/updateMetadata.test.ts b/clients/js/test/updateMetadata.test.ts index e5d8802..4bb20b4 100644 --- a/clients/js/test/updateMetadata.test.ts +++ b/clients/js/test/updateMetadata.test.ts @@ -228,3 +228,11 @@ test('it updates a non-canonical metadata account with data larger than a transa data: newData, }); }); + +test.todo( + 'it can close a new buffer after using it to update a new metadata account' +); + +test.todo( + 'it can close an existing buffer after using it to update a new metadata account' +); From b4938318ced2588432177644b88fb2d2c4e862e4 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 15:10:42 +0100 Subject: [PATCH 33/60] wip --- clients/js/src/updateMetadata.ts | 9 +- clients/js/test/updateMetadata.test.ts | 159 +++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 8 deletions(-) diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index bca6512..3f81264 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -23,7 +23,6 @@ import { getSetAuthorityInstruction, getSetDataInstruction, getTrimInstruction, - getWriteInstruction, Metadata, PROGRAM_METADATA_PROGRAM_ADDRESS, SetDataInput, @@ -92,7 +91,7 @@ export async function getUpdateMetadataInstructionPlan( ): Promise { if (!input.buffer && !input.data) { throw new Error( - 'Either `buffer` or `data` must be provided to update a new metadata account.' + 'Either `buffer` or `data` must be provided to update a metadata account.' ); } if (!input.metadata.data.mutable) { @@ -294,12 +293,6 @@ export function getUpdateMetadataInstructionPlanUsingExistingBuffer( }), ] : []), - getWriteInstruction({ - buffer: input.buffer, - authority: input.authority, - sourceBuffer: input.buffer, - offset: 0, - }), getSetDataInstruction({ ...input, buffer: input.buffer, diff --git a/clients/js/test/updateMetadata.test.ts b/clients/js/test/updateMetadata.test.ts index 4bb20b4..9d722b3 100644 --- a/clients/js/test/updateMetadata.test.ts +++ b/clients/js/test/updateMetadata.test.ts @@ -14,6 +14,7 @@ import { import { createDefaultSolanaClient, createDeployedProgram, + createKeypairBuffer, generateKeyPairSignerWithSol, } from './_setup'; @@ -123,6 +124,65 @@ test('it updates a canonical metadata account with data larger than a transactio }); }); +test('it updates a canonical metadata account using an existing buffer', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const [program] = await createDeployedProgram(client, authority); + + // And the following existing canonical metadata account. + await createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + data: getUtf8Encoder().encode('OLD'), + }); + + // And an existing buffer with the following data. + const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); + const buffer = await createKeypairBuffer(client, { + payer: authority, + data: newData, + }); + + // When we update the metadata account using the existing buffer. + const { metadata } = await updateMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Base58, + compression: Compression.Gzip, + dataSource: DataSource.Url, + format: Format.Toml, + buffer: buffer.address, + }); + + // Then we expect the metadata account to be updated. + const account = await fetchMetadata(client.rpc, metadata); + t.like(account.data, { + discriminator: AccountDiscriminator.Metadata, + program, + authority: none(), + mutable: true, + canonical: true, + seed: 'idl', + encoding: Encoding.Base58, + compression: Compression.Gzip, + dataSource: DataSource.Url, + format: Format.Toml, + dataLength: newData.length, + data: newData, + }); +}); + test('it updates a non-canonical metadata account', async (t) => { // Given the following authority and deployed program. const client = createDefaultSolanaClient(); @@ -229,6 +289,105 @@ test('it updates a non-canonical metadata account with data larger than a transa }); }); +test('it updates a non-canonical metadata account using an existing buffer', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const program = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + // And the following existing non-canonical metadata account. + await createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + data: getUtf8Encoder().encode('OLD'), + }); + + // And an existing buffer with the following data. + const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); + const buffer = await createKeypairBuffer(client, { + payer: authority, + data: newData, + }); + + // When we update the metadata account using the existing buffer. + const { metadata } = await updateMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Base58, + compression: Compression.Gzip, + dataSource: DataSource.Url, + format: Format.Toml, + buffer: buffer.address, + }); + + // Then we expect the metadata account to be updated. + const account = await fetchMetadata(client.rpc, metadata); + t.like(account.data, { + discriminator: AccountDiscriminator.Metadata, + program, + authority: some(authority.address), + mutable: true, + canonical: false, + seed: 'idl', + encoding: Encoding.Base58, + compression: Compression.Gzip, + dataSource: DataSource.Url, + format: Format.Toml, + dataLength: newData.length, + data: newData, + }); +}); + +test('it cannot update a metadata account if no data or buffer is provided', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const program = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + // And the following existing canonical metadata account. + await createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + data: getUtf8Encoder().encode('OLD'), + }); + + // When we try to update a metadata account without providing data or buffer. + const promise = updateMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + }); + + // Then we expect the following error to be thrown. + await t.throwsAsync(promise, { + message: + 'Either `buffer` or `data` must be provided to update a metadata account.', + }); +}); + test.todo( 'it can close a new buffer after using it to update a new metadata account' ); From 508dd5d35b94bccac8fb874e0ae20636498a5cb8 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 15:24:12 +0100 Subject: [PATCH 34/60] wip --- clients/js/test/_setup.ts | 15 ++++ clients/js/test/createMetadata.test.ts | 41 ++++++++- clients/js/test/updateMetadata.test.ts | 114 +++++++++++++++++++++++-- 3 files changed, 161 insertions(+), 9 deletions(-) diff --git a/clients/js/test/_setup.ts b/clients/js/test/_setup.ts index 9268c85..00341c9 100644 --- a/clients/js/test/_setup.ts +++ b/clients/js/test/_setup.ts @@ -46,6 +46,7 @@ import { getExtendInstruction, getInitializeInstruction, getProgramDataPda as getLoaderV3ProgramDataPda, + getSetAuthorityInstruction, getWriteInstruction, InitializeInput, LOADER_V3_PROGRAM_ADDRESS, @@ -330,6 +331,20 @@ export async function createKeypairBuffer( return buffer; } +export async function setAuthority( + client: Client, + input: Parameters[0] & { + payer: TransactionSigner; + } +) { + const setAuthorityIx = getSetAuthorityInstruction(input); + await pipe( + await createDefaultTransaction(client, input.payer), + (tx) => appendTransactionMessageInstructions([setAuthorityIx], tx), + (tx) => signAndSendTransaction(client, tx) + ); +} + type PartialExcept = Partial> & Pick; export async function createMetadata( client: Client, diff --git a/clients/js/test/createMetadata.test.ts b/clients/js/test/createMetadata.test.ts index 6b62f42..340a63c 100644 --- a/clients/js/test/createMetadata.test.ts +++ b/clients/js/test/createMetadata.test.ts @@ -6,6 +6,7 @@ import { createMetadata, DataSource, Encoding, + fetchMaybeBuffer, fetchMetadata, Format, Metadata, @@ -15,6 +16,7 @@ import { createDeployedProgram, createKeypairBuffer, generateKeyPairSignerWithSol, + setAuthority, } from './_setup'; test('it creates a canonical metadata account', async (t) => { @@ -283,6 +285,39 @@ test('it cannot create a metadata account if no data or buffer is provided', asy }); }); -test.todo( - 'it can close an existing buffer after using it to create a new metadata account' -); +test('it can close an existing buffer after using it to create a new metadata account', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const [program] = await createDeployedProgram(client, authority); + + // And an existing buffer with the same authority. + const data = getUtf8Encoder().encode('{"standard":"dummyIdl"}'); + const buffer = await createKeypairBuffer(client, { payer: authority, data }); + await setAuthority(client, { + payer: authority, + authority: buffer, + account: buffer.address, + newAuthority: authority.address, + }); + + // When we create a canonical metadata account + // using the existing buffer and the `closeBuffer` option. + await createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + buffer: buffer.address, + closeBuffer: true, + }); + + // Then we expect the buffer account to no longer exist. + const bufferAccount = await fetchMaybeBuffer(client.rpc, buffer.address); + t.false(bufferAccount.exists); +}); diff --git a/clients/js/test/updateMetadata.test.ts b/clients/js/test/updateMetadata.test.ts index 9d722b3..c7885ee 100644 --- a/clients/js/test/updateMetadata.test.ts +++ b/clients/js/test/updateMetadata.test.ts @@ -6,6 +6,7 @@ import { createMetadata, DataSource, Encoding, + fetchMaybeBuffer, fetchMetadata, Format, Metadata, @@ -16,6 +17,7 @@ import { createDeployedProgram, createKeypairBuffer, generateKeyPairSignerWithSol, + setAuthority, } from './_setup'; test('it updates a canonical metadata account', async (t) => { @@ -388,10 +390,110 @@ test('it cannot update a metadata account if no data or buffer is provided', asy }); }); -test.todo( - 'it can close a new buffer after using it to update a new metadata account' -); +test('it can close a new buffer after using it to update a new metadata account', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const [program] = await createDeployedProgram(client, authority); + + // And the following existing canonical metadata account. + await createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + data: getUtf8Encoder().encode('OLD'), + }); + + // When we update the metadata account using the close buffer. + const newData = getUtf8Encoder().encode('x'.repeat(3_000)); + const { metadata } = await updateMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Base58, + compression: Compression.Gzip, + dataSource: DataSource.Url, + format: Format.Toml, + data: newData, + closeBuffer: true, + }); + + // Then we expect the metadata account to be updated. + const account = await fetchMetadata(client.rpc, metadata); + t.like(account.data, { + discriminator: AccountDiscriminator.Metadata, + program, + authority: none(), + mutable: true, + canonical: true, + seed: 'idl', + encoding: Encoding.Base58, + compression: Compression.Gzip, + dataSource: DataSource.Url, + format: Format.Toml, + dataLength: newData.length, + data: newData, + }); +}); + +test('it can close an existing buffer after using it to update a new metadata account', async (t) => { + // Given the following authority and deployed program. + const client = createDefaultSolanaClient(); + const authority = await generateKeyPairSignerWithSol(client); + const [program] = await createDeployedProgram(client, authority); -test.todo( - 'it can close an existing buffer after using it to update a new metadata account' -); + // And the following existing canonical metadata account. + await createMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Utf8, + compression: Compression.None, + dataSource: DataSource.Direct, + format: Format.Json, + data: getUtf8Encoder().encode('OLD'), + }); + + // And an existing buffer with the following data and the same authority. + const newData = getUtf8Encoder().encode('NEW DATA WITH MORE BYTES'); + const buffer = await createKeypairBuffer(client, { + payer: authority, + data: newData, + }); + await setAuthority(client, { + payer: authority, + account: buffer.address, + authority: buffer, + newAuthority: authority.address, + }); + + // When we update the metadata account using + // the existing buffer and the `closeBuffer` option. + await updateMetadata({ + ...client, + payer: authority, + authority, + program, + seed: 'idl', + encoding: Encoding.Base58, + compression: Compression.Gzip, + dataSource: DataSource.Url, + format: Format.Toml, + buffer: buffer.address, + closeBuffer: true, + }); + + // Then we expect the buffer account to no longer exist. + const bufferAccount = await fetchMaybeBuffer(client.rpc, buffer.address); + t.false(bufferAccount.exists); +}); From f43d22436365f5c25786af7ec5ef799c42259d3a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 15:43:31 +0100 Subject: [PATCH 35/60] wip --- clients/js/src/cli/commands/write.ts | 68 ++++++++++++---------------- clients/js/src/cli/utils.ts | 14 ++++++ clients/js/src/createMetadata.ts | 1 + 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts index f02401a..2a2b645 100644 --- a/clients/js/src/cli/commands/write.ts +++ b/clients/js/src/cli/commands/write.ts @@ -1,16 +1,9 @@ -import { - Address, - getBase58Decoder, - getBase64Decoder, - getTransactionEncoder, - Transaction, -} from '@solana/kit'; +import { Address } from '@solana/kit'; import chalk from 'chalk'; -import { Seed } from '../../generated'; -import { getProgramAuthority } from '../../utils'; -import { writeMetadata } from '../../writeMetadata'; +import { fetchBuffer, fetchMaybeMetadata, Seed } from '../../generated'; +import { getPdaDetails } from '../../internals'; +import { getWriteMetadataInstructionPlan } from '../../writeMetadata'; import { fileArgument, programArgument, seedArgument } from '../arguments'; -import { logErrorAndExit, logSuccess } from '../logs'; import { GlobalOptions, nonCanonicalWriteOption, @@ -19,6 +12,7 @@ import { WriteOptions, } from '../options'; import { + assertValidIsCanonical, CustomCommand, getClient, getFormatFromFile, @@ -47,16 +41,21 @@ export async function doWrite( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { authority: programAuthority } = await getProgramAuthority( - client.rpc, - program - ); - if (!options.nonCanonical && client.authority.address !== programAuthority) { - logErrorAndExit( - 'You must be the program authority to write to a canonical metadata account. Use `--non-canonical` option to write as a third party.' - ); - } - await writeMetadata({ + const { programData, isCanonical, metadata } = await getPdaDetails({ + ...client, + program, + seed, + }); + assertValidIsCanonical(isCanonical, options); + const tempBuffer: Address | undefined = undefined; + const [metadataAccount, buffer] = await Promise.all([ + fetchMaybeMetadata(client.rpc, metadata), + tempBuffer + ? fetchBuffer(client.rpc, tempBuffer) + : Promise.resolve(undefined), + ]); + + const instructionPlan = await getWriteMetadataInstructionPlan({ ...client, ...getPackedData(file, options), payer: client.payer, @@ -65,23 +64,14 @@ export async function doWrite( seed, format: options.format ?? getFormatFromFile(file), closeBuffer: true, - priorityFees: options.priorityFees, + buffer, + metadata: metadataAccount, + programData: isCanonical ? programData : undefined, + planner: client.planner, }); - const exportTransaction = false; // TODO: Option - if (exportTransaction) { - const transactionBytes = getTransactionEncoder().encode({} as Transaction); - const base64EncodedTransaction = - getBase64Decoder().decode(transactionBytes); - const base58EncodedTransaction = - getBase58Decoder().decode(transactionBytes); - logSuccess( - `Buffer successfully created for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"!\n` + - `Use the following transaction data to apply the buffer:\n\n` + - `[base64]\n${base64EncodedTransaction}\n\n[base58]\n${base58EncodedTransaction}` - ); - } else { - logSuccess( - `Metadata uploaded successfully for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"!` - ); - } + + await client.planAndExecute( + `Upload metadata for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"`, + instructionPlan + ); } diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index 8032124..bb616c7 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -38,6 +38,7 @@ import { ExportOption, GlobalOptions, KeypairOption, + NonCanonicalWriteOption, PayerOption, RpcOption, WriteOptions, @@ -262,3 +263,16 @@ export function writeFile(filepath: string, content: string): void { fs.mkdirSync(path.dirname(filepath), { recursive: true }); fs.writeFileSync(filepath, content); } + +export function assertValidIsCanonical( + isCanonical: boolean, + options: NonCanonicalWriteOption +): void { + const wantsCanonical = !options.nonCanonical; + if (wantsCanonical && !isCanonical) { + logErrorAndExit( + 'You must be the program authority or an authorized authority to manage canonical metadata accounts. ' + + 'Use the `--non-canonical` option to manage metadata accounts as a third party.' + ); + } +} diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 10e5d1a..9c36385 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -75,6 +75,7 @@ export async function getCreateMetadataInstructionPlan( payer: TransactionSigner; planner: TransactionPlanner; rpc: Rpc; + closeBuffer?: boolean; } ): Promise { if (!input.buffer && !input.data) { From e217472846686d8cea8cc6b5b241dd687f195f18 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 15:46:22 +0100 Subject: [PATCH 36/60] wip --- clients/js/src/updateMetadata.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index 3f81264..f4eb163 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -87,6 +87,7 @@ export async function getUpdateMetadataInstructionPlan( payer: TransactionSigner; planner: TransactionPlanner; rpc: Rpc; + closeBuffer?: boolean; } ): Promise { if (!input.buffer && !input.data) { From aac26c83f2f3c119be2be8079469f040fbaee360 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 15:58:41 +0100 Subject: [PATCH 37/60] wip --- clients/js/src/cli/commands/write.ts | 8 ++++---- clients/js/src/cli/options.ts | 10 +++++++++- clients/js/src/cli/utils.ts | 17 ++++++++++++++--- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts index 2a2b645..1eed8fd 100644 --- a/clients/js/src/cli/commands/write.ts +++ b/clients/js/src/cli/commands/write.ts @@ -47,14 +47,14 @@ export async function doWrite( seed, }); assertValidIsCanonical(isCanonical, options); - const tempBuffer: Address | undefined = undefined; const [metadataAccount, buffer] = await Promise.all([ fetchMaybeMetadata(client.rpc, metadata), - tempBuffer - ? fetchBuffer(client.rpc, tempBuffer) + options.buffer + ? fetchBuffer(client.rpc, options.buffer) : Promise.resolve(undefined), ]); + const closeBufferOption: boolean = false; // TODO const instructionPlan = await getWriteMetadataInstructionPlan({ ...client, ...getPackedData(file, options), @@ -63,7 +63,7 @@ export async function doWrite( program, seed, format: options.format ?? getFormatFromFile(file), - closeBuffer: true, + closeBuffer: buffer ? closeBufferOption : true, buffer, metadata: metadataAccount, programData: isCanonical ? programData : undefined, diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index 790d3b8..158ce49 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -62,7 +62,8 @@ export const exportOption = new Option( } }); -export type WriteOptions = TextOption & +export type WriteOptions = BufferOption & + TextOption & UrlOption & AccountOption & AccountOffsetOption & @@ -74,6 +75,7 @@ export type WriteOptions = TextOption & export function setWriteOptions(command: CustomCommand) { command // Data sources. + .addOption(bufferOption) .addOption(textOption) .addOption(urlOption) .addOption(accountOption) @@ -85,6 +87,12 @@ export function setWriteOptions(command: CustomCommand) { .addOption(formatOption); } +export type BufferOption = { buffer?: Address }; +export const bufferOption = new Option( + '--buffer
', + 'The address of the buffer to use as source (creates a "direct" data source).' +); + export type TextOption = { text?: string }; export const textOption = new Option( '--text ', diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index bb616c7..dc87ac9 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -18,7 +18,7 @@ import { } from '@solana/kit'; import { Command } from 'commander'; import { parse as parseYaml } from 'yaml'; -import { Format } from '../generated'; +import { DataSource, Format } from '../generated'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, @@ -45,6 +45,8 @@ import { } from './options'; const LOCALHOST_URL = 'http://127.0.0.1:8899'; +const DATA_SOURCE_OPTIONS = + '`[file]`, `--buffer
`, `--text `, `--url ` or `--account
`'; export class CustomCommand extends Command { createCommand(name: string) { @@ -215,7 +217,7 @@ export function getPackedData( const assertSingleUse = () => { if (packData) { logErrorAndExit( - 'Multiple data sources provided. Use only one of: `[file]`, `--text `, `--url ` or `--account
` to provide data.' + `Multiple data sources provided. Use only one of: ${DATA_SOURCE_OPTIONS} to provide data.` ); } }; @@ -227,6 +229,15 @@ export function getPackedData( const fileContent = fs.readFileSync(file, 'utf-8'); packData = packDirectData({ content: fileContent, compression, encoding }); } + if (options.buffer) { + assertSingleUse(); + packData = { + data: new Uint8Array(0), + compression, + encoding, + dataSource: DataSource.Direct, + }; + } if (options.text) { assertSingleUse(); packData = packDirectData({ content: options.text, compression, encoding }); @@ -252,7 +263,7 @@ export function getPackedData( if (!packData) { logErrorAndExit( - 'No data provided. Use `[file]`, `--text `, `--url ` or `--account
` to provide data.' + `No data provided. Use ${DATA_SOURCE_OPTIONS} to provide data.` ); } From 8a17a51b78766edf8b9c3309cc932fcf10d14c9d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 16:24:24 +0100 Subject: [PATCH 38/60] wip --- clients/js/src/cli/commands/write.ts | 32 ++++++++------------- clients/js/src/cli/options.ts | 26 +++++++++++------ clients/js/src/cli/utils.ts | 43 +++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 30 deletions(-) diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts index 1eed8fd..213c066 100644 --- a/clients/js/src/cli/commands/write.ts +++ b/clients/js/src/cli/commands/write.ts @@ -1,7 +1,6 @@ import { Address } from '@solana/kit'; import chalk from 'chalk'; -import { fetchBuffer, fetchMaybeMetadata, Seed } from '../../generated'; -import { getPdaDetails } from '../../internals'; +import { fetchMaybeMetadata, Seed } from '../../generated'; import { getWriteMetadataInstructionPlan } from '../../writeMetadata'; import { fileArgument, programArgument, seedArgument } from '../arguments'; import { @@ -12,11 +11,10 @@ import { WriteOptions, } from '../options'; import { - assertValidIsCanonical, CustomCommand, getClient, - getFormatFromFile, - getPackedData, + getPdaDetailsForWriting, + getWriteInput, } from '../utils'; export function setWriteCommand(program: CustomCommand): void { @@ -41,32 +39,26 @@ export async function doWrite( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { programData, isCanonical, metadata } = await getPdaDetails({ - ...client, + const { metadata, programData } = await getPdaDetailsForWriting( + client, + options, program, - seed, - }); - assertValidIsCanonical(isCanonical, options); - const [metadataAccount, buffer] = await Promise.all([ + seed + ); + const [metadataAccount, writeInput] = await Promise.all([ fetchMaybeMetadata(client.rpc, metadata), - options.buffer - ? fetchBuffer(client.rpc, options.buffer) - : Promise.resolve(undefined), + getWriteInput(client, file, options), ]); - const closeBufferOption: boolean = false; // TODO const instructionPlan = await getWriteMetadataInstructionPlan({ ...client, - ...getPackedData(file, options), + ...writeInput, payer: client.payer, authority: client.authority, program, + programData, seed, - format: options.format ?? getFormatFromFile(file), - closeBuffer: buffer ? closeBufferOption : true, - buffer, metadata: metadataAccount, - programData: isCanonical ? programData : undefined, planner: client.planner, }); diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index 158ce49..5504ff6 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -62,12 +62,13 @@ export const exportOption = new Option( } }); -export type WriteOptions = BufferOption & - TextOption & +export type WriteOptions = TextOption & UrlOption & AccountOption & AccountOffsetOption & AccountLengthOption & + BufferOption & + CloseBufferOption & CompressionOption & EncodingOption & FormatOption; @@ -75,24 +76,19 @@ export type WriteOptions = BufferOption & export function setWriteOptions(command: CustomCommand) { command // Data sources. - .addOption(bufferOption) .addOption(textOption) .addOption(urlOption) .addOption(accountOption) .addOption(accountOffsetOption) .addOption(accountLengthOption) + .addOption(bufferOption) + .addOption(closeBufferOption) // Enums. .addOption(compressionOption) .addOption(encodingOption) .addOption(formatOption); } -export type BufferOption = { buffer?: Address }; -export const bufferOption = new Option( - '--buffer
', - 'The address of the buffer to use as source (creates a "direct" data source).' -); - export type TextOption = { text?: string }; export const textOption = new Option( '--text ', @@ -123,6 +119,18 @@ export const accountLengthOption = new Option( 'The length of the data on the provided account. Requires "--account" to be set.' ).default(undefined, 'the rest of the data'); +export type BufferOption = { buffer?: Address }; +export const bufferOption = new Option( + '--buffer
', + 'The address of the buffer to use as source (creates a "direct" data source).' +); + +export type CloseBufferOption = { closeBuffer: boolean }; +export const closeBufferOption = new Option( + '--close-buffer', + 'Whether to close provided `--buffer` account after using its data.' +).default(false); + export type CompressionOption = { compression: Compression }; export const compressionOption = new Option( '--compression ', diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index dc87ac9..2c3ad2d 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -3,6 +3,8 @@ import os from 'os'; import path from 'path'; import { + Account, + Address, address, Commitment, createKeyPairSignerFromBytes, @@ -18,7 +20,7 @@ import { } from '@solana/kit'; import { Command } from 'commander'; import { parse as parseYaml } from 'yaml'; -import { DataSource, Format } from '../generated'; +import { Buffer, DataSource, fetchBuffer, Format, Seed } from '../generated'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, @@ -27,6 +29,7 @@ import { TransactionPlanner, TransactionPlanResult, } from '../instructionPlans'; +import { getPdaDetails, PdaDetails } from '../internals'; import { packDirectData, PackedData, @@ -208,6 +211,44 @@ export function getFormatFromFile(file: string | undefined): Format { } } +export async function getPdaDetailsForWriting( + client: Client, + options: NonCanonicalWriteOption, + program: Address, + seed: Seed +): Promise { + const details = await getPdaDetails({ ...client, program, seed }); + assertValidIsCanonical(details.isCanonical, options); + const isCanonical = !options.nonCanonical; + return { + programData: isCanonical ? details.programData : undefined, + metadata: details.metadata, + isCanonical, + }; +} + +export async function getWriteInput( + client: Client, + file: string | undefined, + options: WriteOptions +): Promise< + PackedData & { + buffer?: Account; + format: Format; + closeBuffer?: boolean; + } +> { + const buffer = options.buffer + ? await fetchBuffer(client.rpc, options.buffer) + : undefined; + return { + ...getPackedData(file, options), + format: options.format ?? getFormatFromFile(file), + closeBuffer: options.buffer ? options.closeBuffer : true, + buffer, + }; +} + export function getPackedData( file: string | undefined, options: WriteOptions From 0a88223a47a01770a84fd9b8afec0228d34ad165 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 16:31:25 +0100 Subject: [PATCH 39/60] wip --- clients/js/src/cli/commands/set-authority.ts | 23 ++++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts index 48efeb7..0537bfe 100644 --- a/clients/js/src/cli/commands/set-authority.ts +++ b/clients/js/src/cli/commands/set-authority.ts @@ -6,6 +6,7 @@ import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; import { GlobalOptions } from '../options'; import { CustomCommand, getClient } from '../utils'; +import { logErrorAndExit } from '../logs'; export function setSetAuthorityCommand(program: CustomCommand): void { program @@ -15,33 +16,41 @@ export function setSetAuthorityCommand(program: CustomCommand): void { ) .addArgument(seedArgument) .addArgument(programArgument) - .argument('', 'The new authority to set', address) // TODO: Make it a mandatory option to be explicit. + .requiredOption( + '--new-authority ', + 'The new authority to set', + (value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid new authority address: "${value}"`); + } + } + ) .action(doSetAuthority); } -type Options = {}; +type Options = { newAuthority: Address }; async function doSetAuthority( seed: Seed, program: Address, - newAuthority: Address, _: Options, cmd: CustomCommand ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); const { metadata, programData } = await getPdaDetails({ - rpc: client.rpc, + ...client, program, - authority: client.authority, seed, }); await client.planAndExecute( - `Set additional authority on metadata account to ${chalk.bold(newAuthority)}`, + `Set additional authority on metadata account to ${chalk.bold(options.newAuthority)}`, sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, authority: client.authority, - newAuthority: address(newAuthority), + newAuthority: options.newAuthority, program, programData, }), From dd1c0274dbc103afa6e37b9f3d3a9a8fa55fe7d9 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 16:33:37 +0100 Subject: [PATCH 40/60] wip --- clients/js/src/cli/commands/close.ts | 24 +++++--------------- clients/js/src/cli/commands/set-immutable.ts | 24 +++++--------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/clients/js/src/cli/commands/close.ts b/clients/js/src/cli/commands/close.ts index 7b6c783..c358d58 100644 --- a/clients/js/src/cli/commands/close.ts +++ b/clients/js/src/cli/commands/close.ts @@ -1,16 +1,13 @@ import { Address } from '@solana/kit'; import { getCloseInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; -import { getPdaDetails } from '../../internals'; -import { getProgramAuthority } from '../../utils'; import { programArgument, seedArgument } from '../arguments'; -import { logErrorAndExit } from '../logs'; import { GlobalOptions, NonCanonicalWriteOption, nonCanonicalWriteOption, } from '../options'; -import { CustomCommand, getClient } from '../utils'; +import { CustomCommand, getClient, getPdaDetailsForWriting } from '../utils'; export function setCloseCommand(program: CustomCommand): void { program @@ -31,21 +28,12 @@ async function doClose( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { authority: programAuthority } = await getProgramAuthority( - client.rpc, - program - ); - if (!options.nonCanonical && client.authority.address !== programAuthority) { - logErrorAndExit( - 'You must be the program authority to close a canonical metadata account. Use `--non-canonical` option to close as a third party.' - ); - } - const { metadata, programData } = await getPdaDetails({ - rpc: client.rpc, + const { metadata, programData } = await getPdaDetailsForWriting( + client, + options, program, - authority: client.authority, - seed, - }); + seed + ); await client.planAndExecute( 'Close metadata account and recover rent', sequentialInstructionPlan([ diff --git a/clients/js/src/cli/commands/set-immutable.ts b/clients/js/src/cli/commands/set-immutable.ts index ca3a750..2bc4b2a 100644 --- a/clients/js/src/cli/commands/set-immutable.ts +++ b/clients/js/src/cli/commands/set-immutable.ts @@ -1,16 +1,13 @@ import { Address } from '@solana/kit'; import { getSetImmutableInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; -import { getPdaDetails } from '../../internals'; -import { getProgramAuthority } from '../../utils'; import { programArgument, seedArgument } from '../arguments'; -import { logErrorAndExit } from '../logs'; import { GlobalOptions, NonCanonicalWriteOption, nonCanonicalWriteOption, } from '../options'; -import { CustomCommand, getClient } from '../utils'; +import { CustomCommand, getClient, getPdaDetailsForWriting } from '../utils'; export function setSetImmutableCommand(program: CustomCommand): void { program @@ -33,21 +30,12 @@ async function doSetImmutable( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { authority: programAuthority } = await getProgramAuthority( - client.rpc, - program - ); - if (!options.nonCanonical && client.authority.address !== programAuthority) { - logErrorAndExit( - 'You must be the program authority to update a canonical metadata account. Use the `--non-canonical` option to update as a third party.' - ); - } - const { metadata, programData } = await getPdaDetails({ - rpc: client.rpc, + const { metadata, programData } = await getPdaDetailsForWriting( + client, + options, program, - authority: client.authority, - seed, - }); + seed + ); await client.planAndExecute( 'Make metadata account immutable', sequentialInstructionPlan([ From 57f55da746d3127145a52fce5771f621f505eb95 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 16:42:31 +0100 Subject: [PATCH 41/60] wip --- clients/js/src/cli/commands/create.ts | 77 +++++++++++++++++++++++++++ clients/js/src/cli/commands/index.ts | 4 ++ clients/js/src/cli/commands/update.ts | 76 ++++++++++++++++++++++++++ clients/js/src/cli/commands/write.ts | 2 +- 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 clients/js/src/cli/commands/create.ts create mode 100644 clients/js/src/cli/commands/update.ts diff --git a/clients/js/src/cli/commands/create.ts b/clients/js/src/cli/commands/create.ts new file mode 100644 index 0000000..00af6db --- /dev/null +++ b/clients/js/src/cli/commands/create.ts @@ -0,0 +1,77 @@ +import { Address } from '@solana/kit'; +import chalk from 'chalk'; +import { getCreateMetadataInstructionPlan } from '../../createMetadata'; +import { fetchMaybeMetadata, Seed } from '../../generated'; +import { fileArgument, programArgument, seedArgument } from '../arguments'; +import { logErrorAndExit } from '../logs'; +import { + GlobalOptions, + nonCanonicalWriteOption, + NonCanonicalWriteOption, + setWriteOptions, + WriteOptions, +} from '../options'; +import { + CustomCommand, + getClient, + getPdaDetailsForWriting, + getWriteInput, +} from '../utils'; + +export function setCreateCommand(program: CustomCommand): void { + program + .command('create') + .description('Create a metadata account for a given program.') + .addArgument(seedArgument) + .addArgument(programArgument) + .addArgument(fileArgument) + .tap(setWriteOptions) + .addOption(nonCanonicalWriteOption) + .action(doCreate); +} + +type Options = WriteOptions & NonCanonicalWriteOption; +export async function doCreate( + seed: Seed, + program: Address, + file: string | undefined, + _: Options, + cmd: CustomCommand +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const { metadata, programData } = await getPdaDetailsForWriting( + client, + options, + program, + seed + ); + const [metadataAccount, writeInput] = await Promise.all([ + fetchMaybeMetadata(client.rpc, metadata), + getWriteInput(client, file, options), + ]); + + if (metadataAccount.exists) { + // TODO: show derivation seeds. + logErrorAndExit( + `Metadata account ${chalk.bold(metadataAccount.address)} already exists.` + ); + } + + const instructionPlan = await getCreateMetadataInstructionPlan({ + ...client, + ...writeInput, + payer: client.payer, + authority: client.authority, + program, + programData, + seed, + metadata, + planner: client.planner, + }); + + await client.planAndExecute( + `Create metadata for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"`, + instructionPlan + ); +} diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index 6f640a7..e7134f9 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -1,15 +1,19 @@ import { CustomCommand } from '../utils'; import { setCloseCommand } from './close'; +import { setCreateCommand } from './create'; import { setFetchCommand } from './fetch'; import { setRemoveAuthorityCommand } from './remove-authority'; import { setSetAuthorityCommand } from './set-authority'; import { setSetImmutableCommand } from './set-immutable'; +import { setUpdateCommand } from './update'; import { setWriteCommand } from './write'; export function setCommands(program: CustomCommand): void { program // Metadata commands. .tap(setWriteCommand) + .tap(setCreateCommand) + .tap(setUpdateCommand) .tap(setFetchCommand) .tap(setSetAuthorityCommand) .tap(setRemoveAuthorityCommand) diff --git a/clients/js/src/cli/commands/update.ts b/clients/js/src/cli/commands/update.ts new file mode 100644 index 0000000..22d76a1 --- /dev/null +++ b/clients/js/src/cli/commands/update.ts @@ -0,0 +1,76 @@ +import { Address } from '@solana/kit'; +import chalk from 'chalk'; +import { fetchMaybeMetadata, Seed } from '../../generated'; +import { fileArgument, programArgument, seedArgument } from '../arguments'; +import { + GlobalOptions, + nonCanonicalWriteOption, + NonCanonicalWriteOption, + setWriteOptions, + WriteOptions, +} from '../options'; +import { + CustomCommand, + getClient, + getPdaDetailsForWriting, + getWriteInput, +} from '../utils'; +import { logErrorAndExit } from '../logs'; +import { getUpdateMetadataInstructionPlan } from '../../updateMetadata'; + +export function setUpdateCommand(program: CustomCommand): void { + program + .command('update') + .description('Update a metadata account for a given program.') + .addArgument(seedArgument) + .addArgument(programArgument) + .addArgument(fileArgument) + .tap(setWriteOptions) + .addOption(nonCanonicalWriteOption) + .action(doWrite); +} + +type Options = WriteOptions & NonCanonicalWriteOption; +export async function doWrite( + seed: Seed, + program: Address, + file: string | undefined, + _: Options, + cmd: CustomCommand +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const { metadata, programData } = await getPdaDetailsForWriting( + client, + options, + program, + seed + ); + const [metadataAccount, writeInput] = await Promise.all([ + fetchMaybeMetadata(client.rpc, metadata), + getWriteInput(client, file, options), + ]); + + if (!metadataAccount.exists) { + // TODO: show derivation seeds. + logErrorAndExit( + `Metadata account ${chalk.bold(metadataAccount.address)} does not exist.` + ); + } + + const instructionPlan = await getUpdateMetadataInstructionPlan({ + ...client, + ...writeInput, + payer: client.payer, + authority: client.authority, + program, + programData, + metadata: metadataAccount, + planner: client.planner, + }); + + await client.planAndExecute( + `Update metadata for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"`, + instructionPlan + ); +} diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts index 213c066..9f80db0 100644 --- a/clients/js/src/cli/commands/write.ts +++ b/clients/js/src/cli/commands/write.ts @@ -63,7 +63,7 @@ export async function doWrite( }); await client.planAndExecute( - `Upload metadata for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"`, + `Write metadata for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"`, instructionPlan ); } From b18f6662dbc038e012e420b49a408e7287a1a93a Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 17:52:48 +0100 Subject: [PATCH 42/60] wip --- clients/js/src/createBuffer.ts | 85 ++++++++++++++++++++++++++++++++ clients/js/src/updateMetadata.ts | 41 ++++----------- 2 files changed, 94 insertions(+), 32 deletions(-) create mode 100644 clients/js/src/createBuffer.ts diff --git a/clients/js/src/createBuffer.ts b/clients/js/src/createBuffer.ts new file mode 100644 index 0000000..4391962 --- /dev/null +++ b/clients/js/src/createBuffer.ts @@ -0,0 +1,85 @@ +import { getCreateAccountInstruction } from '@solana-program/system'; +import { + Account, + Lamports, + ReadonlyUint8Array, + TransactionSigner, +} from '@solana/kit'; +import { + Buffer, + getAllocateInstruction, + getCloseInstruction, + getSetAuthorityInstruction, + getWriteInstruction, + PROGRAM_METADATA_PROGRAM_ADDRESS, +} from './generated'; +import { + parallelInstructionPlan, + sequentialInstructionPlan, +} from './instructionPlans'; +import { getAccountSize, getWriteInstructionPlan } from './utils'; + +export function getCreateBufferInstructionPlan(input: { + newBuffer: TransactionSigner; + authority: TransactionSigner; + payer: TransactionSigner; + sourceBuffer?: Account; + closeBuffer?: boolean; + data?: ReadonlyUint8Array; + rent: Lamports; +}) { + if (!input.data && !input.sourceBuffer) { + throw new Error( + 'Either `data` or `sourceBuffer` must be provided to create a buffer.' + ); + } + + const data = (input.data ?? + input.sourceBuffer?.data.data) as ReadonlyUint8Array; + + return sequentialInstructionPlan([ + getCreateAccountInstruction({ + payer: input.payer, + newAccount: input.newBuffer, + lamports: input.rent, + space: getAccountSize(data.length), + programAddress: PROGRAM_METADATA_PROGRAM_ADDRESS, + }), + getAllocateInstruction({ + buffer: input.newBuffer.address, + authority: input.newBuffer, + }), + getSetAuthorityInstruction({ + account: input.newBuffer.address, + authority: input.newBuffer, + newAuthority: input.authority.address, + }), + ...(input.sourceBuffer + ? [ + getWriteInstruction({ + buffer: input.newBuffer.address, + authority: input.authority, + sourceBuffer: input.sourceBuffer.address, + offset: 0, + }), + ] + : [ + parallelInstructionPlan([ + getWriteInstructionPlan({ + buffer: input.newBuffer.address, + authority: input.authority, + data, + }), + ]), + ]), + ...(input.closeBuffer && input.sourceBuffer + ? [ + getCloseInstruction({ + account: input.sourceBuffer.address, + authority: input.authority, + destination: input.payer.address, + }), + ] + : []), + ]); +} diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index f4eb163..ca7253c 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -1,7 +1,4 @@ -import { - getCreateAccountInstruction, - getTransferSolInstruction, -} from '@solana-program/system'; +import { getTransferSolInstruction } from '@solana-program/system'; import { Account, Address, @@ -14,24 +11,21 @@ import { Rpc, TransactionSigner, } from '@solana/kit'; +import { getCreateBufferInstructionPlan } from './createBuffer'; import { Buffer, fetchBuffer, fetchMetadata, - getAllocateInstruction, getCloseInstruction, - getSetAuthorityInstruction, getSetDataInstruction, getTrimInstruction, Metadata, - PROGRAM_METADATA_PROGRAM_ADDRESS, SetDataInput, } from './generated'; import { createDefaultTransactionPlanExecutor, InstructionPlan, isValidInstructionPlan, - parallelInstructionPlan, sequentialInstructionPlan, TransactionPlanner, } from './instructionPlans'; @@ -43,7 +37,6 @@ import { import { getAccountSize, getExtendInstructionPlan, - getWriteInstructionPlan, MetadataInput, MetadataResponse, } from './utils'; @@ -199,22 +192,6 @@ export function getUpdateMetadataInstructionPlanUsingNewBuffer( }), ] : []), - getCreateAccountInstruction({ - payer: input.payer, - newAccount: input.buffer, - lamports: input.fullRent, - space: getAccountSize(input.data.length), - programAddress: PROGRAM_METADATA_PROGRAM_ADDRESS, - }), - getAllocateInstruction({ - buffer: input.buffer.address, - authority: input.buffer, - }), - getSetAuthorityInstruction({ - account: input.buffer.address, - authority: input.buffer, - newAuthority: input.authority.address, - }), ...(input.sizeDifference > REALLOC_LIMIT ? [ getExtendInstructionPlan({ @@ -226,13 +203,13 @@ export function getUpdateMetadataInstructionPlanUsingNewBuffer( }), ] : []), - parallelInstructionPlan([ - getWriteInstructionPlan({ - buffer: input.buffer.address, - authority: input.authority, - data: input.data, - }), - ]), + getCreateBufferInstructionPlan({ + newBuffer: input.buffer, + authority: input.authority, + payer: input.payer, + rent: input.fullRent, + data: input.data, + }), getSetDataInstruction({ ...input, buffer: input.buffer.address, From bc8633d29b77439041eff188fda814f5065efeea Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 19:14:37 +0100 Subject: [PATCH 43/60] wip --- clients/js/src/createBuffer.ts | 4 +- clients/js/src/updateBuffer.ts | 97 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 clients/js/src/updateBuffer.ts diff --git a/clients/js/src/createBuffer.ts b/clients/js/src/createBuffer.ts index 4391962..4859de5 100644 --- a/clients/js/src/createBuffer.ts +++ b/clients/js/src/createBuffer.ts @@ -24,7 +24,7 @@ export function getCreateBufferInstructionPlan(input: { authority: TransactionSigner; payer: TransactionSigner; sourceBuffer?: Account; - closeBuffer?: boolean; + closeSourceBuffer?: boolean; data?: ReadonlyUint8Array; rent: Lamports; }) { @@ -72,7 +72,7 @@ export function getCreateBufferInstructionPlan(input: { }), ]), ]), - ...(input.closeBuffer && input.sourceBuffer + ...(input.closeSourceBuffer && input.sourceBuffer ? [ getCloseInstruction({ account: input.sourceBuffer.address, diff --git a/clients/js/src/updateBuffer.ts b/clients/js/src/updateBuffer.ts new file mode 100644 index 0000000..59528ef --- /dev/null +++ b/clients/js/src/updateBuffer.ts @@ -0,0 +1,97 @@ +import { getTransferSolInstruction } from '@solana-program/system'; +import { + Account, + Address, + Lamports, + ReadonlyUint8Array, + TransactionSigner, +} from '@solana/kit'; +import { + Buffer, + getCloseInstruction, + getTrimInstruction, + getWriteInstruction, +} from './generated'; +import { + parallelInstructionPlan, + sequentialInstructionPlan, +} from './instructionPlans'; +import { REALLOC_LIMIT } from './internals'; +import { getExtendInstructionPlan, getWriteInstructionPlan } from './utils'; + +export function getUpdateBufferInstructionPlan(input: { + buffer: Address; + authority: TransactionSigner; + payer: TransactionSigner; + extraRent: Lamports; + sizeDifference: number | bigint; + sourceBuffer?: Account; + closeSourceBuffer?: boolean; + data?: ReadonlyUint8Array; +}) { + if (!input.data && !input.sourceBuffer) { + throw new Error( + 'Either `data` or `sourceBuffer` must be provided to update a buffer.' + ); + } + + const data = (input.data ?? + input.sourceBuffer?.data.data) as ReadonlyUint8Array; + + return sequentialInstructionPlan([ + ...(input.sizeDifference > 0 + ? [ + getTransferSolInstruction({ + source: input.payer, + destination: input.buffer, + amount: input.extraRent, + }), + ] + : []), + ...(input.sizeDifference > REALLOC_LIMIT + ? [ + getExtendInstructionPlan({ + account: input.buffer, + authority: input.authority, + extraLength: Number(input.sizeDifference), + }), + ] + : []), + ...(input.sourceBuffer + ? [ + getWriteInstruction({ + buffer: input.buffer, + authority: input.authority, + sourceBuffer: input.sourceBuffer.address, + offset: 0, + }), + ] + : [ + parallelInstructionPlan([ + getWriteInstructionPlan({ + buffer: input.buffer, + authority: input.authority, + data, + }), + ]), + ]), + ...(input.sizeDifference < 0 + ? [ + getTrimInstruction({ + account: input.buffer, + authority: input.authority, + destination: input.payer.address, + }), + ] + : []), + ...(input.sourceBuffer && input.closeSourceBuffer + ? [ + getCloseInstruction({ + account: input.sourceBuffer.address, + authority: input.authority, + destination: input.payer.address, + }), + ] + : []), + ]); +} From 61d38ab3b63f4ff713ae2485407386118ddd5953 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 19:28:53 +0100 Subject: [PATCH 44/60] wip --- clients/js/src/cli/commands/create-buffer.ts | 49 ++++++++++++++++++++ clients/js/src/cli/commands/index.ts | 2 + clients/js/src/createBuffer.ts | 4 +- clients/js/src/createMetadata.ts | 4 +- clients/js/src/updateBuffer.ts | 4 +- clients/js/src/updateMetadata.ts | 4 +- 6 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 clients/js/src/cli/commands/create-buffer.ts diff --git a/clients/js/src/cli/commands/create-buffer.ts b/clients/js/src/cli/commands/create-buffer.ts new file mode 100644 index 0000000..cd297df --- /dev/null +++ b/clients/js/src/cli/commands/create-buffer.ts @@ -0,0 +1,49 @@ +import { generateKeyPairSigner } from '@solana/kit'; +import chalk from 'chalk'; +import { getCreateBufferInstructionPlan } from '../../createBuffer'; +import { getAccountSize } from '../../utils'; +import { fileArgument } from '../arguments'; +import { GlobalOptions, setWriteOptions, WriteOptions } from '../options'; +import { CustomCommand, getClient, getWriteInput } from '../utils'; + +export function setCreateBufferCommand(program: CustomCommand): void { + program + .command('create-buffer') + .description('Create a buffer account to use on metadata account later on.') + .addArgument(fileArgument) + .tap(setWriteOptions) + .action(doCreateBuffer); +} + +type Options = WriteOptions; +export async function doCreateBuffer( + file: string | undefined, + _: Options, + cmd: CustomCommand +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const [buffer, writeInput] = await Promise.all([ + generateKeyPairSigner(), + getWriteInput(client, file, options), + ]); + const data = writeInput.buffer?.data.data ?? writeInput.data; + const rent = await client.rpc + .getMinimumBalanceForRentExemption(getAccountSize(data.length)) + .send(); + + const instructionPlan = getCreateBufferInstructionPlan({ + newBuffer: buffer, + authority: client.authority, + payer: client.payer, + rent, + sourceBuffer: writeInput.buffer, + closeSourceBuffer: writeInput.closeBuffer, + data, + }); + + await client.planAndExecute( + `Create buffer ${chalk.bold(buffer.address)}`, + instructionPlan + ); +} diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index e7134f9..8e28c97 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -1,6 +1,7 @@ import { CustomCommand } from '../utils'; import { setCloseCommand } from './close'; import { setCreateCommand } from './create'; +import { setCreateBufferCommand } from './create-buffer'; import { setFetchCommand } from './fetch'; import { setRemoveAuthorityCommand } from './remove-authority'; import { setSetAuthorityCommand } from './set-authority'; @@ -21,6 +22,7 @@ export function setCommands(program: CustomCommand): void { .tap(setCloseCommand) // TODO: list: List all metadata accounts owned by an authority. // Buffer commands. + .tap(setCreateBufferCommand) // TODO: list-buffers: List all buffer accounts owned by an authority. .tap(() => {}); } diff --git a/clients/js/src/createBuffer.ts b/clients/js/src/createBuffer.ts index 4859de5..45bfeb8 100644 --- a/clients/js/src/createBuffer.ts +++ b/clients/js/src/createBuffer.ts @@ -34,8 +34,8 @@ export function getCreateBufferInstructionPlan(input: { ); } - const data = (input.data ?? - input.sourceBuffer?.data.data) as ReadonlyUint8Array; + const data = (input.sourceBuffer?.data.data ?? + input.data) as ReadonlyUint8Array; return sequentialInstructionPlan([ getCreateAccountInstruction({ diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 9c36385..8adc732 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -84,9 +84,7 @@ export async function getCreateMetadataInstructionPlan( ); } - const data = ( - input.buffer ? input.buffer.data.data : input.data - ) as ReadonlyUint8Array; + const data = (input.buffer?.data.data ?? input.data) as ReadonlyUint8Array; const rent = await input.rpc .getMinimumBalanceForRentExemption(getAccountSize(data.length)) .send(); diff --git a/clients/js/src/updateBuffer.ts b/clients/js/src/updateBuffer.ts index 59528ef..af628ac 100644 --- a/clients/js/src/updateBuffer.ts +++ b/clients/js/src/updateBuffer.ts @@ -35,8 +35,8 @@ export function getUpdateBufferInstructionPlan(input: { ); } - const data = (input.data ?? - input.sourceBuffer?.data.data) as ReadonlyUint8Array; + const data = (input.sourceBuffer?.data.data ?? + input.data) as ReadonlyUint8Array; return sequentialInstructionPlan([ ...(input.sizeDifference > 0 diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index ca7253c..b09a635 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -92,9 +92,7 @@ export async function getUpdateMetadataInstructionPlan( throw new Error('Metadata account is immutable'); } - const data = ( - input.buffer ? input.buffer.data.data : input.data - ) as ReadonlyUint8Array; + const data = (input.buffer?.data.data ?? input.data) as ReadonlyUint8Array; const fullRentPromise = input.rpc .getMinimumBalanceForRentExemption(getAccountSize(data.length)) .send(); From f8765c9dc94442183a1a59f886a0283a930ccaec Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 19:40:02 +0100 Subject: [PATCH 45/60] wip --- clients/js/src/cli/commands/index.ts | 8 ++- clients/js/src/cli/commands/update-buffer.ts | 73 ++++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 clients/js/src/cli/commands/update-buffer.ts diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index 8e28c97..fb3b033 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -7,11 +7,13 @@ import { setRemoveAuthorityCommand } from './remove-authority'; import { setSetAuthorityCommand } from './set-authority'; import { setSetImmutableCommand } from './set-immutable'; import { setUpdateCommand } from './update'; +import { setUpdateBufferCommand } from './update-buffer'; import { setWriteCommand } from './write'; export function setCommands(program: CustomCommand): void { program // Metadata commands. + // TODO: list: List all metadata accounts owned by an authority. .tap(setWriteCommand) .tap(setCreateCommand) .tap(setUpdateCommand) @@ -20,9 +22,9 @@ export function setCommands(program: CustomCommand): void { .tap(setRemoveAuthorityCommand) .tap(setSetImmutableCommand) .tap(setCloseCommand) - // TODO: list: List all metadata accounts owned by an authority. + // Buffer commands. - .tap(setCreateBufferCommand) // TODO: list-buffers: List all buffer accounts owned by an authority. - .tap(() => {}); + .tap(setCreateBufferCommand) + .tap(setUpdateBufferCommand); } diff --git a/clients/js/src/cli/commands/update-buffer.ts b/clients/js/src/cli/commands/update-buffer.ts new file mode 100644 index 0000000..e9e9095 --- /dev/null +++ b/clients/js/src/cli/commands/update-buffer.ts @@ -0,0 +1,73 @@ +import { address, Address, lamports } from '@solana/kit'; +import chalk from 'chalk'; +import { fetchMaybeBuffer } from '../../generated'; +import { getUpdateBufferInstructionPlan } from '../../updateBuffer'; +import { fileArgument } from '../arguments'; +import { logErrorAndExit } from '../logs'; +import { GlobalOptions, setWriteOptions, WriteOptions } from '../options'; +import { CustomCommand, getClient, getWriteInput } from '../utils'; + +export function setUpdateBufferCommand(program: CustomCommand): void { + program + .command('update-buffer') + .description('Update an existing buffer account.') + .argument( + '', + 'The address of the buffer account to update.', + (value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid buffer address: "${value}"`); + } + } + ) + .addArgument(fileArgument) + .tap(setWriteOptions) + .action(doUpdateBuffer); +} + +type Options = WriteOptions; +export async function doUpdateBuffer( + buffer: Address, + file: string | undefined, + _: Options, + cmd: CustomCommand +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const [writeInput, bufferAccount] = await Promise.all([ + getWriteInput(client, file, options), + fetchMaybeBuffer(client.rpc, buffer), + ]); + + if (!bufferAccount.exists) { + logErrorAndExit(`Buffer account not found: "${buffer}"`); + } + + const currentData = bufferAccount.data.data; + const newData = writeInput.buffer?.data.data ?? writeInput.data; + const sizeDifference = newData.length - currentData.length; + const extraRent = + sizeDifference > 0 + ? await client.rpc + .getMinimumBalanceForRentExemption(BigInt(sizeDifference)) + .send() + : lamports(0n); + + const instructionPlan = getUpdateBufferInstructionPlan({ + buffer, + authority: client.authority, + payer: client.payer, + extraRent, + sizeDifference, + sourceBuffer: writeInput.buffer, + closeSourceBuffer: writeInput.closeBuffer, + data: newData, + }); + + await client.planAndExecute( + `Update buffer ${chalk.bold(buffer)}`, + instructionPlan + ); +} From 2a61e4d875a4753cfdc897ba98e5a316c2503443 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 19:47:00 +0100 Subject: [PATCH 46/60] wip --- clients/js/src/cli/commands/close-buffer.ts | 66 +++++++++++++++++++++ clients/js/src/cli/commands/index.ts | 4 +- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 clients/js/src/cli/commands/close-buffer.ts diff --git a/clients/js/src/cli/commands/close-buffer.ts b/clients/js/src/cli/commands/close-buffer.ts new file mode 100644 index 0000000..b57df2d --- /dev/null +++ b/clients/js/src/cli/commands/close-buffer.ts @@ -0,0 +1,66 @@ +import { address, Address } from '@solana/kit'; +import chalk from 'chalk'; +import { fetchMaybeBuffer, getCloseInstruction } from '../../generated'; +import { sequentialInstructionPlan } from '../../instructionPlans'; +import { logErrorAndExit } from '../logs'; +import { GlobalOptions } from '../options'; +import { CustomCommand, getClient } from '../utils'; + +export function setCloseBufferCommand(program: CustomCommand): void { + program + .command('close-buffer') + .description('Close an existing buffer account.') + .argument( + '', + 'The address of the buffer account to close.', + (value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid buffer address: "${value}"`); + } + } + ) + .option( + '--recipient ', + 'Address receiving the storage fees for the closed account.', + (value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid recipient address: "${value}"`); + } + } + ) + .action(doUpdateBuffer); +} + +type Options = { recipient?: Address }; +export async function doUpdateBuffer( + buffer: Address, + _: Options, + cmd: CustomCommand +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + const [bufferAccount] = await Promise.all([ + fetchMaybeBuffer(client.rpc, buffer), + ]); + + if (!bufferAccount.exists) { + logErrorAndExit(`Buffer account not found: "${buffer}"`); + } + + const instructionPlan = sequentialInstructionPlan([ + getCloseInstruction({ + account: buffer, + authority: client.authority, + destination: options.recipient ?? client.payer.address, + }), + ]); + + await client.planAndExecute( + `Close buffer ${chalk.bold(buffer)}`, + instructionPlan + ); +} diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index fb3b033..8683211 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -1,5 +1,6 @@ import { CustomCommand } from '../utils'; import { setCloseCommand } from './close'; +import { setCloseBufferCommand } from './close-buffer'; import { setCreateCommand } from './create'; import { setCreateBufferCommand } from './create-buffer'; import { setFetchCommand } from './fetch'; @@ -26,5 +27,6 @@ export function setCommands(program: CustomCommand): void { // Buffer commands. // TODO: list-buffers: List all buffer accounts owned by an authority. .tap(setCreateBufferCommand) - .tap(setUpdateBufferCommand); + .tap(setUpdateBufferCommand) + .tap(setCloseBufferCommand); } From 9a16e2061bc343aa2d980b3a65226ee8fa8a02ab Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 22:05:33 +0100 Subject: [PATCH 47/60] wip --- clients/js/src/cli/commands/close-buffer.ts | 8 +-- clients/js/src/cli/commands/fetch-buffer.ts | 70 +++++++++++++++++++++ clients/js/src/cli/commands/fetch.ts | 32 +++++++--- clients/js/src/cli/commands/index.ts | 6 +- 4 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 clients/js/src/cli/commands/fetch-buffer.ts diff --git a/clients/js/src/cli/commands/close-buffer.ts b/clients/js/src/cli/commands/close-buffer.ts index b57df2d..239c609 100644 --- a/clients/js/src/cli/commands/close-buffer.ts +++ b/clients/js/src/cli/commands/close-buffer.ts @@ -32,20 +32,18 @@ export function setCloseBufferCommand(program: CustomCommand): void { } } ) - .action(doUpdateBuffer); + .action(doCloseBuffer); } type Options = { recipient?: Address }; -export async function doUpdateBuffer( +export async function doCloseBuffer( buffer: Address, _: Options, cmd: CustomCommand ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const [bufferAccount] = await Promise.all([ - fetchMaybeBuffer(client.rpc, buffer), - ]); + const bufferAccount = await fetchMaybeBuffer(client.rpc, buffer); if (!bufferAccount.exists) { logErrorAndExit(`Buffer account not found: "${buffer}"`); diff --git a/clients/js/src/cli/commands/fetch-buffer.ts b/clients/js/src/cli/commands/fetch-buffer.ts new file mode 100644 index 0000000..52f19de --- /dev/null +++ b/clients/js/src/cli/commands/fetch-buffer.ts @@ -0,0 +1,70 @@ +import { address, Address } from '@solana/kit'; +import chalk from 'chalk'; +import { Option } from 'commander'; +import { Compression, Encoding, fetchMaybeBuffer } from '../../generated'; +import { unpackDirectData } from '../../packData'; +import { logErrorAndExit, logSuccess } from '../logs'; +import { + compressionOption, + CompressionOption, + encodingOption, + EncodingOption, + GlobalOptions, + outputOption, + OutputOption, +} from '../options'; +import { CustomCommand, getReadonlyClient, writeFile } from '../utils'; + +export function setFetchBufferCommand(program: CustomCommand): void { + program + .command('fetch-buffer') + .description('Fetch the content of a buffer account.') + .argument( + '', + 'The address of the buffer account to fetch.', + (value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid buffer address: "${value}"`); + } + } + ) + .addOption(outputOption) + .addOption(compressionOption) + .addOption(encodingOption) + .addOption( + new Option('--raw', 'Output raw data in hexadecimal format.') + .default(false) + .implies({ compression: Compression.None, encoding: Encoding.None }) + ) + .action(doFetchBuffer); +} + +type Options = OutputOption & CompressionOption & EncodingOption; +export async function doFetchBuffer( + buffer: Address, + _: Options, + cmd: CustomCommand +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = getReadonlyClient(options); + const bufferAccount = await fetchMaybeBuffer(client.rpc, buffer); + + if (!bufferAccount.exists) { + logErrorAndExit(`Buffer account not found: "${buffer}"`); + } + + const content = unpackDirectData({ + data: bufferAccount.data.data, + encoding: options.encoding, + compression: options.compression, + }); + + if (options.output) { + writeFile(options.output, content); + logSuccess(`Buffer content saved to ${chalk.bold(options.output)}`); + } else { + console.log(content); + } +} diff --git a/clients/js/src/cli/commands/fetch.ts b/clients/js/src/cli/commands/fetch.ts index c7518df..8482f04 100644 --- a/clients/js/src/cli/commands/fetch.ts +++ b/clients/js/src/cli/commands/fetch.ts @@ -1,7 +1,13 @@ import { Address, isSolanaError } from '@solana/kit'; import chalk from 'chalk'; -import { fetchMetadataContent } from '../../fetchMetadataContent'; -import { Seed } from '../../generated'; +import { Option } from 'commander'; +import { + Compression, + Encoding, + fetchMetadataFromSeeds, + Seed, +} from '../../generated'; +import { unpackAndFetchData, unpackDirectData } from '../../packData'; import { programArgument, seedArgument } from '../arguments'; import { logErrorAndExit, logSuccess } from '../logs'; import { @@ -26,10 +32,15 @@ export function setFetchCommand(program: CustomCommand): void { .addArgument(programArgument) .addOption(nonCanonicalReadOption) .addOption(outputOption) + .addOption( + new Option('--raw', 'Output raw data in hexadecimal format.').default( + false + ) + ) .action(doFetch); } -type Options = NonCanonicalReadOption & OutputOption; +type Options = NonCanonicalReadOption & OutputOption & { raw: boolean }; async function doFetch( seed: Seed, program: Address, @@ -45,12 +56,19 @@ async function doFetch( ? options.nonCanonical : undefined; try { - const content = await fetchMetadataContent( - client.rpc, + const metadataAccount = await fetchMetadataFromSeeds(client.rpc, { program, + authority: authority ?? null, seed, - authority - ); + }); + const content = options.raw + ? unpackDirectData({ + encoding: Encoding.None, + data: metadataAccount.data.data, + compression: Compression.None, + }) + : await unpackAndFetchData({ rpc: client.rpc, ...metadataAccount.data }); + if (options.output) { writeFile(options.output, content); logSuccess(`Metadata content saved to ${chalk.bold(options.output)}`); diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index 8683211..5300438 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -4,6 +4,7 @@ import { setCloseBufferCommand } from './close-buffer'; import { setCreateCommand } from './create'; import { setCreateBufferCommand } from './create-buffer'; import { setFetchCommand } from './fetch'; +import { setFetchBufferCommand } from './fetch-buffer'; import { setRemoveAuthorityCommand } from './remove-authority'; import { setSetAuthorityCommand } from './set-authority'; import { setSetImmutableCommand } from './set-immutable'; @@ -14,10 +15,10 @@ import { setWriteCommand } from './write'; export function setCommands(program: CustomCommand): void { program // Metadata commands. - // TODO: list: List all metadata accounts owned by an authority. .tap(setWriteCommand) .tap(setCreateCommand) .tap(setUpdateCommand) + // TODO: list: List all metadata accounts owned by an authority. .tap(setFetchCommand) .tap(setSetAuthorityCommand) .tap(setRemoveAuthorityCommand) @@ -25,8 +26,9 @@ export function setCommands(program: CustomCommand): void { .tap(setCloseCommand) // Buffer commands. - // TODO: list-buffers: List all buffer accounts owned by an authority. .tap(setCreateBufferCommand) .tap(setUpdateBufferCommand) + // TODO: list-buffers: List all buffer accounts owned by an authority. + .tap(setFetchBufferCommand) .tap(setCloseBufferCommand); } From 366f425b86b8ad8c519a324284c076bf9fbf2fc4 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 22:14:42 +0100 Subject: [PATCH 48/60] wip --- clients/js/package.json | 2 +- clients/js/pnpm-lock.yaml | 6 +++--- clients/js/src/cli/commands/close-buffer.ts | 4 ++-- clients/js/src/cli/commands/create-buffer.ts | 4 ++-- clients/js/src/cli/commands/create.ts | 6 +++--- clients/js/src/cli/commands/fetch-buffer.ts | 4 ++-- clients/js/src/cli/commands/fetch.ts | 6 ++++-- clients/js/src/cli/commands/set-authority.ts | 4 ++-- clients/js/src/cli/commands/update-buffer.ts | 4 ++-- clients/js/src/cli/commands/update.ts | 6 +++--- clients/js/src/cli/commands/write.ts | 4 ++-- clients/js/src/cli/logs.ts | 8 ++++---- 12 files changed, 30 insertions(+), 28 deletions(-) diff --git a/clients/js/package.json b/clients/js/package.json index 3b96bf5..bd57176 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -51,9 +51,9 @@ "@iarna/toml": "^2.2.5", "@solana-program/compute-budget": "^0.7.0", "@solana-program/system": "^0.7.0", - "chalk": "^4.1.0", "commander": "^13.0.0", "pako": "^2.1.0", + "picocolors": "^1.1.1", "yaml": "^2.7.0" }, "devDependencies": { diff --git a/clients/js/pnpm-lock.yaml b/clients/js/pnpm-lock.yaml index 0893f6d..4fe4fff 100644 --- a/clients/js/pnpm-lock.yaml +++ b/clients/js/pnpm-lock.yaml @@ -17,15 +17,15 @@ importers: '@solana-program/system': specifier: ^0.7.0 version: 0.7.0(@solana/kit@2.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.3)(ws@8.18.0)) - chalk: - specifier: ^4.1.0 - version: 4.1.2 commander: specifier: ^13.0.0 version: 13.0.0 pako: specifier: ^2.1.0 version: 2.1.0 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 yaml: specifier: ^2.7.0 version: 2.7.0 diff --git a/clients/js/src/cli/commands/close-buffer.ts b/clients/js/src/cli/commands/close-buffer.ts index 239c609..e3993ee 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, Address } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { fetchMaybeBuffer, getCloseInstruction } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { logErrorAndExit } from '../logs'; @@ -58,7 +58,7 @@ export async function doCloseBuffer( ]); await client.planAndExecute( - `Close buffer ${chalk.bold(buffer)}`, + `Close buffer ${picocolors.bold(buffer)}`, instructionPlan ); } diff --git a/clients/js/src/cli/commands/create-buffer.ts b/clients/js/src/cli/commands/create-buffer.ts index cd297df..fd7af6f 100644 --- a/clients/js/src/cli/commands/create-buffer.ts +++ b/clients/js/src/cli/commands/create-buffer.ts @@ -1,5 +1,5 @@ import { generateKeyPairSigner } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { getCreateBufferInstructionPlan } from '../../createBuffer'; import { getAccountSize } from '../../utils'; import { fileArgument } from '../arguments'; @@ -43,7 +43,7 @@ export async function doCreateBuffer( }); await client.planAndExecute( - `Create buffer ${chalk.bold(buffer.address)}`, + `Create buffer ${picocolors.bold(buffer.address)}`, instructionPlan ); } diff --git a/clients/js/src/cli/commands/create.ts b/clients/js/src/cli/commands/create.ts index 00af6db..d103653 100644 --- a/clients/js/src/cli/commands/create.ts +++ b/clients/js/src/cli/commands/create.ts @@ -1,5 +1,5 @@ import { Address } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { getCreateMetadataInstructionPlan } from '../../createMetadata'; import { fetchMaybeMetadata, Seed } from '../../generated'; import { fileArgument, programArgument, seedArgument } from '../arguments'; @@ -54,7 +54,7 @@ export async function doCreate( if (metadataAccount.exists) { // TODO: show derivation seeds. logErrorAndExit( - `Metadata account ${chalk.bold(metadataAccount.address)} already exists.` + `Metadata account ${picocolors.bold(metadataAccount.address)} already exists.` ); } @@ -71,7 +71,7 @@ export async function doCreate( }); await client.planAndExecute( - `Create metadata for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"`, + `Create metadata for program ${picocolors.bold(program)} and seed "${picocolors.bold(seed)}"`, instructionPlan ); } diff --git a/clients/js/src/cli/commands/fetch-buffer.ts b/clients/js/src/cli/commands/fetch-buffer.ts index 52f19de..9535082 100644 --- a/clients/js/src/cli/commands/fetch-buffer.ts +++ b/clients/js/src/cli/commands/fetch-buffer.ts @@ -1,5 +1,5 @@ import { address, Address } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { Option } from 'commander'; import { Compression, Encoding, fetchMaybeBuffer } from '../../generated'; import { unpackDirectData } from '../../packData'; @@ -63,7 +63,7 @@ export async function doFetchBuffer( if (options.output) { writeFile(options.output, content); - logSuccess(`Buffer content saved to ${chalk.bold(options.output)}`); + logSuccess(`Buffer content saved to ${picocolors.bold(options.output)}`); } else { console.log(content); } diff --git a/clients/js/src/cli/commands/fetch.ts b/clients/js/src/cli/commands/fetch.ts index 8482f04..90607b9 100644 --- a/clients/js/src/cli/commands/fetch.ts +++ b/clients/js/src/cli/commands/fetch.ts @@ -1,5 +1,5 @@ import { Address, isSolanaError } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { Option } from 'commander'; import { Compression, @@ -71,7 +71,9 @@ async function doFetch( if (options.output) { writeFile(options.output, content); - logSuccess(`Metadata content saved to ${chalk.bold(options.output)}`); + logSuccess( + `Metadata content saved to ${picocolors.bold(options.output)}` + ); } else { console.log(content); } diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts index 0537bfe..28d529a 100644 --- a/clients/js/src/cli/commands/set-authority.ts +++ b/clients/js/src/cli/commands/set-authority.ts @@ -1,5 +1,5 @@ import { address, Address } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { getSetAuthorityInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; @@ -45,7 +45,7 @@ async function doSetAuthority( seed, }); await client.planAndExecute( - `Set additional authority on metadata account to ${chalk.bold(options.newAuthority)}`, + `Set additional authority on metadata account to ${picocolors.bold(options.newAuthority)}`, sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, diff --git a/clients/js/src/cli/commands/update-buffer.ts b/clients/js/src/cli/commands/update-buffer.ts index e9e9095..274d1e3 100644 --- a/clients/js/src/cli/commands/update-buffer.ts +++ b/clients/js/src/cli/commands/update-buffer.ts @@ -1,5 +1,5 @@ import { address, Address, lamports } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { fetchMaybeBuffer } from '../../generated'; import { getUpdateBufferInstructionPlan } from '../../updateBuffer'; import { fileArgument } from '../arguments'; @@ -67,7 +67,7 @@ export async function doUpdateBuffer( }); await client.planAndExecute( - `Update buffer ${chalk.bold(buffer)}`, + `Update buffer ${picocolors.bold(buffer)}`, instructionPlan ); } diff --git a/clients/js/src/cli/commands/update.ts b/clients/js/src/cli/commands/update.ts index 22d76a1..943e605 100644 --- a/clients/js/src/cli/commands/update.ts +++ b/clients/js/src/cli/commands/update.ts @@ -1,5 +1,5 @@ import { Address } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { fetchMaybeMetadata, Seed } from '../../generated'; import { fileArgument, programArgument, seedArgument } from '../arguments'; import { @@ -54,7 +54,7 @@ export async function doWrite( if (!metadataAccount.exists) { // TODO: show derivation seeds. logErrorAndExit( - `Metadata account ${chalk.bold(metadataAccount.address)} does not exist.` + `Metadata account ${picocolors.bold(metadataAccount.address)} does not exist.` ); } @@ -70,7 +70,7 @@ export async function doWrite( }); await client.planAndExecute( - `Update metadata for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"`, + `Update metadata for program ${picocolors.bold(program)} and seed "${picocolors.bold(seed)}"`, instructionPlan ); } diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts index 9f80db0..1cfa6f4 100644 --- a/clients/js/src/cli/commands/write.ts +++ b/clients/js/src/cli/commands/write.ts @@ -1,5 +1,5 @@ import { Address } from '@solana/kit'; -import chalk from 'chalk'; +import picocolors from 'picocolors'; import { fetchMaybeMetadata, Seed } from '../../generated'; import { getWriteMetadataInstructionPlan } from '../../writeMetadata'; import { fileArgument, programArgument, seedArgument } from '../arguments'; @@ -63,7 +63,7 @@ export async function doWrite( }); await client.planAndExecute( - `Write metadata for program ${chalk.bold(program)} and seed "${chalk.bold(seed)}"`, + `Write metadata for program ${picocolors.bold(program)} and seed "${picocolors.bold(seed)}"`, instructionPlan ); } diff --git a/clients/js/src/cli/logs.ts b/clients/js/src/cli/logs.ts index 6b6086e..8ad6f4d 100644 --- a/clients/js/src/cli/logs.ts +++ b/clients/js/src/cli/logs.ts @@ -1,15 +1,15 @@ -import chalk from 'chalk'; +import picocolors from 'picocolors'; export function logSuccess(message: string): void { - console.warn(chalk.green(`[Success] `) + message); + console.warn(picocolors.green(`[Success] `) + message); } export function logWarning(message: string): void { - console.warn(chalk.yellow(`[Warning] `) + message); + console.warn(picocolors.yellow(`[Warning] `) + message); } export function logError(message: string): void { - console.error(chalk.red(`[Error] `) + message); + console.error(picocolors.red(`[Error] `) + message); } export function logErrorAndExit(message: string): never { From e1f0d7c33eaf2fb5e6646affa0e89063cca36d8d Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 22:38:49 +0100 Subject: [PATCH 49/60] wip --- clients/js/src/cli/commands/close-buffer.ts | 9 ++--- clients/js/src/cli/commands/close.ts | 2 +- clients/js/src/cli/commands/create-buffer.ts | 3 +- clients/js/src/cli/commands/create.ts | 38 ++++++++++--------- .../js/src/cli/commands/remove-authority.ts | 2 +- clients/js/src/cli/commands/set-authority.ts | 5 +-- clients/js/src/cli/commands/set-immutable.ts | 2 +- clients/js/src/cli/commands/update-buffer.ts | 3 +- clients/js/src/cli/commands/update.ts | 38 ++++++++++--------- clients/js/src/cli/commands/write.ts | 37 ++++++++++-------- clients/js/src/cli/logs.ts | 22 +++++++++++ clients/js/src/cli/utils.ts | 3 -- 12 files changed, 96 insertions(+), 68 deletions(-) diff --git a/clients/js/src/cli/commands/close-buffer.ts b/clients/js/src/cli/commands/close-buffer.ts index e3993ee..993c11c 100644 --- a/clients/js/src/cli/commands/close-buffer.ts +++ b/clients/js/src/cli/commands/close-buffer.ts @@ -2,7 +2,7 @@ import { address, Address } from '@solana/kit'; import picocolors from 'picocolors'; import { fetchMaybeBuffer, getCloseInstruction } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; -import { logErrorAndExit } from '../logs'; +import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions } from '../options'; import { CustomCommand, getClient } from '../utils'; @@ -57,8 +57,7 @@ export async function doCloseBuffer( }), ]); - await client.planAndExecute( - `Close buffer ${picocolors.bold(buffer)}`, - instructionPlan - ); + logCommand(`Closing buffer ${picocolors.bold(buffer)}...`); + + await client.planAndExecute(instructionPlan); } diff --git a/clients/js/src/cli/commands/close.ts b/clients/js/src/cli/commands/close.ts index c358d58..ed86f1e 100644 --- a/clients/js/src/cli/commands/close.ts +++ b/clients/js/src/cli/commands/close.ts @@ -35,7 +35,7 @@ async function doClose( seed ); await client.planAndExecute( - 'Close metadata account and recover rent', + // 'Close metadata account and recover rent', sequentialInstructionPlan([ getCloseInstruction({ account: metadata, diff --git a/clients/js/src/cli/commands/create-buffer.ts b/clients/js/src/cli/commands/create-buffer.ts index fd7af6f..85f29d7 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 picocolors from 'picocolors'; import { getCreateBufferInstructionPlan } from '../../createBuffer'; import { getAccountSize } from '../../utils'; import { fileArgument } from '../arguments'; @@ -43,7 +42,7 @@ export async function doCreateBuffer( }); await client.planAndExecute( - `Create buffer ${picocolors.bold(buffer.address)}`, + // `Create buffer ${picocolors.bold(buffer.address)}`, instructionPlan ); } diff --git a/clients/js/src/cli/commands/create.ts b/clients/js/src/cli/commands/create.ts index d103653..4cbd01f 100644 --- a/clients/js/src/cli/commands/create.ts +++ b/clients/js/src/cli/commands/create.ts @@ -3,7 +3,7 @@ import picocolors from 'picocolors'; import { getCreateMetadataInstructionPlan } from '../../createMetadata'; import { fetchMaybeMetadata, Seed } from '../../generated'; import { fileArgument, programArgument, seedArgument } from '../arguments'; -import { logErrorAndExit } from '../logs'; +import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions, nonCanonicalWriteOption, @@ -40,38 +40,42 @@ export async function doCreate( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { metadata, programData } = await getPdaDetailsForWriting( + const { metadata, programData, isCanonical } = await getPdaDetailsForWriting( client, options, program, seed ); + + logCommand(`Creating metadata account...`, { + metadata, + program, + seed, + authority: isCanonical ? undefined : client.authority.address, + }); + const [metadataAccount, writeInput] = await Promise.all([ fetchMaybeMetadata(client.rpc, metadata), getWriteInput(client, file, options), ]); if (metadataAccount.exists) { - // TODO: show derivation seeds. logErrorAndExit( `Metadata account ${picocolors.bold(metadataAccount.address)} already exists.` ); } - const instructionPlan = await getCreateMetadataInstructionPlan({ - ...client, - ...writeInput, - payer: client.payer, - authority: client.authority, - program, - programData, - seed, - metadata, - planner: client.planner, - }); - await client.planAndExecute( - `Create metadata for program ${picocolors.bold(program)} and seed "${picocolors.bold(seed)}"`, - instructionPlan + await getCreateMetadataInstructionPlan({ + ...client, + ...writeInput, + payer: client.payer, + authority: client.authority, + program, + programData, + seed, + metadata, + planner: client.planner, + }) ); } diff --git a/clients/js/src/cli/commands/remove-authority.ts b/clients/js/src/cli/commands/remove-authority.ts index e4526cb..cfa528d 100644 --- a/clients/js/src/cli/commands/remove-authority.ts +++ b/clients/js/src/cli/commands/remove-authority.ts @@ -33,7 +33,7 @@ async function doRemoveAuthority( seed, }); await client.planAndExecute( - 'Remove additional authority from metadata account', + // 'Remove additional authority from metadata account', sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts index 28d529a..d25f6af 100644 --- a/clients/js/src/cli/commands/set-authority.ts +++ b/clients/js/src/cli/commands/set-authority.ts @@ -1,12 +1,11 @@ import { address, Address } from '@solana/kit'; -import picocolors from 'picocolors'; import { getSetAuthorityInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; +import { logErrorAndExit } from '../logs'; import { GlobalOptions } from '../options'; import { CustomCommand, getClient } from '../utils'; -import { logErrorAndExit } from '../logs'; export function setSetAuthorityCommand(program: CustomCommand): void { program @@ -45,7 +44,7 @@ async function doSetAuthority( seed, }); await client.planAndExecute( - `Set additional authority on metadata account to ${picocolors.bold(options.newAuthority)}`, + // `Set additional authority on metadata account to ${picocolors.bold(options.newAuthority)}`, sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, diff --git a/clients/js/src/cli/commands/set-immutable.ts b/clients/js/src/cli/commands/set-immutable.ts index 2bc4b2a..4f128a7 100644 --- a/clients/js/src/cli/commands/set-immutable.ts +++ b/clients/js/src/cli/commands/set-immutable.ts @@ -37,7 +37,7 @@ async function doSetImmutable( seed ); await client.planAndExecute( - 'Make metadata account immutable', + // 'Make metadata account immutable', sequentialInstructionPlan([ getSetImmutableInstruction({ metadata, diff --git a/clients/js/src/cli/commands/update-buffer.ts b/clients/js/src/cli/commands/update-buffer.ts index 274d1e3..ea8fcf4 100644 --- a/clients/js/src/cli/commands/update-buffer.ts +++ b/clients/js/src/cli/commands/update-buffer.ts @@ -1,5 +1,4 @@ import { address, Address, lamports } from '@solana/kit'; -import picocolors from 'picocolors'; import { fetchMaybeBuffer } from '../../generated'; import { getUpdateBufferInstructionPlan } from '../../updateBuffer'; import { fileArgument } from '../arguments'; @@ -67,7 +66,7 @@ export async function doUpdateBuffer( }); await client.planAndExecute( - `Update buffer ${picocolors.bold(buffer)}`, + // `Update buffer ${picocolors.bold(buffer)}`, instructionPlan ); } diff --git a/clients/js/src/cli/commands/update.ts b/clients/js/src/cli/commands/update.ts index 943e605..6625f72 100644 --- a/clients/js/src/cli/commands/update.ts +++ b/clients/js/src/cli/commands/update.ts @@ -1,7 +1,9 @@ import { Address } from '@solana/kit'; import picocolors from 'picocolors'; import { fetchMaybeMetadata, Seed } from '../../generated'; +import { getUpdateMetadataInstructionPlan } from '../../updateMetadata'; import { fileArgument, programArgument, seedArgument } from '../arguments'; +import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions, nonCanonicalWriteOption, @@ -15,8 +17,6 @@ import { getPdaDetailsForWriting, getWriteInput, } from '../utils'; -import { logErrorAndExit } from '../logs'; -import { getUpdateMetadataInstructionPlan } from '../../updateMetadata'; export function setUpdateCommand(program: CustomCommand): void { program @@ -40,37 +40,41 @@ export async function doWrite( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { metadata, programData } = await getPdaDetailsForWriting( + const { metadata, programData, isCanonical } = await getPdaDetailsForWriting( client, options, program, seed ); + + logCommand(`Updating metadata account...`, { + metadata, + program, + seed, + authority: isCanonical ? undefined : client.authority.address, + }); + const [metadataAccount, writeInput] = await Promise.all([ fetchMaybeMetadata(client.rpc, metadata), getWriteInput(client, file, options), ]); if (!metadataAccount.exists) { - // TODO: show derivation seeds. logErrorAndExit( `Metadata account ${picocolors.bold(metadataAccount.address)} does not exist.` ); } - const instructionPlan = await getUpdateMetadataInstructionPlan({ - ...client, - ...writeInput, - payer: client.payer, - authority: client.authority, - program, - programData, - metadata: metadataAccount, - planner: client.planner, - }); - await client.planAndExecute( - `Update metadata for program ${picocolors.bold(program)} and seed "${picocolors.bold(seed)}"`, - instructionPlan + await getUpdateMetadataInstructionPlan({ + ...client, + ...writeInput, + payer: client.payer, + authority: client.authority, + program, + programData, + metadata: metadataAccount, + planner: client.planner, + }) ); } diff --git a/clients/js/src/cli/commands/write.ts b/clients/js/src/cli/commands/write.ts index 1cfa6f4..9453485 100644 --- a/clients/js/src/cli/commands/write.ts +++ b/clients/js/src/cli/commands/write.ts @@ -1,8 +1,8 @@ import { Address } from '@solana/kit'; -import picocolors from 'picocolors'; import { fetchMaybeMetadata, Seed } from '../../generated'; import { getWriteMetadataInstructionPlan } from '../../writeMetadata'; import { fileArgument, programArgument, seedArgument } from '../arguments'; +import { logCommand } from '../logs'; import { GlobalOptions, nonCanonicalWriteOption, @@ -39,31 +39,36 @@ export async function doWrite( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { metadata, programData } = await getPdaDetailsForWriting( + const { metadata, programData, isCanonical } = await getPdaDetailsForWriting( client, options, program, seed ); - const [metadataAccount, writeInput] = await Promise.all([ - fetchMaybeMetadata(client.rpc, metadata), - getWriteInput(client, file, options), - ]); - const instructionPlan = await getWriteMetadataInstructionPlan({ - ...client, - ...writeInput, - payer: client.payer, - authority: client.authority, + logCommand(`Writing metadata account...`, { + metadata, program, - programData, seed, - metadata: metadataAccount, - planner: client.planner, + authority: isCanonical ? undefined : client.authority.address, }); + const [metadataAccount, writeInput] = await Promise.all([ + fetchMaybeMetadata(client.rpc, metadata), + getWriteInput(client, file, options), + ]); + await client.planAndExecute( - `Write metadata for program ${picocolors.bold(program)} and seed "${picocolors.bold(seed)}"`, - instructionPlan + await getWriteMetadataInstructionPlan({ + ...client, + ...writeInput, + payer: client.payer, + authority: client.authority, + program, + programData, + seed, + metadata: metadataAccount, + planner: client.planner, + }) ); } diff --git a/clients/js/src/cli/logs.ts b/clients/js/src/cli/logs.ts index 8ad6f4d..44cd95e 100644 --- a/clients/js/src/cli/logs.ts +++ b/clients/js/src/cli/logs.ts @@ -1,5 +1,27 @@ import picocolors from 'picocolors'; +export function logCommand( + message: string, + data: Record = {} +): void { + console.log(''); + console.log(picocolors.bold(picocolors.blue(message))); + const entries = Object.entries(data).filter( + ([_, value]) => value !== undefined + ); + if (entries.length > 0) { + console.log( + entries + .map(([key, value], i) => { + const prefix = i === entries.length - 1 ? '└─' : '├─'; + return ` ${picocolors.blue(prefix + ' ' + key)}: ${value}`; + }) + .join('\n') + ); + } + console.log(''); +} + export function logSuccess(message: string): void { console.warn(picocolors.green(`[Success] `) + message); } diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index 2c3ad2d..1af499f 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -67,7 +67,6 @@ export type Client = ReadonlyClient & { executor: TransactionPlanExecutor; payer: TransactionSigner & MessageSigner; planAndExecute: ( - description: string, instructionPlan: InstructionPlan ) => Promise; planner: TransactionPlanner; @@ -89,10 +88,8 @@ export async function getClient(options: GlobalOptions): Promise { parallelChunkSize: 5, }); const planAndExecute = async ( - description: string, instructionPlan: InstructionPlan ): Promise => { - console.log(description); const transactionPlan = await planner(instructionPlan); const result = await executor(transactionPlan); logSuccess('Operation executed successfully'); From b6c883411b0aebef954029fa21361340ff5f928c Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 15 Apr 2025 22:48:44 +0100 Subject: [PATCH 50/60] wip --- clients/js/src/cli/commands/close-buffer.ts | 23 ++++++++-------- clients/js/src/cli/commands/close.ts | 12 +++++++-- clients/js/src/cli/commands/create-buffer.ts | 25 ++++++++--------- clients/js/src/cli/commands/create.ts | 2 +- .../js/src/cli/commands/remove-authority.ts | 9 ++++++- clients/js/src/cli/commands/set-authority.ts | 11 ++++++-- clients/js/src/cli/commands/set-immutable.ts | 12 +++++++-- clients/js/src/cli/commands/update-buffer.ts | 27 +++++++++---------- 8 files changed, 75 insertions(+), 46 deletions(-) diff --git a/clients/js/src/cli/commands/close-buffer.ts b/clients/js/src/cli/commands/close-buffer.ts index 993c11c..4316a20 100644 --- a/clients/js/src/cli/commands/close-buffer.ts +++ b/clients/js/src/cli/commands/close-buffer.ts @@ -1,5 +1,4 @@ import { address, Address } from '@solana/kit'; -import picocolors from 'picocolors'; import { fetchMaybeBuffer, getCloseInstruction } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { logCommand, logErrorAndExit } from '../logs'; @@ -41,6 +40,8 @@ export async function doCloseBuffer( _: Options, cmd: CustomCommand ) { + logCommand(`Closing buffer...`, { buffer }); + const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); const bufferAccount = await fetchMaybeBuffer(client.rpc, buffer); @@ -49,15 +50,13 @@ export async function doCloseBuffer( logErrorAndExit(`Buffer account not found: "${buffer}"`); } - const instructionPlan = sequentialInstructionPlan([ - getCloseInstruction({ - account: buffer, - authority: client.authority, - destination: options.recipient ?? client.payer.address, - }), - ]); - - logCommand(`Closing buffer ${picocolors.bold(buffer)}...`); - - await client.planAndExecute(instructionPlan); + await client.planAndExecute( + sequentialInstructionPlan([ + getCloseInstruction({ + account: buffer, + authority: client.authority, + 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 ed86f1e..6e5ffa1 100644 --- a/clients/js/src/cli/commands/close.ts +++ b/clients/js/src/cli/commands/close.ts @@ -8,6 +8,7 @@ import { nonCanonicalWriteOption, } from '../options'; import { CustomCommand, getClient, getPdaDetailsForWriting } from '../utils'; +import { logCommand } from '../logs'; export function setCloseCommand(program: CustomCommand): void { program @@ -28,14 +29,21 @@ async function doClose( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { metadata, programData } = await getPdaDetailsForWriting( + const { metadata, programData, isCanonical } = await getPdaDetailsForWriting( client, options, program, seed ); + + logCommand(`Closing metadata account...`, { + metadata, + program, + seed, + authority: isCanonical ? undefined : client.authority.address, + }); + await client.planAndExecute( - // 'Close metadata account and recover rent', sequentialInstructionPlan([ getCloseInstruction({ account: metadata, diff --git a/clients/js/src/cli/commands/create-buffer.ts b/clients/js/src/cli/commands/create-buffer.ts index 85f29d7..58bcb0b 100644 --- a/clients/js/src/cli/commands/create-buffer.ts +++ b/clients/js/src/cli/commands/create-buffer.ts @@ -4,6 +4,7 @@ import { getAccountSize } from '../../utils'; import { fileArgument } from '../arguments'; import { GlobalOptions, setWriteOptions, WriteOptions } from '../options'; import { CustomCommand, getClient, getWriteInput } from '../utils'; +import { logCommand } from '../logs'; export function setCreateBufferCommand(program: CustomCommand): void { program @@ -26,23 +27,23 @@ export async function doCreateBuffer( generateKeyPairSigner(), getWriteInput(client, file, options), ]); + + logCommand(`Creating new buffer...`, { buffer: buffer.address }); + const data = writeInput.buffer?.data.data ?? writeInput.data; const rent = await client.rpc .getMinimumBalanceForRentExemption(getAccountSize(data.length)) .send(); - const instructionPlan = getCreateBufferInstructionPlan({ - newBuffer: buffer, - authority: client.authority, - payer: client.payer, - rent, - sourceBuffer: writeInput.buffer, - closeSourceBuffer: writeInput.closeBuffer, - data, - }); - await client.planAndExecute( - // `Create buffer ${picocolors.bold(buffer.address)}`, - instructionPlan + getCreateBufferInstructionPlan({ + newBuffer: buffer, + authority: client.authority, + payer: client.payer, + rent, + sourceBuffer: writeInput.buffer, + closeSourceBuffer: writeInput.closeBuffer, + data, + }) ); } diff --git a/clients/js/src/cli/commands/create.ts b/clients/js/src/cli/commands/create.ts index 4cbd01f..573a0e5 100644 --- a/clients/js/src/cli/commands/create.ts +++ b/clients/js/src/cli/commands/create.ts @@ -47,7 +47,7 @@ export async function doCreate( seed ); - logCommand(`Creating metadata account...`, { + logCommand(`Creating new metadata account...`, { metadata, program, seed, diff --git a/clients/js/src/cli/commands/remove-authority.ts b/clients/js/src/cli/commands/remove-authority.ts index cfa528d..d20dbc7 100644 --- a/clients/js/src/cli/commands/remove-authority.ts +++ b/clients/js/src/cli/commands/remove-authority.ts @@ -5,6 +5,7 @@ import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; import { GlobalOptions } from '../options'; import { CustomCommand, getClient } from '../utils'; +import { logCommand } from '../logs'; export function setRemoveAuthorityCommand(program: CustomCommand): void { program @@ -32,8 +33,14 @@ async function doRemoveAuthority( authority: client.authority, seed, }); + + logCommand(`Removing additional authority from metadata account...`, { + metadata, + program, + seed, + }); + await client.planAndExecute( - // 'Remove additional authority from metadata account', sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts index d25f6af..f973892 100644 --- a/clients/js/src/cli/commands/set-authority.ts +++ b/clients/js/src/cli/commands/set-authority.ts @@ -3,7 +3,7 @@ import { getSetAuthorityInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; -import { logErrorAndExit } from '../logs'; +import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions } from '../options'; import { CustomCommand, getClient } from '../utils'; @@ -43,8 +43,15 @@ async function doSetAuthority( program, seed, }); + + logCommand(`Setting additional authority from metadata account...`, { + 'new authority': options.newAuthority, + metadata, + program, + seed, + }); + await client.planAndExecute( - // `Set additional authority on metadata account to ${picocolors.bold(options.newAuthority)}`, sequentialInstructionPlan([ getSetAuthorityInstruction({ account: metadata, diff --git a/clients/js/src/cli/commands/set-immutable.ts b/clients/js/src/cli/commands/set-immutable.ts index 4f128a7..7dc3585 100644 --- a/clients/js/src/cli/commands/set-immutable.ts +++ b/clients/js/src/cli/commands/set-immutable.ts @@ -2,6 +2,7 @@ import { Address } from '@solana/kit'; import { getSetImmutableInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { programArgument, seedArgument } from '../arguments'; +import { logCommand } from '../logs'; import { GlobalOptions, NonCanonicalWriteOption, @@ -30,14 +31,21 @@ async function doSetImmutable( ) { const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); - const { metadata, programData } = await getPdaDetailsForWriting( + const { metadata, programData, isCanonical } = await getPdaDetailsForWriting( client, options, program, seed ); + + logCommand(`Making metadata account immutable...`, { + metadata, + program, + seed, + authority: isCanonical ? undefined : client.authority.address, + }); + await client.planAndExecute( - // 'Make metadata account immutable', sequentialInstructionPlan([ getSetImmutableInstruction({ metadata, diff --git a/clients/js/src/cli/commands/update-buffer.ts b/clients/js/src/cli/commands/update-buffer.ts index ea8fcf4..e022be8 100644 --- a/clients/js/src/cli/commands/update-buffer.ts +++ b/clients/js/src/cli/commands/update-buffer.ts @@ -2,7 +2,7 @@ import { address, Address, lamports } from '@solana/kit'; import { fetchMaybeBuffer } from '../../generated'; import { getUpdateBufferInstructionPlan } from '../../updateBuffer'; import { fileArgument } from '../arguments'; -import { logErrorAndExit } from '../logs'; +import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions, setWriteOptions, WriteOptions } from '../options'; import { CustomCommand, getClient, getWriteInput } from '../utils'; @@ -33,6 +33,8 @@ export async function doUpdateBuffer( _: Options, cmd: CustomCommand ) { + logCommand(`Updating buffer...`, { buffer }); + const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); const [writeInput, bufferAccount] = await Promise.all([ @@ -54,19 +56,16 @@ export async function doUpdateBuffer( .send() : lamports(0n); - const instructionPlan = getUpdateBufferInstructionPlan({ - buffer, - authority: client.authority, - payer: client.payer, - extraRent, - sizeDifference, - sourceBuffer: writeInput.buffer, - closeSourceBuffer: writeInput.closeBuffer, - data: newData, - }); - await client.planAndExecute( - // `Update buffer ${picocolors.bold(buffer)}`, - instructionPlan + getUpdateBufferInstructionPlan({ + buffer, + authority: client.authority, + payer: client.payer, + extraRent, + sizeDifference, + sourceBuffer: writeInput.buffer, + closeSourceBuffer: writeInput.closeBuffer, + data: newData, + }) ); } From b07ed764b08db32b2a275cbdbecbfc2e4cbdf231 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 09:12:18 +0100 Subject: [PATCH 51/60] wip --- clients/js/src/cli/arguments.ts | 11 +++++ clients/js/src/cli/commands/close-buffer.ts | 17 ++----- clients/js/src/cli/commands/fetch-buffer.ts | 17 ++----- clients/js/src/cli/commands/index.ts | 3 +- .../src/cli/commands/set-buffer-authority.ts | 45 +++++++++++++++++++ clients/js/src/cli/commands/update-buffer.ts | 20 +++------ clients/js/src/cli/options.ts | 14 ++++++ 7 files changed, 85 insertions(+), 42 deletions(-) create mode 100644 clients/js/src/cli/commands/set-buffer-authority.ts diff --git a/clients/js/src/cli/arguments.ts b/clients/js/src/cli/arguments.ts index 02e0fd2..f2aed8e 100644 --- a/clients/js/src/cli/arguments.ts +++ b/clients/js/src/cli/arguments.ts @@ -22,3 +22,14 @@ export const fileArgument = new Argument( '[file]', 'Filepath of the data to upload (creates a "direct" data source). See options for other sources such as --text, --url and --account.' ); + +export const bufferArgument = new Argument( + '', + 'The address of the buffer account.' +).argParser((value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid buffer address: "${value}"`); + } +}); diff --git a/clients/js/src/cli/commands/close-buffer.ts b/clients/js/src/cli/commands/close-buffer.ts index 4316a20..cfbed28 100644 --- a/clients/js/src/cli/commands/close-buffer.ts +++ b/clients/js/src/cli/commands/close-buffer.ts @@ -4,22 +4,13 @@ import { sequentialInstructionPlan } from '../../instructionPlans'; import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions } from '../options'; import { CustomCommand, getClient } from '../utils'; +import { bufferArgument } from '../arguments'; export function setCloseBufferCommand(program: CustomCommand): void { program .command('close-buffer') .description('Close an existing buffer account.') - .argument( - '', - 'The address of the buffer account to close.', - (value: string): Address => { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid buffer address: "${value}"`); - } - } - ) + .addArgument(bufferArgument) .option( '--recipient ', 'Address receiving the storage fees for the closed account.', @@ -40,10 +31,10 @@ export async function doCloseBuffer( _: Options, cmd: CustomCommand ) { - logCommand(`Closing buffer...`, { buffer }); - const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); + + logCommand(`Closing buffer...`, { buffer }); const bufferAccount = await fetchMaybeBuffer(client.rpc, buffer); if (!bufferAccount.exists) { diff --git a/clients/js/src/cli/commands/fetch-buffer.ts b/clients/js/src/cli/commands/fetch-buffer.ts index 9535082..27fd65c 100644 --- a/clients/js/src/cli/commands/fetch-buffer.ts +++ b/clients/js/src/cli/commands/fetch-buffer.ts @@ -1,8 +1,9 @@ -import { address, Address } from '@solana/kit'; -import picocolors from 'picocolors'; +import { Address } from '@solana/kit'; import { Option } from 'commander'; +import picocolors from 'picocolors'; import { Compression, Encoding, fetchMaybeBuffer } from '../../generated'; import { unpackDirectData } from '../../packData'; +import { bufferArgument } from '../arguments'; import { logErrorAndExit, logSuccess } from '../logs'; import { compressionOption, @@ -19,17 +20,7 @@ export function setFetchBufferCommand(program: CustomCommand): void { program .command('fetch-buffer') .description('Fetch the content of a buffer account.') - .argument( - '', - 'The address of the buffer account to fetch.', - (value: string): Address => { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid buffer address: "${value}"`); - } - } - ) + .addArgument(bufferArgument) .addOption(outputOption) .addOption(compressionOption) .addOption(encodingOption) diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index 5300438..7a419c3 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -7,6 +7,7 @@ import { setFetchCommand } from './fetch'; import { setFetchBufferCommand } from './fetch-buffer'; import { setRemoveAuthorityCommand } from './remove-authority'; import { setSetAuthorityCommand } from './set-authority'; +import { setSetBufferAuthorityCommand } from './set-buffer-authority'; import { setSetImmutableCommand } from './set-immutable'; import { setUpdateCommand } from './update'; import { setUpdateBufferCommand } from './update-buffer'; @@ -18,7 +19,6 @@ export function setCommands(program: CustomCommand): void { .tap(setWriteCommand) .tap(setCreateCommand) .tap(setUpdateCommand) - // TODO: list: List all metadata accounts owned by an authority. .tap(setFetchCommand) .tap(setSetAuthorityCommand) .tap(setRemoveAuthorityCommand) @@ -30,5 +30,6 @@ export function setCommands(program: CustomCommand): void { .tap(setUpdateBufferCommand) // TODO: list-buffers: List all buffer accounts owned by an authority. .tap(setFetchBufferCommand) + .tap(setSetBufferAuthorityCommand) .tap(setCloseBufferCommand); } diff --git a/clients/js/src/cli/commands/set-buffer-authority.ts b/clients/js/src/cli/commands/set-buffer-authority.ts new file mode 100644 index 0000000..ad136a6 --- /dev/null +++ b/clients/js/src/cli/commands/set-buffer-authority.ts @@ -0,0 +1,45 @@ +import { Address } from '@solana/kit'; +import { getSetAuthorityInstruction } from '../../generated'; +import { sequentialInstructionPlan } from '../../instructionPlans'; +import { bufferArgument } from '../arguments'; +import { logCommand } from '../logs'; +import { + GlobalOptions, + NewAuthorityOption, + newAuthorityOption, +} from '../options'; +import { CustomCommand, getClient } from '../utils'; + +export function setSetBufferAuthorityCommand(program: CustomCommand): void { + program + .command('set-buffer-authority') + .description('Update the authority of an existing buffer account.') + .addArgument(bufferArgument) + .addOption(newAuthorityOption) + .action(doSetBufferAuthority); +} + +type Options = NewAuthorityOption; +export async function doSetBufferAuthority( + buffer: Address, + _: Options, + cmd: CustomCommand +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = await getClient(options); + + logCommand(`Updating buffer authority...`, { + buffer, + 'new authority': options.newAuthority, + }); + + await client.planAndExecute( + sequentialInstructionPlan([ + getSetAuthorityInstruction({ + account: buffer, + authority: client.authority, + newAuthority: options.newAuthority, + }), + ]) + ); +} diff --git a/clients/js/src/cli/commands/update-buffer.ts b/clients/js/src/cli/commands/update-buffer.ts index e022be8..afbef64 100644 --- a/clients/js/src/cli/commands/update-buffer.ts +++ b/clients/js/src/cli/commands/update-buffer.ts @@ -1,7 +1,7 @@ -import { address, Address, lamports } from '@solana/kit'; +import { Address, lamports } from '@solana/kit'; import { fetchMaybeBuffer } from '../../generated'; import { getUpdateBufferInstructionPlan } from '../../updateBuffer'; -import { fileArgument } from '../arguments'; +import { bufferArgument, fileArgument } from '../arguments'; import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions, setWriteOptions, WriteOptions } from '../options'; import { CustomCommand, getClient, getWriteInput } from '../utils'; @@ -10,17 +10,7 @@ export function setUpdateBufferCommand(program: CustomCommand): void { program .command('update-buffer') .description('Update an existing buffer account.') - .argument( - '', - 'The address of the buffer account to update.', - (value: string): Address => { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid buffer address: "${value}"`); - } - } - ) + .addArgument(bufferArgument) .addArgument(fileArgument) .tap(setWriteOptions) .action(doUpdateBuffer); @@ -33,10 +23,10 @@ export async function doUpdateBuffer( _: Options, cmd: CustomCommand ) { - logCommand(`Updating buffer...`, { buffer }); - const options = cmd.optsWithGlobals() as GlobalOptions & Options; const client = await getClient(options); + + logCommand(`Updating buffer...`, { buffer }); const [writeInput, bufferAccount] = await Promise.all([ getWriteInput(client, file, options), fetchMaybeBuffer(client.rpc, buffer), diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index 5504ff6..cbe4cdd 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -221,3 +221,17 @@ export const outputOption = new Option( '-o, --output ', 'Path to save the retrieved data.' ); + +export type NewAuthorityOption = { newAuthority: Address }; +export const newAuthorityOption = new Option( + '--new-authority ', + 'The new authority to set' +) + .makeOptionMandatory() + .argParser((value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid new authority address: "${value}"`); + } + }); From 39ef0138ff6264b71826e15ceee7722e8437ee48 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 09:20:03 +0100 Subject: [PATCH 52/60] wip --- clients/js/src/cli/arguments.ts | 19 ++----------- clients/js/src/cli/commands/close-buffer.ts | 13 +++------ clients/js/src/cli/commands/set-authority.ts | 24 ++++++---------- clients/js/src/cli/options.ts | 29 ++++---------------- clients/js/src/cli/parsers.ts | 19 +++++++++++++ 5 files changed, 40 insertions(+), 64 deletions(-) create mode 100644 clients/js/src/cli/parsers.ts diff --git a/clients/js/src/cli/arguments.ts b/clients/js/src/cli/arguments.ts index f2aed8e..db8d15e 100644 --- a/clients/js/src/cli/arguments.ts +++ b/clients/js/src/cli/arguments.ts @@ -1,6 +1,5 @@ import { Argument } from 'commander'; -import { address, Address } from '@solana/kit'; -import { logErrorAndExit } from './logs'; +import { addressParser } from './parsers'; export const seedArgument = new Argument( '', @@ -10,13 +9,7 @@ export const seedArgument = new Argument( export const programArgument = new Argument( '', 'Program associated with the metadata account.' -).argParser((value: string): Address => { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid program address: "${value}"`); - } -}); +).argParser(addressParser('program')); export const fileArgument = new Argument( '[file]', @@ -26,10 +19,4 @@ export const fileArgument = new Argument( export const bufferArgument = new Argument( '', 'The address of the buffer account.' -).argParser((value: string): Address => { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid buffer address: "${value}"`); - } -}); +).argParser(addressParser('buffer')); diff --git a/clients/js/src/cli/commands/close-buffer.ts b/clients/js/src/cli/commands/close-buffer.ts index cfbed28..e4a13c2 100644 --- a/clients/js/src/cli/commands/close-buffer.ts +++ b/clients/js/src/cli/commands/close-buffer.ts @@ -1,10 +1,11 @@ -import { address, Address } from '@solana/kit'; +import { Address } from '@solana/kit'; import { fetchMaybeBuffer, getCloseInstruction } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; +import { bufferArgument } from '../arguments'; import { logCommand, logErrorAndExit } from '../logs'; import { GlobalOptions } from '../options'; +import { addressParser } from '../parsers'; import { CustomCommand, getClient } from '../utils'; -import { bufferArgument } from '../arguments'; export function setCloseBufferCommand(program: CustomCommand): void { program @@ -14,13 +15,7 @@ export function setCloseBufferCommand(program: CustomCommand): void { .option( '--recipient ', 'Address receiving the storage fees for the closed account.', - (value: string): Address => { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid recipient address: "${value}"`); - } - } + addressParser('recipient') ) .action(doCloseBuffer); } diff --git a/clients/js/src/cli/commands/set-authority.ts b/clients/js/src/cli/commands/set-authority.ts index f973892..b4d011b 100644 --- a/clients/js/src/cli/commands/set-authority.ts +++ b/clients/js/src/cli/commands/set-authority.ts @@ -1,10 +1,14 @@ -import { address, Address } from '@solana/kit'; +import { Address } from '@solana/kit'; import { getSetAuthorityInstruction, Seed } from '../../generated'; import { sequentialInstructionPlan } from '../../instructionPlans'; import { getPdaDetails } from '../../internals'; import { programArgument, seedArgument } from '../arguments'; -import { logCommand, logErrorAndExit } from '../logs'; -import { GlobalOptions } from '../options'; +import { logCommand } from '../logs'; +import { + GlobalOptions, + NewAuthorityOption, + newAuthorityOption, +} from '../options'; import { CustomCommand, getClient } from '../utils'; export function setSetAuthorityCommand(program: CustomCommand): void { @@ -15,21 +19,11 @@ export function setSetAuthorityCommand(program: CustomCommand): void { ) .addArgument(seedArgument) .addArgument(programArgument) - .requiredOption( - '--new-authority ', - 'The new authority to set', - (value: string): Address => { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid new authority address: "${value}"`); - } - } - ) + .addOption(newAuthorityOption) .action(doSetAuthority); } -type Options = { newAuthority: Address }; +type Options = NewAuthorityOption; async function doSetAuthority( seed: Seed, program: Address, diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index cbe4cdd..184eb58 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -1,7 +1,8 @@ -import { address, type Address, type MicroLamports } from '@solana/kit'; +import { type Address, type MicroLamports } from '@solana/kit'; import { Option } from 'commander'; import { Compression, Encoding, Format } from '../generated'; import { logErrorAndExit } from './logs'; +import { addressOrBooleanParser, addressParser } from './parsers'; import { CustomCommand } from './utils'; export type GlobalOptions = KeypairOption & @@ -53,14 +54,7 @@ export const exportOption = new Option( 'When provided, export transactions instead of running them. An optional address can be provided to override the local keypair as the authority.' ) .default(false) - .argParser((value: string | undefined): Address | boolean => { - if (value === undefined) return true; - try { - return address(value); - } catch { - logErrorAndExit(`Invalid export address: "${value}"`); - } - }); + .argParser(addressOrBooleanParser('export')); export type WriteOptions = TextOption & UrlOption & @@ -207,14 +201,7 @@ export const nonCanonicalReadOption = new Option( 'When provided, a non-canonical metadata account will be downloaded using the provided address or the active keypair as the authority.' ) .default(false) - .argParser((value: string | undefined): Address | boolean => { - if (value === undefined) return true; - try { - return address(value); - } catch { - logErrorAndExit(`Invalid non-canonical address: "${value}"`); - } - }); + .argParser(addressOrBooleanParser('non-canonical')); export type OutputOption = { output?: string }; export const outputOption = new Option( @@ -228,10 +215,4 @@ export const newAuthorityOption = new Option( 'The new authority to set' ) .makeOptionMandatory() - .argParser((value: string): Address => { - try { - return address(value); - } catch { - logErrorAndExit(`Invalid new authority address: "${value}"`); - } - }); + .argParser(addressParser('new authority')); diff --git a/clients/js/src/cli/parsers.ts b/clients/js/src/cli/parsers.ts new file mode 100644 index 0000000..0405924 --- /dev/null +++ b/clients/js/src/cli/parsers.ts @@ -0,0 +1,19 @@ +import { Address, address } from '@solana/kit'; +import { logErrorAndExit } from './logs'; + +export const addressParser = + (identifier: string) => + (value: string): Address => { + try { + return address(value); + } catch { + logErrorAndExit(`Invalid ${identifier} address: "${value}"`); + } + }; + +export const addressOrBooleanParser = + (identifier: string) => + (value: string | undefined): Address | boolean => { + if (value === undefined) return true; + return addressParser(identifier)(value); + }; From a03bbd7d47762b0493a04e6f129119f3f5622adc Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 10:03:30 +0100 Subject: [PATCH 53/60] wip --- clients/js/src/cli/commands/index.ts | 3 +- clients/js/src/cli/commands/list-buffers.ts | 98 +++++++++++++++++++++ clients/js/src/cli/logs.ts | 60 +++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 clients/js/src/cli/commands/list-buffers.ts diff --git a/clients/js/src/cli/commands/index.ts b/clients/js/src/cli/commands/index.ts index 7a419c3..71cd59e 100644 --- a/clients/js/src/cli/commands/index.ts +++ b/clients/js/src/cli/commands/index.ts @@ -5,6 +5,7 @@ import { setCreateCommand } from './create'; import { setCreateBufferCommand } from './create-buffer'; import { setFetchCommand } from './fetch'; import { setFetchBufferCommand } from './fetch-buffer'; +import { setListBuffersCommand } from './list-buffers'; import { setRemoveAuthorityCommand } from './remove-authority'; import { setSetAuthorityCommand } from './set-authority'; import { setSetBufferAuthorityCommand } from './set-buffer-authority'; @@ -28,7 +29,7 @@ export function setCommands(program: CustomCommand): void { // Buffer commands. .tap(setCreateBufferCommand) .tap(setUpdateBufferCommand) - // TODO: list-buffers: List all buffer accounts owned by an authority. + .tap(setListBuffersCommand) .tap(setFetchBufferCommand) .tap(setSetBufferAuthorityCommand) .tap(setCloseBufferCommand); diff --git a/clients/js/src/cli/commands/list-buffers.ts b/clients/js/src/cli/commands/list-buffers.ts new file mode 100644 index 0000000..d5a5224 --- /dev/null +++ b/clients/js/src/cli/commands/list-buffers.ts @@ -0,0 +1,98 @@ +import { + Address, + getAddressEncoder, + getBase64Decoder, + GetProgramAccountsMemcmpFilter, + pipe, +} from '@solana/kit'; +import { Argument } from 'commander'; +import { + AccountDiscriminator, + getAccountDiscriminatorEncoder, + PROGRAM_METADATA_PROGRAM_ADDRESS, +} from '../../generated'; +import { humanFileSize, logCommand, logTable } from '../logs'; +import { GlobalOptions } from '../options'; +import { addressParser } from '../parsers'; +import { CustomCommand, getKeyPairSigners, getReadonlyClient } from '../utils'; +import { ACCOUNT_HEADER_LENGTH } from '../../utils'; + +const DISCRIMINATOR_OFFSET = 0n; +const AUTHORITY_OFFSET = 33n; + +export function setListBuffersCommand(program: CustomCommand): void { + program + .command('list-buffers') + .description('List all buffer accounts owned by an authority.') + .addArgument( + new Argument('[authority]', 'Authority to list buffer accounts for.') + .default(undefined, 'keypair option') + .argParser(addressParser('authority')) + ) + .action(doListBuffers); +} + +type Options = {}; +export async function doListBuffers( + authorityArgument: Address | undefined, + _: Options, + cmd: CustomCommand +) { + const options = cmd.optsWithGlobals() as GlobalOptions & Options; + const client = getReadonlyClient(options); + const authority = + authorityArgument === undefined + ? (await getKeyPairSigners(options, client.configs))[0].address + : authorityArgument; + + logCommand(`Listing buffer accounts for...`, { authority }); + + const result = await client.rpc + .getProgramAccounts(PROGRAM_METADATA_PROGRAM_ADDRESS, { + dataSlice: { offset: 0, length: 0 }, + filters: [getDiscriminatorFilter(), getAuthorityFilter(authority)], + }) + .send(); + + if (result.length === 0) { + console.log('No buffer accounts found.'); + return; + } + + const content = result.map(({ pubkey, account }) => ({ + address: pubkey, + size: humanFileSize(Number(account.space) - ACCOUNT_HEADER_LENGTH), + })); + + logTable(content); +} + +function getDiscriminatorFilter(): GetProgramAccountsMemcmpFilter { + return { + memcmp: { + bytes: pipe( + AccountDiscriminator.Buffer, + getAccountDiscriminatorEncoder().encode, + getBase64Decoder().decode + ), + encoding: 'base64', + offset: DISCRIMINATOR_OFFSET, + }, + }; +} + +function getAuthorityFilter( + authority: Address +): GetProgramAccountsMemcmpFilter { + return { + memcmp: { + bytes: pipe( + authority, + getAddressEncoder().encode, + getBase64Decoder().decode + ), + encoding: 'base64', + offset: AUTHORITY_OFFSET, + }, + }; +} diff --git a/clients/js/src/cli/logs.ts b/clients/js/src/cli/logs.ts index 44cd95e..355b56a 100644 --- a/clients/js/src/cli/logs.ts +++ b/clients/js/src/cli/logs.ts @@ -1,3 +1,6 @@ +import { Console } from 'node:console'; +import { Transform } from 'node:stream'; + import picocolors from 'picocolors'; export function logCommand( @@ -38,3 +41,60 @@ export function logErrorAndExit(message: string): never { logError(message); process.exit(1); } + +/** @see https://stackoverflow.com/a/69874540/11440277 */ +export function logTable(tabularData: unknown) { + const ts = new Transform({ + transform(chunk, _, cb) { + cb(null, chunk); + }, + }); + const logger = new Console({ stdout: ts }); + logger.table(tabularData); + const table: string = (ts.read() || '').toString(); + let result = ''; + for (const row of table.split(/[\r\n]+/)) { + let r = row.replace(/[^┬]*┬/, '┌'); + r = r.replace(/^├─*┼/, '├'); + r = r.replace(/│[^│]*/, ''); + r = r.replace(/^└─*┴/, '└'); + r = r.replace(/'/g, ' '); + result += `${r}\n`; + } + console.log(result); +} + +/** + * Format bytes as human-readable text. + * @see https://stackoverflow.com/a/14919494/11440277 + * + * @param bytes Number of bytes. + * @param si True to use metric (SI) units, aka powers of 1000. False to use + * binary (IEC), aka powers of 1024. + * @param dp Number of decimal places to display. + * + * @return Formatted string. + */ +export function humanFileSize(bytes: number, si = false, dp = 1) { + const thresh = si ? 1000 : 1024; + + if (Math.abs(bytes) < thresh) { + return bytes + ' B'; + } + + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + const r = 10 ** dp; + + do { + bytes /= thresh; + ++u; + } while ( + Math.round(Math.abs(bytes) * r) / r >= thresh && + u < units.length - 1 + ); + + return bytes.toFixed(dp) + ' ' + units[u]; +} From 630df96a14d7dc93a2c4487289ff10af26e417a9 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 12:38:35 +0100 Subject: [PATCH 54/60] wip --- clients/js/src/cli/logs.ts | 12 ++++++++ clients/js/src/cli/utils.ts | 61 +++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/clients/js/src/cli/logs.ts b/clients/js/src/cli/logs.ts index 355b56a..8e17aaa 100644 --- a/clients/js/src/cli/logs.ts +++ b/clients/js/src/cli/logs.ts @@ -1,3 +1,4 @@ +import { Address } from '@solana/kit'; import { Console } from 'node:console'; import { Transform } from 'node:stream'; @@ -25,6 +26,17 @@ export function logCommand( console.log(''); } +export function logExports( + transactionLength: number, + authority?: Address +): void { + const transactionPluralized = + transactionLength === 1 ? 'transaction' : 'transactions'; + const forAuthority = authority ? ` for ${picocolors.bold(authority)}` : ''; + const message = `Exporting ${transactionLength} ${transactionPluralized}${forAuthority} in Base64:\n`; + console.log(picocolors.yellow(message)); +} + export function logSuccess(message: string): void { console.warn(picocolors.green(`[Success] `) + message); } diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index 1af499f..7704ff7 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -7,10 +7,12 @@ import { Address, address, Commitment, + compileTransaction, createKeyPairSignerFromBytes, createNoopSigner, createSolanaRpc, createSolanaRpcSubscriptions, + getBase64EncodedWireTransaction, MessageSigner, Rpc, RpcSubscriptions, @@ -19,15 +21,17 @@ import { TransactionSigner, } from '@solana/kit'; import { Command } from 'commander'; +import picocolors from 'picocolors'; import { parse as parseYaml } from 'yaml'; import { Buffer, DataSource, fetchBuffer, Format, Seed } from '../generated'; import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, + getAllSingleTransactionPlans, InstructionPlan, + TransactionPlan, TransactionPlanExecutor, TransactionPlanner, - TransactionPlanResult, } from '../instructionPlans'; import { getPdaDetails, PdaDetails } from '../internals'; import { @@ -36,7 +40,7 @@ import { packExternalData, packUrlData, } from '../packData'; -import { logErrorAndExit, logSuccess, logWarning } from './logs'; +import { logErrorAndExit, logExports, logSuccess, logWarning } from './logs'; import { ExportOption, GlobalOptions, @@ -66,9 +70,7 @@ export type Client = ReadonlyClient & { authority: TransactionSigner & MessageSigner; executor: TransactionPlanExecutor; payer: TransactionSigner & MessageSigner; - planAndExecute: ( - instructionPlan: InstructionPlan - ) => Promise; + planAndExecute: (instructionPlan: InstructionPlan) => Promise; planner: TransactionPlanner; }; @@ -87,13 +89,13 @@ export async function getClient(options: GlobalOptions): Promise { rpcSubscriptions: readonlyClient.rpcSubscriptions, parallelChunkSize: 5, }); - const planAndExecute = async ( - instructionPlan: InstructionPlan - ): Promise => { + const planAndExecute = async (instructionPlan: InstructionPlan) => { const transactionPlan = await planner(instructionPlan); - const result = await executor(transactionPlan); - logSuccess('Operation executed successfully'); - return result; + if (options.export) { + exportTransactionPlan(transactionPlan, options); + } else { + await executeTransactionPlan(transactionPlan, executor); + } }; return { ...readonlyClient, @@ -105,6 +107,33 @@ export async function getClient(options: GlobalOptions): Promise { }; } +function exportTransactionPlan( + transactionPlan: TransactionPlan, + options: GlobalOptions +) { + const singleTransactions = getAllSingleTransactionPlans(transactionPlan); + logExports( + singleTransactions.length, + typeof options.export === 'string' ? options.export : undefined + ); + + for (let i = 0; i < singleTransactions.length; i++) { + const transaction = compileTransaction(singleTransactions[i].message); + const encodedTransaction = getBase64EncodedWireTransaction(transaction); + const prefix = picocolors.yellow(`[Transaction #${i + 1}]`); + console.log(`${prefix}\n${encodedTransaction}\n`); + } +} + +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; @@ -162,16 +191,16 @@ export async function getKeyPairSigners( ): Promise< [TransactionSigner & MessageSigner, TransactionSigner & MessageSigner] > { + if (typeof options.export === 'string') { + const exportSigner = createNoopSigner(options.export); + return [exportSigner, exportSigner]; + } const keypairPath = getKeyPairPath(options, configs); const keypairPromise = getKeyPairSignerFromPath(keypairPath); const payerPromise = options.payer ? getKeyPairSignerFromPath(options.payer) : keypairPromise; - const [keypair, payer] = await Promise.all([keypairPromise, payerPromise]); - if (typeof options.export === 'string') { - return [createNoopSigner(options.export), payer]; - } - return [keypair, payer]; + return await Promise.all([keypairPromise, payerPromise]); } function getKeyPairPath( From a31bbd10aa97dab73e52d3f7482d32db57a453b1 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 15:05:20 +0100 Subject: [PATCH 55/60] Add recipient to --close-buffer option --- clients/js/src/cli/options.ts | 6 ++++-- clients/js/src/cli/utils.ts | 2 +- clients/js/src/createBuffer.ts | 10 +++++++--- clients/js/src/createMetadata.ts | 9 ++++++--- clients/js/src/updateBuffer.ts | 7 +++++-- clients/js/src/updateMetadata.ts | 16 +++++++++++----- clients/js/src/utils.ts | 5 ++--- 7 files changed, 36 insertions(+), 19 deletions(-) diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index 184eb58..702bf55 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -119,11 +119,13 @@ export const bufferOption = new Option( 'The address of the buffer to use as source (creates a "direct" data source).' ); -export type CloseBufferOption = { closeBuffer: boolean }; +export type CloseBufferOption = { closeBuffer: Address | boolean }; export const closeBufferOption = new Option( '--close-buffer', 'Whether to close provided `--buffer` account after using its data.' -).default(false); +) + .default(false) + .argParser(addressOrBooleanParser('--close-buffer recipient')); export type CompressionOption = { compression: Compression }; export const compressionOption = new Option( diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index 7704ff7..24835c7 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -261,7 +261,7 @@ export async function getWriteInput( PackedData & { buffer?: Account; format: Format; - closeBuffer?: boolean; + closeBuffer?: Address | boolean; } > { const buffer = options.buffer diff --git a/clients/js/src/createBuffer.ts b/clients/js/src/createBuffer.ts index 45bfeb8..46b5718 100644 --- a/clients/js/src/createBuffer.ts +++ b/clients/js/src/createBuffer.ts @@ -1,6 +1,7 @@ import { getCreateAccountInstruction } from '@solana-program/system'; import { Account, + Address, Lamports, ReadonlyUint8Array, TransactionSigner, @@ -24,7 +25,7 @@ export function getCreateBufferInstructionPlan(input: { authority: TransactionSigner; payer: TransactionSigner; sourceBuffer?: Account; - closeSourceBuffer?: boolean; + closeSourceBuffer?: Address | boolean; data?: ReadonlyUint8Array; rent: Lamports; }) { @@ -72,12 +73,15 @@ export function getCreateBufferInstructionPlan(input: { }), ]), ]), - ...(input.closeSourceBuffer && input.sourceBuffer + ...(input.sourceBuffer && input.closeSourceBuffer ? [ getCloseInstruction({ account: input.sourceBuffer.address, authority: input.authority, - destination: input.payer.address, + destination: + typeof input.closeSourceBuffer === 'string' + ? input.closeSourceBuffer + : input.payer.address, }), ] : []), diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 8adc732..5703cde 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -75,7 +75,7 @@ export async function getCreateMetadataInstructionPlan( payer: TransactionSigner; planner: TransactionPlanner; rpc: Rpc; - closeBuffer?: boolean; + closeBuffer?: Address | boolean; } ): Promise { if (!input.buffer && !input.data) { @@ -172,7 +172,7 @@ export function getCreateMetadataInstructionPlanUsingExistingBuffer( dataLength: number; payer: TransactionSigner; rent: Lamports; - closeBuffer?: boolean; + closeBuffer?: Address | boolean; } ) { return sequentialInstructionPlan([ @@ -215,7 +215,10 @@ export function getCreateMetadataInstructionPlanUsingExistingBuffer( getCloseInstruction({ account: input.buffer, authority: input.authority, - destination: input.payer.address, + destination: + typeof input.closeBuffer === 'string' + ? input.closeBuffer + : input.payer.address, program: input.program, programData: input.programData, }), diff --git a/clients/js/src/updateBuffer.ts b/clients/js/src/updateBuffer.ts index af628ac..d70ac23 100644 --- a/clients/js/src/updateBuffer.ts +++ b/clients/js/src/updateBuffer.ts @@ -26,7 +26,7 @@ export function getUpdateBufferInstructionPlan(input: { extraRent: Lamports; sizeDifference: number | bigint; sourceBuffer?: Account; - closeSourceBuffer?: boolean; + closeSourceBuffer?: Address | boolean; data?: ReadonlyUint8Array; }) { if (!input.data && !input.sourceBuffer) { @@ -89,7 +89,10 @@ export function getUpdateBufferInstructionPlan(input: { getCloseInstruction({ account: input.sourceBuffer.address, authority: input.authority, - destination: input.payer.address, + destination: + typeof input.closeSourceBuffer === 'string' + ? input.closeSourceBuffer + : input.payer.address, }), ] : []), diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index b09a635..e6e0507 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -80,7 +80,7 @@ export async function getUpdateMetadataInstructionPlan( payer: TransactionSigner; planner: TransactionPlanner; rpc: Rpc; - closeBuffer?: boolean; + closeBuffer?: Address | boolean; } ): Promise { if (!input.buffer && !input.data) { @@ -172,7 +172,7 @@ export function getUpdateMetadataInstructionPlanUsingInstructionData( export function getUpdateMetadataInstructionPlanUsingNewBuffer( input: Omit & { buffer: TransactionSigner; - closeBuffer?: boolean; + closeBuffer?: Address | boolean; data: ReadonlyUint8Array; extraRent: Lamports; fullRent: Lamports; @@ -218,7 +218,10 @@ export function getUpdateMetadataInstructionPlanUsingNewBuffer( getCloseInstruction({ account: input.buffer.address, authority: input.authority, - destination: input.payer.address, + destination: + typeof input.closeBuffer === 'string' + ? input.closeBuffer + : input.payer.address, program: input.program, programData: input.programData, }), @@ -241,7 +244,7 @@ export function getUpdateMetadataInstructionPlanUsingNewBuffer( export function getUpdateMetadataInstructionPlanUsingExistingBuffer( input: Omit & { buffer: Address; - closeBuffer?: boolean; + closeBuffer?: Address | boolean; extraRent: Lamports; fullRent: Lamports; payer: TransactionSigner; @@ -279,7 +282,10 @@ export function getUpdateMetadataInstructionPlanUsingExistingBuffer( getCloseInstruction({ account: input.buffer, authority: input.authority, - destination: input.payer.address, + destination: + typeof input.closeBuffer === 'string' + ? input.closeBuffer + : input.payer.address, program: input.program, programData: input.programData, }), diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 2fe69eb..4f97eac 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -62,11 +62,10 @@ export type MetadataInput = { priorityFees?: MicroLamports; /** * When using a buffer, whether to close the buffer account after the operation. - * This is only relevant when updating a metadata account since, when creating - * them, buffer accounts are transformed into metadata accounts. + * If an address is provided, it will be used as the destination for the close instruction. * Defaults to `true`. */ - closeBuffer?: boolean; + closeBuffer?: Address | boolean; }; export type MetadataResponse = { From abefa01d93ca527105cdaed51bd610c5afbbfe65 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 15:15:11 +0100 Subject: [PATCH 56/60] wip --- clients/js/src/cli/options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index 702bf55..5224c52 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -121,7 +121,7 @@ export const bufferOption = new Option( export type CloseBufferOption = { closeBuffer: Address | boolean }; export const closeBufferOption = new Option( - '--close-buffer', + '--close-buffer [address]', 'Whether to close provided `--buffer` account after using its data.' ) .default(false) From f0ce8c098c11d997ec118965e636b6339f5f4b69 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 15:26:04 +0100 Subject: [PATCH 57/60] Add --export-encoding option --- clients/js/src/cli/logs.ts | 16 +++++++++++++--- clients/js/src/cli/options.ts | 36 ++++++++++++++++++----------------- clients/js/src/cli/parsers.ts | 16 ++++++++++++++++ clients/js/src/cli/utils.ts | 15 +++++++++------ 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/clients/js/src/cli/logs.ts b/clients/js/src/cli/logs.ts index 8e17aaa..75e7f75 100644 --- a/clients/js/src/cli/logs.ts +++ b/clients/js/src/cli/logs.ts @@ -3,6 +3,7 @@ import { Console } from 'node:console'; import { Transform } from 'node:stream'; import picocolors from 'picocolors'; +import { Encoding } from '../generated'; export function logCommand( message: string, @@ -28,12 +29,21 @@ export function logCommand( export function logExports( transactionLength: number, - authority?: Address + options: { export: Address | boolean; exportEncoding: Encoding } ): void { const transactionPluralized = transactionLength === 1 ? 'transaction' : 'transactions'; - const forAuthority = authority ? ` for ${picocolors.bold(authority)}` : ''; - const message = `Exporting ${transactionLength} ${transactionPluralized}${forAuthority} in Base64:\n`; + const forAuthority = + typeof options.export === 'string' + ? ` for ${picocolors.bold(options.export)}` + : ''; + const encodingName = { + [Encoding.None]: 'hexadecimal', + [Encoding.Utf8]: 'UTF-8', + [Encoding.Base58]: 'Base58', + [Encoding.Base64]: 'Base64', + }[options.exportEncoding]; + const message = `Exporting ${transactionLength} ${transactionPluralized}${forAuthority} in ${encodingName}:\n`; console.log(picocolors.yellow(message)); } diff --git a/clients/js/src/cli/options.ts b/clients/js/src/cli/options.ts index 5224c52..2749027 100644 --- a/clients/js/src/cli/options.ts +++ b/clients/js/src/cli/options.ts @@ -2,14 +2,19 @@ import { type Address, type MicroLamports } from '@solana/kit'; import { Option } from 'commander'; import { Compression, Encoding, Format } from '../generated'; import { logErrorAndExit } from './logs'; -import { addressOrBooleanParser, addressParser } from './parsers'; +import { + addressOrBooleanParser, + addressParser, + encodingParser, +} from './parsers'; import { CustomCommand } from './utils'; export type GlobalOptions = KeypairOption & PayerOption & PriorityFeesOption & RpcOption & - ExportOption; + ExportOption & + ExportEncodingOption; export function setGlobalOptions(command: CustomCommand) { command @@ -17,7 +22,8 @@ export function setGlobalOptions(command: CustomCommand) { .addOption(payerOption) .addOption(priorityFeesOption) .addOption(rpcOption) - .addOption(exportOption); + .addOption(exportOption) + .addOption(exportEncodingOption); } export type KeypairOption = { keypair?: string }; @@ -56,6 +62,15 @@ export const exportOption = new Option( .default(false) .argParser(addressOrBooleanParser('export')); +export type ExportEncodingOption = { exportEncoding: Encoding }; +export const exportEncodingOption = new Option( + '--export-encoding ', + 'Describes how to encode exported transactions.' +) + .choices(['none', 'utf8', 'base58', 'base64']) + .default(Encoding.Base64, 'base64') + .argParser(encodingParser); + export type WriteOptions = TextOption & UrlOption & AccountOption & @@ -154,20 +169,7 @@ export const encodingOption = new Option( ) .choices(['none', 'utf8', 'base58', 'base64']) .default(Encoding.Utf8, 'utf8') - .argParser((value: string): Encoding => { - switch (value) { - case 'none': - return Encoding.None; - case 'utf8': - return Encoding.Utf8; - case 'base58': - return Encoding.Base58; - case 'base64': - return Encoding.Base64; - default: - logErrorAndExit(`Invalid encoding option: ${value}`); - } - }); + .argParser(encodingParser); export type FormatOption = { format?: Format }; export const formatOption = new Option( diff --git a/clients/js/src/cli/parsers.ts b/clients/js/src/cli/parsers.ts index 0405924..89e94a8 100644 --- a/clients/js/src/cli/parsers.ts +++ b/clients/js/src/cli/parsers.ts @@ -1,5 +1,6 @@ import { Address, address } from '@solana/kit'; import { logErrorAndExit } from './logs'; +import { Encoding } from '../generated'; export const addressParser = (identifier: string) => @@ -17,3 +18,18 @@ export const addressOrBooleanParser = if (value === undefined) return true; return addressParser(identifier)(value); }; + +export const encodingParser = (value: string): Encoding => { + switch (value) { + case 'none': + return Encoding.None; + case 'utf8': + return Encoding.Utf8; + case 'base58': + return Encoding.Base58; + case 'base64': + return Encoding.Base64; + default: + logErrorAndExit(`Invalid encoding option: ${value}`); + } +}; diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index 24835c7..6df1e7b 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -12,7 +12,7 @@ import { createNoopSigner, createSolanaRpc, createSolanaRpcSubscriptions, - getBase64EncodedWireTransaction, + getTransactionEncoder, MessageSigner, Rpc, RpcSubscriptions, @@ -35,6 +35,7 @@ import { } from '../instructionPlans'; import { getPdaDetails, PdaDetails } from '../internals'; import { + decodeData, packDirectData, PackedData, packExternalData, @@ -112,14 +113,16 @@ function exportTransactionPlan( options: GlobalOptions ) { const singleTransactions = getAllSingleTransactionPlans(transactionPlan); - logExports( - singleTransactions.length, - typeof options.export === 'string' ? options.export : undefined - ); + const transactionEncoder = getTransactionEncoder(); + + logExports(singleTransactions.length, options); for (let i = 0; i < singleTransactions.length; i++) { const transaction = compileTransaction(singleTransactions[i].message); - const encodedTransaction = getBase64EncodedWireTransaction(transaction); + const encodedTransaction = decodeData( + transactionEncoder.encode(transaction), + options.exportEncoding + ); const prefix = picocolors.yellow(`[Transaction #${i + 1}]`); console.log(`${prefix}\n${encodedTransaction}\n`); } From e2e13c7c8d75b59d492f5b7928d261500e96a69f Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 17:13:30 +0100 Subject: [PATCH 58/60] Clean up exported messages --- clients/js/src/cli/utils.ts | 31 +++++++++++++++++-- .../instructionPlans/computeBudgetHelpers.ts | 2 +- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/clients/js/src/cli/utils.ts b/clients/js/src/cli/utils.ts index 6df1e7b..b5517bc 100644 --- a/clients/js/src/cli/utils.ts +++ b/clients/js/src/cli/utils.ts @@ -7,6 +7,7 @@ import { Address, address, Commitment, + CompilableTransactionMessage, compileTransaction, createKeyPairSignerFromBytes, createNoopSigner, @@ -14,8 +15,10 @@ import { createSolanaRpcSubscriptions, getTransactionEncoder, MessageSigner, + pipe, Rpc, RpcSubscriptions, + setTransactionMessageLifetimeUsingBlockhash, SolanaRpcApi, SolanaRpcSubscriptionsApi, TransactionSigner, @@ -28,6 +31,7 @@ import { createDefaultTransactionPlanExecutor, createDefaultTransactionPlanner, getAllSingleTransactionPlans, + getSetComputeUnitLimitInstructionIndex, InstructionPlan, TransactionPlan, TransactionPlanExecutor, @@ -93,7 +97,7 @@ export async function getClient(options: GlobalOptions): Promise { const planAndExecute = async (instructionPlan: InstructionPlan) => { const transactionPlan = await planner(instructionPlan); if (options.export) { - exportTransactionPlan(transactionPlan, options); + await exportTransactionPlan(transactionPlan, readonlyClient, options); } else { await executeTransactionPlan(transactionPlan, executor); } @@ -108,8 +112,9 @@ export async function getClient(options: GlobalOptions): Promise { }; } -function exportTransactionPlan( +async function exportTransactionPlan( transactionPlan: TransactionPlan, + client: Pick, options: GlobalOptions ) { const singleTransactions = getAllSingleTransactionPlans(transactionPlan); @@ -117,8 +122,17 @@ function exportTransactionPlan( logExports(singleTransactions.length, options); + const { value: latestBlockhash } = await client.rpc + .getLatestBlockhash() + .send(); + for (let i = 0; i < singleTransactions.length; i++) { - const transaction = compileTransaction(singleTransactions[i].message); + const transaction = pipe( + singleTransactions[i].message, + (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), + removeComputeUnitLimitInstruction, + compileTransaction + ); const encodedTransaction = decodeData( transactionEncoder.encode(transaction), options.exportEncoding @@ -128,6 +142,17 @@ function exportTransactionPlan( } } +function removeComputeUnitLimitInstruction< + TTransactionMessage extends CompilableTransactionMessage, +>(message: TTransactionMessage): TTransactionMessage { + const index = getSetComputeUnitLimitInstructionIndex(message); + if (index === -1) return message; + return { + ...message, + instructions: message.instructions.filter((_, i) => i !== index), + }; +} + async function executeTransactionPlan( transactionPlan: TransactionPlan, executor: TransactionPlanExecutor diff --git a/clients/js/src/instructionPlans/computeBudgetHelpers.ts b/clients/js/src/instructionPlans/computeBudgetHelpers.ts index f7f8368..4627d47 100644 --- a/clients/js/src/instructionPlans/computeBudgetHelpers.ts +++ b/clients/js/src/instructionPlans/computeBudgetHelpers.ts @@ -125,7 +125,7 @@ function getSetComputeUnitLimitInstructionIndexAndUnits( return { index, units }; } -function getSetComputeUnitLimitInstructionIndex( +export function getSetComputeUnitLimitInstructionIndex( transactionMessage: BaseTransactionMessage ) { return transactionMessage.instructions.findIndex((ix) => { From 69134e3f45d4ad84c7e343f2faac85debd269aed Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Wed, 16 Apr 2025 17:28:26 +0100 Subject: [PATCH 59/60] Remove unnecessary accounts to close buffer ixs --- clients/js/src/createMetadata.ts | 2 -- clients/js/src/updateMetadata.ts | 4 ---- 2 files changed, 6 deletions(-) diff --git a/clients/js/src/createMetadata.ts b/clients/js/src/createMetadata.ts index 5703cde..489a110 100644 --- a/clients/js/src/createMetadata.ts +++ b/clients/js/src/createMetadata.ts @@ -219,8 +219,6 @@ export function getCreateMetadataInstructionPlanUsingExistingBuffer( typeof input.closeBuffer === 'string' ? input.closeBuffer : input.payer.address, - program: input.program, - programData: input.programData, }), ] : []), diff --git a/clients/js/src/updateMetadata.ts b/clients/js/src/updateMetadata.ts index e6e0507..a7af747 100644 --- a/clients/js/src/updateMetadata.ts +++ b/clients/js/src/updateMetadata.ts @@ -222,8 +222,6 @@ export function getUpdateMetadataInstructionPlanUsingNewBuffer( typeof input.closeBuffer === 'string' ? input.closeBuffer : input.payer.address, - program: input.program, - programData: input.programData, }), ] : []), @@ -286,8 +284,6 @@ export function getUpdateMetadataInstructionPlanUsingExistingBuffer( typeof input.closeBuffer === 'string' ? input.closeBuffer : input.payer.address, - program: input.program, - programData: input.programData, }), ] : []), From 32270dea450ceb31aceb379f1f8fcc822eb145c3 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 17 Apr 2025 10:24:37 +0100 Subject: [PATCH 60/60] Mention authority change in CLI log --- clients/js/src/cli/commands/create-buffer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clients/js/src/cli/commands/create-buffer.ts b/clients/js/src/cli/commands/create-buffer.ts index 58bcb0b..a4d5ffd 100644 --- a/clients/js/src/cli/commands/create-buffer.ts +++ b/clients/js/src/cli/commands/create-buffer.ts @@ -28,7 +28,10 @@ export async function doCreateBuffer( getWriteInput(client, file, options), ]); - logCommand(`Creating new buffer...`, { buffer: buffer.address }); + logCommand(`Creating new buffer and setting authority...`, { + buffer: buffer.address, + authority: client.authority.address, + }); const data = writeInput.buffer?.data.data ?? writeInput.data; const rent = await client.rpc