@solana/kit
Version:
Solana Javascript API
274 lines (254 loc) • 13 kB
text/typescript
import {
getSolanaErrorFromTransactionError,
isSolanaError,
type RpcSimulateTransactionResult,
SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT,
SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT,
SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS,
SolanaError,
} from '@solana/errors';
import { pipe } from '@solana/functional';
import type { Rpc, SimulateTransactionApi } from '@solana/rpc';
import type { Commitment, Slot } from '@solana/rpc-types';
import {
getTransactionMessageComputeUnitLimit,
getTransactionMessageLoadedAccountsDataSizeLimit,
isTransactionMessageWithDurableNonceLifetime,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit,
TransactionMessage,
TransactionMessageWithFeePayer,
} from '@solana/transaction-messages';
import { compileTransaction, getBase64EncodedWireTransaction } from '@solana/transactions';
const PROVISORY_LIMIT = 0;
// From Agave: https://github.com/anza-xyz/agave/blob/2a165e7a90af75c76426d1e031ed0284211d5d1e/program-runtime/src/execution_budget.rs#L39
const MAX_COMPUTE_UNIT_LIMIT = 1_400_000;
// From Agave: https://github.com/anza-xyz/agave/blob/2a165e7a90af75c76426d1e031ed0284211d5d1e/program-runtime/src/execution_budget.rs#L53
const MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT = 64 * 1024 * 1024;
type EstimateResourceLimitsFactoryConfig = Readonly<{
rpc: Rpc<SimulateTransactionApi>;
}>;
type EstimateResourceLimitsConfig = Readonly<{
abortSignal?: AbortSignal;
commitment?: Commitment;
minContextSlot?: Slot;
}>;
/**
* The estimated resource limits for a transaction message.
*
* `computeUnitLimit` is always returned. For version 1 transaction messages,
* `loadedAccountsDataSizeLimit` is also required. For legacy and version 0 messages, the
* `loadedAccountsDataSizeLimit` is returned when the RPC includes it in the simulation result,
* but is not required — callers that don't apply it can ignore the field.
*/
export type ResourceLimitsEstimate<TTransactionMessage extends TransactionMessage> = TTransactionMessage extends {
version: 1;
}
? { computeUnitLimit: number; loadedAccountsDataSizeLimit: number }
: { computeUnitLimit: number; loadedAccountsDataSizeLimit?: number };
type EstimateResourceLimitsFunction = <TTransactionMessage extends TransactionMessage & TransactionMessageWithFeePayer>(
transactionMessage: TTransactionMessage,
config?: EstimateResourceLimitsConfig,
) => Promise<ResourceLimitsEstimate<TTransactionMessage>>;
/**
* Returns a function that estimates the resource limits required by a transaction message by
* simulating it.
*
* The estimator sets the compute unit limit to the maximum (1,400,000) and the loaded accounts data
* size limit to the maximum (64 MiB) before simulating, so the simulation does not fail due to
* resource exhaustion. For blockhash-lifetime transactions, the RPC is asked to replace the
* blockhash during simulation, so any blockhash value will work. For durable nonce transactions,
* the actual nonce value is used.
*
* For version 1 transaction messages, both `computeUnitLimit` and `loadedAccountsDataSizeLimit` are
* returned. The function throws if the RPC does not return a `loadedAccountsDataSize` value, since
* this value is required for version 1 transactions. For legacy and version 0 messages, only
* `computeUnitLimit` is returned.
*
* @param factoryConfig - An object containing the RPC instance to use for simulation.
* @return A function that accepts a transaction message and returns the estimated resource limits.
*
* @example
* ```ts
* import { estimateResourceLimitsFactory } from '@solana/kit';
*
* const estimateResourceLimits = estimateResourceLimitsFactory({ rpc });
* const { computeUnitLimit, loadedAccountsDataSizeLimit } = await estimateResourceLimits(transactionMessage);
* ```
*/
export function estimateResourceLimitsFactory({
rpc,
}: EstimateResourceLimitsFactoryConfig): EstimateResourceLimitsFunction {
return async function estimateResourceLimits<
TTransactionMessage extends TransactionMessage & TransactionMessageWithFeePayer,
>(transactionMessage: TTransactionMessage, config?: EstimateResourceLimitsConfig) {
const { abortSignal, ...simulateConfig } = config ?? {};
const replaceRecentBlockhash = !isTransactionMessageWithDurableNonceLifetime(transactionMessage);
const isDataSizeRequired = transactionMessage.version === 1;
const transaction = pipe(
transactionMessage,
m => setTransactionMessageComputeUnitLimit(MAX_COMPUTE_UNIT_LIMIT, m),
m =>
isDataSizeRequired
? setTransactionMessageLoadedAccountsDataSizeLimit(MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, m)
: m,
compileTransaction,
);
const wireTransactionBytes = getBase64EncodedWireTransaction(transaction);
try {
const response = await rpc
.simulateTransaction(wireTransactionBytes, {
...simulateConfig,
encoding: 'base64',
replaceRecentBlockhash,
sigVerify: false,
})
.send({ abortSignal });
// The API response and `RpcSimulateTransactionResult` describe overlapping but
// structurally different shapes (eg. `replacementBlockhash` is a parsed
// `TransactionBlockhashLifetime` object here, but a raw string there). Cast through
// `unknown` to bridge the structural gap.
const { err: transactionError, ...simulationResult } =
response.value as unknown as RpcSimulateTransactionResult;
if (simulationResult.unitsConsumed == null) {
throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT);
}
if (isDataSizeRequired && simulationResult.loadedAccountsDataSize == null) {
throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT);
}
if (transactionError) {
throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS, {
...simulationResult,
cause: getSolanaErrorFromTransactionError(transactionError),
});
}
// Downcast from bigint to number, capping at u32 max.
const computeUnitLimit =
simulationResult.unitsConsumed > 4_294_967_295n
? 4_294_967_295
: Number(simulationResult.unitsConsumed);
if (isDataSizeRequired) {
return {
computeUnitLimit,
loadedAccountsDataSizeLimit: simulationResult.loadedAccountsDataSize!,
} as ResourceLimitsEstimate<TTransactionMessage>;
}
return (
simulationResult.loadedAccountsDataSize == null
? { computeUnitLimit }
: { computeUnitLimit, loadedAccountsDataSizeLimit: simulationResult.loadedAccountsDataSize }
) as ResourceLimitsEstimate<TTransactionMessage>;
} catch (e) {
if (
isSolanaError(e, SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS) ||
isSolanaError(e, SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT)
) {
throw e;
}
throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT, {
cause: e,
});
}
};
}
/**
* Returns a function that estimates the resource limits for a transaction message and sets them on
* the message.
*
* For all versions, the compute unit limit is updated to the estimated value if it is not already
* set to an explicit, non-provisory value. The compute unit limit also treats the maximum
* (1,400,000) as non-explicit, so messages that pre-set the max for simulation get re-estimated.
*
* For version 1 messages, the loaded accounts data size limit is updated only if it is unset or set
* to the provisory value of 0. An explicit value — including the runtime maximum — is left
* untouched, since callers who set it explicitly are signaling a deliberate choice.
*
* This is designed to work with {@link fillTransactionMessageProvisoryResourceLimits}: first add
* provisory limits during transaction construction, then later estimate and replace them before
* sending.
*
* @param estimateResourceLimits - The estimator function, typically created by
* {@link estimateResourceLimitsFactory}. You can also pass a custom wrapper that applies a buffer
* to the returned values.
* @return A function that accepts a transaction message and returns it with resource limits set to
* the estimated values.
*
* @example
* ```ts
* import { estimateAndSetResourceLimitsFactory, estimateResourceLimitsFactory } from '@solana/kit';
*
* const estimator = estimateResourceLimitsFactory({ rpc });
* const estimateAndSet = estimateAndSetResourceLimitsFactory(estimator);
* const updatedMessage = await estimateAndSet(transactionMessage);
* ```
*/
export function estimateAndSetResourceLimitsFactory(
estimateResourceLimits: EstimateResourceLimitsFunction,
): <TTransactionMessage extends TransactionMessage & TransactionMessageWithFeePayer>(
transactionMessage: TTransactionMessage,
config?: EstimateResourceLimitsConfig,
) => Promise<TTransactionMessage> {
return async function estimateAndSetResourceLimits(transactionMessage, config) {
const existingComputeUnitLimit = getTransactionMessageComputeUnitLimit(transactionMessage);
const computeUnitLimitIsExplicit =
existingComputeUnitLimit !== undefined &&
existingComputeUnitLimit !== PROVISORY_LIMIT &&
existingComputeUnitLimit !== MAX_COMPUTE_UNIT_LIMIT;
const isV1 = transactionMessage.version === 1;
let loadedAccountsDataSizeLimitIsExplicit = true;
if (isV1) {
const existingLoadedAccountsDataSizeLimit =
getTransactionMessageLoadedAccountsDataSizeLimit(transactionMessage);
loadedAccountsDataSizeLimitIsExplicit =
existingLoadedAccountsDataSizeLimit !== undefined &&
existingLoadedAccountsDataSizeLimit !== PROVISORY_LIMIT;
}
// Nothing to do — every applicable limit is already explicitly set.
if (computeUnitLimitIsExplicit && loadedAccountsDataSizeLimitIsExplicit) {
return transactionMessage;
}
const estimate = await estimateResourceLimits(transactionMessage, config);
let result = transactionMessage;
if (!computeUnitLimitIsExplicit) {
result = setTransactionMessageComputeUnitLimit(estimate.computeUnitLimit, result);
}
if (isV1 && !loadedAccountsDataSizeLimitIsExplicit && 'loadedAccountsDataSizeLimit' in estimate) {
result = setTransactionMessageLoadedAccountsDataSizeLimit(estimate.loadedAccountsDataSizeLimit, result);
}
return result;
};
}
/**
* Sets resource limits to a provisory value of 0 if no limit is currently set on the transaction
* message.
*
* For all versions, this fills the compute unit limit. For version 1 messages, it also fills the
* loaded accounts data size limit. If a limit is already set (any value, including 0), that limit
* is left unchanged.
*
* This is useful during transaction construction to reserve space for resource limits that will
* later be replaced with actual estimates via {@link estimateAndSetResourceLimitsFactory}.
*
* @param transactionMessage - The transaction message to add provisory limits to.
* @return The transaction message with provisory resource limits set, or unchanged if all
* applicable limits were already present.
*
* @example
* ```ts
* import { fillTransactionMessageProvisoryResourceLimits } from '@solana/kit';
*
* const messageWithProvisoryLimits = fillTransactionMessageProvisoryResourceLimits(transactionMessage);
* ```
*/
export function fillTransactionMessageProvisoryResourceLimits<TTransactionMessage extends TransactionMessage>(
transactionMessage: TTransactionMessage,
): TTransactionMessage {
let result: TTransactionMessage = transactionMessage;
if (getTransactionMessageComputeUnitLimit(result) === undefined) {
result = setTransactionMessageComputeUnitLimit(PROVISORY_LIMIT, result);
}
if (result.version === 1 && getTransactionMessageLoadedAccountsDataSizeLimit(result) === undefined) {
result = setTransactionMessageLoadedAccountsDataSizeLimit(PROVISORY_LIMIT, result);
}
return result;
}