From f534476dabdd7bea99253dd1ce541fc63f41941b Mon Sep 17 00:00:00 2001 From: catmcgee Date: Mon, 29 Jun 2026 15:25:39 -0300 Subject: [PATCH] Add resource limit estimation opt-out --- .changeset/quiet-lemons-allow.md | 5 ++ packages/kit-plugin-rpc/README.md | 5 ++ packages/kit-plugin-rpc/src/index.ts | 1 + .../src/resource-limit-estimation.ts | 9 ++++ packages/kit-plugin-rpc/src/solana-rpc.ts | 27 ++++++++-- .../src/transaction-plan-executor.ts | 13 ++++- .../kit-plugin-rpc/src/transaction-planner.ts | 13 ++++- packages/kit-plugin-rpc/test/index.test.ts | 54 ++++++++++++++++++- .../test/transaction-plan-executor.test.ts | 33 ++++++++++++ .../test/transaction-planner.test.ts | 25 +++++++++ 10 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 .changeset/quiet-lemons-allow.md create mode 100644 packages/kit-plugin-rpc/src/resource-limit-estimation.ts diff --git a/.changeset/quiet-lemons-allow.md b/.changeset/quiet-lemons-allow.md new file mode 100644 index 00000000..537d2962 --- /dev/null +++ b/.changeset/quiet-lemons-allow.md @@ -0,0 +1,5 @@ +--- +"@solana/kit-plugin-rpc": patch +--- + +Add an opt-out for automatic resource-limit estimation in the RPC transaction planner and executor. diff --git a/packages/kit-plugin-rpc/README.md b/packages/kit-plugin-rpc/README.md index dd1f0c87..8f9ac830 100644 --- a/packages/kit-plugin-rpc/README.md +++ b/packages/kit-plugin-rpc/README.md @@ -44,6 +44,7 @@ All options are provided via a `SolanaRpcConfig` object: - `transactionConfig`: Options to configure how transaction messages are created. See `rpcTransactionPlanner` options below. - `maxConcurrency`: Maximum number of concurrent transaction executions. Defaults to 10. - `skipPreflight`: Whether to always skip preflight simulation. Defaults to `false`. +- `resourceLimitEstimation`: Whether to reserve, estimate, and set resource limits. Accepts `'estimate'` or `'none'`. Defaults to `'estimate'`. ### Features @@ -246,6 +247,7 @@ All options are provided via a `TransactionPlannerConfig` object: - `version`: The transaction message version to use. Accepts `0` or `'legacy'`. Defaults to `0`. - `microLamportsPerComputeUnit`: Priority fees in micro lamports per compute unit. Defaults to no priority fees. +- `resourceLimitEstimation`: Whether to reserve space for resource limits that the executor can later estimate and set. Accepts `'estimate'` or `'none'`. Defaults to `'estimate'`. ### Features @@ -278,6 +280,7 @@ const client = await createClient() - `maxConcurrency`: Maximum number of concurrent executions (default: 10). - `skipPreflight`: Whether to skip the preflight simulation when sending transactions (default: `false`). +- `resourceLimitEstimation`: Whether to estimate and set missing resource limits before sending. Accepts `'estimate'` or `'none'`. Defaults to `'estimate'`. ### Features @@ -301,6 +304,8 @@ Setting `skipPreflight: true` changes the behavior: | Estimation fails | Throw | Use consumed resources, skip preflight | | Explicit limits set | Run preflight | Skip preflight | +Set `resourceLimitEstimation: 'none'` to opt out of automatic resource-limit handling. The RPC planner will not reserve provisional resource-limit instructions, and the RPC executor will not simulate to estimate or inject missing limits. Explicit resource-limit instructions already present in a transaction message are preserved. This can be useful for transactions that are close to the message size limit, where adding a compute-budget instruction would make an otherwise valid transaction too large. + ## Deprecated plugins The following plugins are still exported for backward compatibility but are deprecated. Prefer `solanaRpcConnection` for new code. diff --git a/packages/kit-plugin-rpc/src/index.ts b/packages/kit-plugin-rpc/src/index.ts index 42fc195a..e13d2a50 100644 --- a/packages/kit-plugin-rpc/src/index.ts +++ b/packages/kit-plugin-rpc/src/index.ts @@ -1,5 +1,6 @@ export * from './airdrop'; export * from './get-minimum-balance'; +export * from './resource-limit-estimation'; export * from './rpc'; export * from './solana-rpc'; export * from './transaction-plan-executor'; diff --git a/packages/kit-plugin-rpc/src/resource-limit-estimation.ts b/packages/kit-plugin-rpc/src/resource-limit-estimation.ts new file mode 100644 index 00000000..9389ee4c --- /dev/null +++ b/packages/kit-plugin-rpc/src/resource-limit-estimation.ts @@ -0,0 +1,9 @@ +/** + * Controls whether the RPC transaction planner and executor automatically + * reserve, estimate, and set transaction resource limits. + */ +export type ResourceLimitEstimationMode = 'estimate' | 'none'; + +export function shouldEstimateResourceLimits(mode: ResourceLimitEstimationMode | undefined): boolean { + return mode !== 'none'; +} diff --git a/packages/kit-plugin-rpc/src/solana-rpc.ts b/packages/kit-plugin-rpc/src/solana-rpc.ts index 535df0ee..a6564ac4 100644 --- a/packages/kit-plugin-rpc/src/solana-rpc.ts +++ b/packages/kit-plugin-rpc/src/solana-rpc.ts @@ -18,6 +18,7 @@ import { planAndSendTransactions } from '@solana/kit-plugin-instruction-plan'; import { rpcAirdrop } from './airdrop'; import { rpcGetMinimumBalance } from './get-minimum-balance'; +import { type ResourceLimitEstimationMode } from './resource-limit-estimation'; import { rpcSubscriptionsConnection } from './rpc'; import { rpcTransactionPlanExecutor } from './transaction-plan-executor'; import { rpcTransactionPlanner, TransactionPlannerConfig } from './transaction-planner'; @@ -76,6 +77,14 @@ export type SolanaRpcConfig = Solan * Defaults to `false`. */ skipPreflight?: boolean; + /** + * Whether to reserve, estimate, and set transaction resource limits. + * Set to `none` for transactions where automatic compute-budget + * instructions can push the message over the size limit. + * + * Defaults to `estimate`. + */ + resourceLimitEstimation?: ResourceLimitEstimationMode; /** * Options to configure how transaction messages are created such as * choosing a transaction version or setting priority fees. @@ -111,15 +120,25 @@ export type SolanaRpcConfig = Solan * @see {@link solanaLocalRpc} */ export function solanaRpc(config: SolanaRpcConfig) { - return (client: T) => - pipe( + return (client: T) => { + const resourceLimitEstimation = + config.resourceLimitEstimation ?? config.transactionConfig?.resourceLimitEstimation; + return pipe( client, solanaRpcConnection(config), rpcGetMinimumBalance(), - rpcTransactionPlanner(config.transactionConfig), - rpcTransactionPlanExecutor({ maxConcurrency: config.maxConcurrency, skipPreflight: config.skipPreflight }), + rpcTransactionPlanner({ + ...config.transactionConfig, + resourceLimitEstimation, + }), + rpcTransactionPlanExecutor({ + maxConcurrency: config.maxConcurrency, + resourceLimitEstimation, + skipPreflight: config.skipPreflight, + }), planAndSendTransactions(), ); + }; } /** diff --git a/packages/kit-plugin-rpc/src/transaction-plan-executor.ts b/packages/kit-plugin-rpc/src/transaction-plan-executor.ts index a7f2c40c..05c034f5 100644 --- a/packages/kit-plugin-rpc/src/transaction-plan-executor.ts +++ b/packages/kit-plugin-rpc/src/transaction-plan-executor.ts @@ -23,6 +23,8 @@ import { TransactionPlanExecutorConfig, } from '@solana/kit'; +import { type ResourceLimitEstimationMode, shouldEstimateResourceLimits } from './resource-limit-estimation'; + /** * A plugin that provides a default transaction plan executor using RPC. * @@ -71,6 +73,14 @@ export function rpcTransactionPlanExecutor( * Defaults to `false`. */ skipPreflight?: boolean; + /** + * Whether to estimate and set missing resource limits before sending. + * Set to `none` for transactions where injecting a compute-budget + * instruction can push the message over the size limit. + * + * Defaults to `estimate`. + */ + resourceLimitEstimation?: ResourceLimitEstimationMode; } = {}, ) { return < @@ -98,6 +108,7 @@ export function rpcTransactionPlanExecutor( }); const estimateResourceLimits = estimateResourceLimitsFactory({ rpc: client.rpc }); const skipPreflight = config.skipPreflight ?? false; + const shouldEstimateResources = shouldEstimateResourceLimits(config.resourceLimitEstimation); const transactionPlanExecutor = createTransactionPlanExecutor({ executeTransactionMessage: limitFunction(async (context, transactionMessage, executorConfig) => { @@ -118,7 +129,7 @@ export function rpcTransactionPlanExecutor( transactionMessage, tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), tx => (context.message = tx), - async tx => await estimateAndSetResourceLimits(tx, executorConfig), + async tx => (shouldEstimateResources ? await estimateAndSetResourceLimits(tx, executorConfig) : tx), async tx => (context.message = await tx), async tx => await signTransactionMessageWithSigners(await tx, executorConfig), async tx => (context.transaction = await tx), diff --git a/packages/kit-plugin-rpc/src/transaction-planner.ts b/packages/kit-plugin-rpc/src/transaction-planner.ts index 314c5f24..404cce08 100644 --- a/packages/kit-plugin-rpc/src/transaction-planner.ts +++ b/packages/kit-plugin-rpc/src/transaction-planner.ts @@ -10,6 +10,8 @@ import { setTransactionMessageFeePayerSigner, } from '@solana/kit'; +import { type ResourceLimitEstimationMode, shouldEstimateResourceLimits } from './resource-limit-estimation'; + /** * A plugin that provides a default transaction planner using RPC. * @@ -39,10 +41,11 @@ export function rpcTransactionPlanner(config: TransactionPlannerConfig = {}) { return (client: T) => { const transactionPlanner = createTransactionPlanner({ createTransactionMessage: () => { + const estimateResourceLimits = shouldEstimateResourceLimits(config.resourceLimitEstimation); return pipe( createTransactionMessage({ version: config.version ?? 0 }), tx => setTransactionMessageFeePayerSigner(client.payer, tx), - tx => fillTransactionMessageProvisoryResourceLimits(tx), + tx => (estimateResourceLimits ? fillTransactionMessageProvisoryResourceLimits(tx) : tx), tx => config.microLamportsPerComputeUnit ? setTransactionMessageComputeUnitPrice(config.microLamportsPerComputeUnit, tx) @@ -71,6 +74,14 @@ export type TransactionPlannerConfig = { * Defaults to using no priority fees. */ microLamportsPerComputeUnit?: MicroLamports; + /** + * Whether to reserve space for resource limits that the executor can later + * estimate and set. Set to `none` for transactions where reserving a + * compute-budget instruction can push the message over the size limit. + * + * Defaults to `estimate`. + */ + resourceLimitEstimation?: ResourceLimitEstimationMode; /** * The transaction message version to use when creating transaction messages. * Defaults to version 0. diff --git a/packages/kit-plugin-rpc/test/index.test.ts b/packages/kit-plugin-rpc/test/index.test.ts index f96bbcf9..0779e790 100644 --- a/packages/kit-plugin-rpc/test/index.test.ts +++ b/packages/kit-plugin-rpc/test/index.test.ts @@ -1,4 +1,16 @@ -import { createClient, createSolanaRpc, createSolanaRpcSubscriptions, mainnet, TransactionSigner } from '@solana/kit'; +import { + Address, + createClient, + createSolanaRpc, + createSolanaRpcSubscriptions, + generateKeyPairSigner, + getTransactionMessageComputeUnitLimit, + mainnet, + singleInstructionPlan, + SingleTransactionPlan, + TransactionMessage, + TransactionSigner, +} from '@solana/kit'; import { beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'; import { @@ -13,6 +25,10 @@ import { solanaRpcSubscriptionsConnection, } from '../src'; +const MOCK_INSTRUCTION = { + programAddress: '11111111111111111111111111111111' as Address, +}; + vi.mock('@solana/kit', async () => { const actual = await vi.importActual('@solana/kit'); return { @@ -171,6 +187,42 @@ describe('solanaRpc', () => { ); expect(client).toHaveProperty('rpcSubscriptions'); }); + + it('passes resource limit estimation config to the transaction planner', async () => { + const payer = await generateKeyPairSigner(); + const client = createClient() + .use(() => ({ payer })) + .use( + solanaRpc({ + resourceLimitEstimation: 'none', + rpcUrl: 'https://api.mainnet-beta.solana.com', + }), + ); + + const transactionPlan = (await client.transactionPlanner( + singleInstructionPlan(MOCK_INSTRUCTION), + )) as SingleTransactionPlan; + const message = transactionPlan.message as TransactionMessage & { version: 'legacy' | 0 }; + expect(getTransactionMessageComputeUnitLimit(message)).toBeUndefined(); + }); + + it('uses resource limit estimation from transactionConfig when no top-level value is provided', async () => { + const payer = await generateKeyPairSigner(); + const client = createClient() + .use(() => ({ payer })) + .use( + solanaRpc({ + rpcUrl: 'https://api.mainnet-beta.solana.com', + transactionConfig: { resourceLimitEstimation: 'none' }, + }), + ); + + const transactionPlan = (await client.transactionPlanner( + singleInstructionPlan(MOCK_INSTRUCTION), + )) as SingleTransactionPlan; + const message = transactionPlan.message as TransactionMessage & { version: 'legacy' | 0 }; + expect(getTransactionMessageComputeUnitLimit(message)).toBeUndefined(); + }); }); describe('solanaMainnetRpc', () => { diff --git a/packages/kit-plugin-rpc/test/transaction-plan-executor.test.ts b/packages/kit-plugin-rpc/test/transaction-plan-executor.test.ts index 2d9d630f..45b9d92e 100644 --- a/packages/kit-plugin-rpc/test/transaction-plan-executor.test.ts +++ b/packages/kit-plugin-rpc/test/transaction-plan-executor.test.ts @@ -3,6 +3,7 @@ import { createClient, createTransactionMessage, generateKeyPairSigner, + getTransactionMessageComputeUnitLimit, parallelTransactionPlan, pipe, Rpc, @@ -103,6 +104,38 @@ describe('rpcTransactionPlanExecutor', () => { }); }); + it('does not estimate or set resource limits when resource limit estimation is disabled', async () => { + const payer = await generateKeyPairSigner(); + const getLatestBlockhash = vi.fn().mockResolvedValue({ value: MOCK_BLOCKHASH }); + const simulateTransaction = vi.fn(); + const rpc = { + getLatestBlockhash: () => ({ send: getLatestBlockhash }), + simulateTransaction: () => ({ send: simulateTransaction }), + } as unknown as Rpc; + const rpcSubscriptions = {} as RpcSubscriptions; + const sendAndConfirmTransaction = vi.fn().mockResolvedValue('MockTransactionSignature'); + (sendAndConfirmTransactionFactory as Mock).mockReturnValueOnce(sendAndConfirmTransaction); + + const client = createClient() + .use(() => ({ payer, rpc, rpcSubscriptions })) + .use(rpcTransactionPlanExecutor({ resourceLimitEstimation: 'none' })); + + const txMessage = setTransactionMessageFeePayerSigner(payer, createTransactionMessage({ version: 0 })); + const result = (await client.transactionPlanExecutor( + singleTransactionPlan(txMessage), + )) as SingleTransactionPlanResult; + const updatedMessage = ( + result.context as { message: Parameters[0] } + ).message; + + expect(simulateTransaction).not.toHaveBeenCalled(); + expect(getTransactionMessageComputeUnitLimit(updatedMessage)).toBeUndefined(); + expect(sendAndConfirmTransaction).toHaveBeenCalledExactlyOnceWith(expect.anything(), { + commitment: 'confirmed', + skipPreflight: false, + }); + }); + it('sends with skipPreflight false when the transaction has an explicit compute unit limit', async () => { const payer = await generateKeyPairSigner(); const getLatestBlockhash = vi.fn().mockResolvedValue({ value: MOCK_BLOCKHASH }); diff --git a/packages/kit-plugin-rpc/test/transaction-planner.test.ts b/packages/kit-plugin-rpc/test/transaction-planner.test.ts index 5c7467e4..632e4100 100644 --- a/packages/kit-plugin-rpc/test/transaction-planner.test.ts +++ b/packages/kit-plugin-rpc/test/transaction-planner.test.ts @@ -2,6 +2,7 @@ import { Address, createClient, generateKeyPairSigner, + getTransactionMessageComputeUnitLimit, getTransactionMessageComputeUnitPrice, MicroLamports, singleInstructionPlan, @@ -49,6 +50,30 @@ describe('rpcTransactionPlanner', () => { expect(transactionPlan.message.version).toBe(0); }); + it('adds provisory resource limits by default', async () => { + const payer = await generateKeyPairSigner(); + const client = createClient() + .use(() => ({ payer })) + .use(rpcTransactionPlanner()); + + const instructionPlan = singleInstructionPlan(MOCK_INSTRUCTION); + const transactionPlan = (await client.transactionPlanner(instructionPlan)) as SingleTransactionPlan; + const message = transactionPlan.message as TransactionMessage & { version: 'legacy' | 0 }; + expect(getTransactionMessageComputeUnitLimit(message)).toBe(0); + }); + + it('does not add provisory resource limits when resource limit estimation is disabled', async () => { + const payer = await generateKeyPairSigner(); + const client = createClient() + .use(() => ({ payer })) + .use(rpcTransactionPlanner({ resourceLimitEstimation: 'none' })); + + const instructionPlan = singleInstructionPlan(MOCK_INSTRUCTION); + const transactionPlan = (await client.transactionPlanner(instructionPlan)) as SingleTransactionPlan; + const message = transactionPlan.message as TransactionMessage & { version: 'legacy' | 0 }; + expect(getTransactionMessageComputeUnitLimit(message)).toBeUndefined(); + }); + it('creates legacy transaction messages when configured', async () => { const payer = await generateKeyPairSigner(); const client = createClient()