Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/quiet-lemons-allow.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions packages/kit-plugin-rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/kit-plugin-rpc/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
9 changes: 9 additions & 0 deletions packages/kit-plugin-rpc/src/resource-limit-estimation.ts
Original file line number Diff line number Diff line change
@@ -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';
}
27 changes: 23 additions & 4 deletions packages/kit-plugin-rpc/src/solana-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -76,6 +77,14 @@ export type SolanaRpcConfig<TClusterUrl extends ClusterUrl = ClusterUrl> = 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.
Expand Down Expand Up @@ -111,15 +120,25 @@ export type SolanaRpcConfig<TClusterUrl extends ClusterUrl = ClusterUrl> = Solan
* @see {@link solanaLocalRpc}
*/
export function solanaRpc<TClusterUrl extends ClusterUrl>(config: SolanaRpcConfig<TClusterUrl>) {
return <T extends ClientWithPayer>(client: T) =>
pipe(
return <T extends ClientWithPayer>(client: T) => {
const resourceLimitEstimation =
config.resourceLimitEstimation ?? config.transactionConfig?.resourceLimitEstimation;
return pipe(
client,
solanaRpcConnection<TClusterUrl>(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(),
);
};
}

/**
Expand Down
13 changes: 12 additions & 1 deletion packages/kit-plugin-rpc/src/transaction-plan-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 <
Expand Down Expand Up @@ -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) => {
Expand All @@ -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),
Expand Down
13 changes: 12 additions & 1 deletion packages/kit-plugin-rpc/src/transaction-planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -39,10 +41,11 @@ export function rpcTransactionPlanner(config: TransactionPlannerConfig = {}) {
return <T extends ClientWithPayer>(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)
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 53 additions & 1 deletion packages/kit-plugin-rpc/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<typeof import('@solana/kit')>('@solana/kit');
return {
Expand Down Expand Up @@ -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', () => {
Expand Down
33 changes: 33 additions & 0 deletions packages/kit-plugin-rpc/test/transaction-plan-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createClient,
createTransactionMessage,
generateKeyPairSigner,
getTransactionMessageComputeUnitLimit,
parallelTransactionPlan,
pipe,
Rpc,
Expand Down Expand Up @@ -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<SolanaRpcApi>;
const rpcSubscriptions = {} as RpcSubscriptions<SolanaRpcSubscriptionsApi>;
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<typeof getTransactionMessageComputeUnitLimit>[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 });
Expand Down
25 changes: 25 additions & 0 deletions packages/kit-plugin-rpc/test/transaction-planner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Address,
createClient,
generateKeyPairSigner,
getTransactionMessageComputeUnitLimit,
getTransactionMessageComputeUnitPrice,
MicroLamports,
singleInstructionPlan,
Expand Down Expand Up @@ -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()
Expand Down