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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ Using a buffer account you can split the metadata update into the uploading of t
- `--priority-fees <number>`: Priority fees in micro-lamports per compute unit (default: 100000)
- `--rpc <string>`: Custom RPC URL
- `--export [address]`: Export transactions instead of running them. Optionally specify an override authority address.
- `--export-encoding <encoding>`: How to encode exported transactions. Choices: none, utf8, base58, base64 (default: base64)
- `--export-encoding <encoding>`: How to encode exported transactions. Choices: none, utf8, base58, base64, instruction-list (default: base64)
- `--single-extend-per-tx`: Never grow an account by more than 10KB within a single exported transaction. Required when the exported transactions are executed through a CPI, e.g. by a multisig such as Squads. Requires `--export`.
- `--tx-version <version>`: Transaction version to build. Choices: legacy, 0 (default: 0)
- `-h, --help`: Show help for command

Expand Down Expand Up @@ -150,7 +151,13 @@ Squads v3 only accepts legacy transactions. If your multisig is on Squads v3, ad
npx @solana-program/program-metadata@latest write idl <program-address> --buffer <buffer-address> --export <multisig-address> --export-encoding base58 --close-buffer <your-address-to-get-the-buffer-rent-back> --tx-version legacy
```

4. Sign the transaction in your multisig and send it
If the metadata account needs to grow by more than 10KB — because it is being created with more than 10KB of data, or updated with more than 10KB of additional data — add `--single-extend-per-tx`. Multisigs execute exported transactions through a CPI and the Solana runtime caps the growth of an account at 10KB per top-level instruction. Therefore, an exported transaction cannot resize an account by more than 10KB. This option spreads the growth over several transactions accordingly, so expect more than one transaction to import:

```bash
npx @solana-program/program-metadata@latest write idl <program-address> --buffer <buffer-address> --export <multisig-address> --export-encoding base58 --close-buffer <your-address-to-get-the-buffer-rent-back> --single-extend-per-tx
```

4. Sign the transaction(s) in your multisig and send them in order

### Examples

Expand Down
1 change: 1 addition & 0 deletions clients/js/src/cli/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export async function doCreate(seed: Seed, program: Address, file: string | unde
programData,
seed,
metadata,
singleExtendPerTransaction: options.singleExtendPerTx,
}),
);
}
1 change: 1 addition & 0 deletions clients/js/src/cli/commands/update-buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export async function doUpdateBuffer(buffer: Address, file: string | undefined,
sourceBuffer: writeInput.buffer,
closeSourceBuffer: writeInput.closeBuffer,
data: newData,
singleExtendPerTransaction: options.singleExtendPerTx,
}),
);
}
1 change: 1 addition & 0 deletions clients/js/src/cli/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export async function doWrite(seed: Seed, program: Address, file: string | undef
program,
programData,
metadata: metadataAccount,
singleExtendPerTransaction: options.singleExtendPerTx,
}),
);
}
1 change: 1 addition & 0 deletions clients/js/src/cli/commands/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export async function doWrite(seed: Seed, program: Address, file: string | undef
programData,
seed,
metadata: metadataAccount,
singleExtendPerTransaction: options.singleExtendPerTx,
}),
);
}
11 changes: 11 additions & 0 deletions clients/js/src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type GlobalOptions = KeypairOption &
RpcOption &
ExportOption &
ExportEncodingOption &
SingleExtendPerTxOption &
TransactionVersionOption;

export function setGlobalOptions(command: CustomCommand) {
Expand All @@ -22,6 +23,7 @@ export function setGlobalOptions(command: CustomCommand) {
.addOption(rpcOption)
.addOption(exportOption)
.addOption(exportEncodingOption)
.addOption(singleExtendPerTxOption)
.addOption(transactionVersionOption);
}

Expand Down Expand Up @@ -68,6 +70,15 @@ export const exportEncodingOption = new Option(
(value: string): ExportEncoding => (value === 'instruction-list' ? 'instruction-list' : encodingParser(value)),
);

export type SingleExtendPerTxOption = { singleExtendPerTx: boolean };
export const singleExtendPerTxOption = new Option(
'--single-extend-per-tx',
'Never grow an account by more than 10KB within a single exported transaction (at most one "extend" instruction per transaction). ' +
'Required when the exported transactions are executed through a CPI, e.g. by a multisig program such as Squads, ' +
'where the whole transaction runs as a single top-level instruction and is therefore subject to the 10KB realloc limit ' +
'that applies per top-level instruction. Requires "--export".',
).default(false);

export type TransactionVersion = 'legacy' | 0;
export type TransactionVersionOption = { txVersion: TransactionVersion };
export const transactionVersionOption = new Option(
Expand Down
14 changes: 14 additions & 0 deletions clients/js/src/cli/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
NonCanonicalWriteOption,
PayerOption,
RpcOption,
SingleExtendPerTxOption,
WriteOptions,
} from './options';
import { createRetryingSolanaRpc, RetryingRpcConfig } from './rpc';
Expand Down Expand Up @@ -85,6 +86,7 @@ export class CustomCommand extends Command {
export type Client = Awaited<ReturnType<typeof getClient>>;

export async function getClient(options: GlobalOptions) {
assertValidExportOptions(options);
const configs = getSolanaConfigs();
const rpcUrl = getRpcUrl(options, configs);
const rpcSubscriptionsUrl = getRpcSubscriptionsUrl(rpcUrl, configs);
Expand All @@ -111,6 +113,18 @@ export async function getClient(options: GlobalOptions) {
.use(cliRunOrExport(options));
}

/**
* Rejects option combinations that only make sense when exporting
* transactions. When instructions are executed directly by the CLI, they run
* as top-level instructions in a transaction, so `--single-extend-per-tx`
* would only add needless transactions.
*/
function assertValidExportOptions(options: ExportOption & SingleExtendPerTxOption): void {
if (options.singleExtendPerTx && !options.export) {
logErrorAndExit('The `--single-extend-per-tx` option can only be used together with `--export`.');
}
}

/**
* Shared configuration for the CLI's retrying RPC. Surfaces a warning whenever a
* request is rate limited and retried, so a paused command does not appear to
Expand Down
9 changes: 6 additions & 3 deletions clients/js/src/createBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import {
PROGRAM_METADATA_PROGRAM_ADDRESS,
SeedArgs,
} from './generated';
import { REALLOC_LIMIT } from './internals';
import { getAccountSize, getExtendInstructionPlan, getWriteInstructionPlan } from './utils';
import { getAccountSize, getExtendInstructionPlan, getWriteInstructionPlan, needsExtend } from './utils';

/**
* Builds a plan that creates a brand new buffer account owned by a fresh
Expand Down Expand Up @@ -120,6 +119,7 @@ export async function getCreateCanonicalBufferInstructionPlan(
program: Address;
programData: Address;
seed: SeedArgs;
singleExtendPerTransaction?: boolean;
},
) {
const buffer = input.buffer ?? (await findCanonicalPda({ program: input.program, seed: input.seed }))[0];
Expand Down Expand Up @@ -151,6 +151,7 @@ export async function getCreateNonCanonicalBufferInstructionPlan(
payer: TransactionSigner;
program: Address;
seed: SeedArgs;
singleExtendPerTransaction?: boolean;
},
) {
const buffer =
Expand All @@ -177,6 +178,7 @@ async function getPdaBufferInstructionPlan(
program: Address;
programData?: Address;
seed: SeedArgs;
singleExtendPerTransaction?: boolean;
},
) {
const dataLength = input.dataLength ?? input.data?.length ?? 0;
Expand All @@ -194,14 +196,15 @@ async function getPdaBufferInstructionPlan(
programData: input.programData,
seed: input.seed,
}),
...(dataLength > REALLOC_LIMIT
...(needsExtend(dataLength)
? [
getExtendInstructionPlan({
account: input.buffer,
authority: input.authority,
extraLength: dataLength,
program: input.program,
programData: input.programData,
singleExtendPerTransaction: input.singleExtendPerTransaction,
}),
]
: []),
Expand Down
12 changes: 9 additions & 3 deletions clients/js/src/createMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ import {
InitializeInput,
PROGRAM_METADATA_PROGRAM_ADDRESS,
} from './generated';
import { isValidInstructionPlan, REALLOC_LIMIT } from './internals';
import { isValidInstructionPlan } from './internals';
import {
getAccountSize,
getExtendInstructionPlan,
getWriteInstructionPlan,
MetadataInput,
needsExtend,
resolveMetadataPda,
} from './utils';

Expand Down Expand Up @@ -66,6 +67,7 @@ export async function getCreateMetadataInstructionPlan(
data?: ReadonlyUint8Array;
payer: TransactionSigner;
closeBuffer?: Address | boolean;
singleExtendPerTransaction?: boolean;
},
): Promise<InstructionPlan> {
if (!input.buffer && !input.data) {
Expand Down Expand Up @@ -115,6 +117,7 @@ export async function getCreateMetadataInstructionPlanUsingNewBuffer(
input: Omit<InitializeInput, 'data'> & {
data: ReadonlyUint8Array;
payer: TransactionSigner;
singleExtendPerTransaction?: boolean;
},
) {
const rent = await client.getMinimumBalance(Number(getAccountSize(input.data.length)));
Expand All @@ -131,14 +134,15 @@ export async function getCreateMetadataInstructionPlanUsingNewBuffer(
programData: input.programData,
seed: input.seed,
}),
...(input.data.length > REALLOC_LIMIT
...(needsExtend(input.data.length)
? [
getExtendInstructionPlan({
account: input.metadata,
authority: input.authority,
extraLength: input.data.length,
program: input.program,
programData: input.programData,
singleExtendPerTransaction: input.singleExtendPerTransaction,
}),
]
: []),
Expand All @@ -164,6 +168,7 @@ export async function getCreateMetadataInstructionPlanUsingExistingBuffer(
dataLength: number;
payer: TransactionSigner;
closeBuffer?: Address | boolean;
singleExtendPerTransaction?: boolean;
},
) {
const rent = await client.getMinimumBalance(Number(getAccountSize(input.dataLength)));
Expand All @@ -180,14 +185,15 @@ export async function getCreateMetadataInstructionPlanUsingExistingBuffer(
programData: input.programData,
seed: input.seed,
}),
...(input.dataLength > REALLOC_LIMIT
...(needsExtend(input.dataLength)
? [
getExtendInstructionPlan({
account: input.metadata,
authority: input.authority,
extraLength: input.dataLength,
program: input.program,
programData: input.programData,
singleExtendPerTransaction: input.singleExtendPerTransaction,
Comment thread
lorisleiva marked this conversation as resolved.
}),
]
: []),
Expand Down
2 changes: 0 additions & 2 deletions clients/js/src/internals.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { ClientWithTransactionPlanning, InstructionPlan } from '@solana/kit';

export const REALLOC_LIMIT = 10_240;

/**
* Returns `true` if the given instruction plan can be planned by the client's
* transaction planner without throwing — i.e. it fits within a single
Expand Down
5 changes: 3 additions & 2 deletions clients/js/src/updateBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import {
} from '@solana/kit';

import { Buffer, getCloseInstruction, getTrimInstruction, getWriteInstruction } from './generated';
import { REALLOC_LIMIT } from './internals';
import { getExtendInstructionPlan, getWriteInstructionPlan } from './utils';
import { getExtendInstructionPlan, getWriteInstructionPlan, REALLOC_LIMIT } from './utils';

export async function getUpdateBufferInstructionPlan(
client: ClientWithGetMinimumBalance,
Expand All @@ -24,6 +23,7 @@ export async function getUpdateBufferInstructionPlan(
sourceBuffer?: Account<Buffer>;
closeSourceBuffer?: Address | boolean;
data?: ReadonlyUint8Array;
singleExtendPerTransaction?: boolean;
},
) {
if (!input.data && !input.sourceBuffer) {
Expand Down Expand Up @@ -52,6 +52,7 @@ export async function getUpdateBufferInstructionPlan(
account: input.buffer,
authority: input.authority,
extraLength: Number(input.sizeDifference),
singleExtendPerTransaction: input.singleExtendPerTransaction,
}),
]
: []),
Expand Down
9 changes: 7 additions & 2 deletions clients/js/src/updateMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import {
Metadata,
SetDataInput,
} from './generated';
import { isValidInstructionPlan, REALLOC_LIMIT } from './internals';
import { getExtendInstructionPlan, MetadataInput, resolveMetadataPda } from './utils';
import { isValidInstructionPlan } from './internals';
import { getExtendInstructionPlan, MetadataInput, REALLOC_LIMIT, resolveMetadataPda } from './utils';

type UpdateMetadataClient = ClientWithGetMinimumBalance &
ClientWithRpc<GetAccountInfoApi> &
Expand Down Expand Up @@ -70,6 +70,7 @@ export async function getUpdateMetadataInstructionPlan(
data?: ReadonlyUint8Array;
payer: TransactionSigner;
closeBuffer?: Address | boolean;
singleExtendPerTransaction?: boolean;
},
): Promise<InstructionPlan> {
if (!input.buffer && !input.data) {
Expand Down Expand Up @@ -149,6 +150,7 @@ export async function getUpdateMetadataInstructionPlanUsingNewBuffer(
data: ReadonlyUint8Array;
metadata: Account<Metadata>;
payer: TransactionSigner;
singleExtendPerTransaction?: boolean;
},
) {
const sizeDifference = BigInt(input.data.length) - BigInt(input.metadata.data.data.length);
Expand All @@ -171,6 +173,7 @@ export async function getUpdateMetadataInstructionPlanUsingNewBuffer(
extraLength: Number(sizeDifference),
program: input.program,
programData: input.programData,
singleExtendPerTransaction: input.singleExtendPerTransaction,
}),
]
: []),
Expand Down Expand Up @@ -217,6 +220,7 @@ export async function getUpdateMetadataInstructionPlanUsingExistingBuffer(
dataLength: number;
metadata: Account<Metadata>;
payer: TransactionSigner;
singleExtendPerTransaction?: boolean;
},
) {
const sizeDifference = BigInt(input.dataLength) - BigInt(input.metadata.data.data.length);
Expand All @@ -239,6 +243,7 @@ export async function getUpdateMetadataInstructionPlanUsingExistingBuffer(
extraLength: Number(sizeDifference),
program: input.program,
programData: input.programData,
singleExtendPerTransaction: input.singleExtendPerTransaction,
}),
]
: []),
Expand Down
Loading
Loading