@solana/kit
Version:
Solana Javascript API
184 lines (169 loc) • 8.09 kB
text/typescript
import {
getSolanaErrorFromTransactionError,
isSolanaError,
type RpcSimulateTransactionResult,
SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT,
SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT,
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,
isTransactionMessageWithDurableNonceLifetime,
setTransactionMessageComputeUnitLimit,
TransactionMessage,
TransactionMessageWithFeePayer,
} from '@solana/transaction-messages';
import { compileTransaction, getBase64EncodedWireTransaction } from '@solana/transactions';
const PROVISORY_COMPUTE_UNIT_LIMIT = 0;
const MAX_COMPUTE_UNIT_LIMIT = 1_400_000;
type EstimateComputeUnitLimitFactoryConfig = Readonly<{
rpc: Rpc<SimulateTransactionApi>;
}>;
type EstimateComputeUnitLimitConfig = Readonly<{
abortSignal?: AbortSignal;
commitment?: Commitment;
minContextSlot?: Slot;
}>;
type EstimateComputeUnitLimitFunction = (
transactionMessage: TransactionMessage & TransactionMessageWithFeePayer,
config?: EstimateComputeUnitLimitConfig,
) => Promise<number>;
/**
* Returns a function that estimates the compute units consumed by a transaction message by
* simulating it.
*
* The estimator sets the compute unit limit to the maximum (1,400,000) before simulating, so the
* simulation does not fail due to compute unit 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.
*
* @param factoryConfig - An object containing the RPC instance to use for simulation.
* @return A function that accepts a transaction message and returns the estimated compute units.
*
* @example
* ```ts
* import { estimateComputeUnitLimitFactory } from '@solana/kit';
*
* const estimateComputeUnitLimit = estimateComputeUnitLimitFactory({ rpc });
* const estimatedUnits = await estimateComputeUnitLimit(transactionMessage);
* ```
*/
export function estimateComputeUnitLimitFactory({
rpc,
}: EstimateComputeUnitLimitFactoryConfig): EstimateComputeUnitLimitFunction {
return async function estimateComputeUnitLimit(transactionMessage, config) {
const { abortSignal, ...simulateConfig } = config ?? {};
const replaceRecentBlockhash = !isTransactionMessageWithDurableNonceLifetime(transactionMessage);
const transaction = pipe(
transactionMessage,
m => setTransactionMessageComputeUnitLimit(MAX_COMPUTE_UNIT_LIMIT, m),
compileTransaction,
);
const wireTransactionBytes = getBase64EncodedWireTransaction(transaction);
try {
const response = await rpc
.simulateTransaction(wireTransactionBytes, {
...simulateConfig,
encoding: 'base64',
replaceRecentBlockhash,
sigVerify: false,
})
.send({ abortSignal });
// The API response type varies based on config (eg. `replacementBlockhash` is only
// present when `replaceRecentBlockhash` is true), but `RpcSimulateTransactionResult`
// is a flat superset. 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 (transactionError) {
throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT, {
...simulationResult,
cause: getSolanaErrorFromTransactionError(transactionError),
});
}
// Downcast from bigint to number, capping at u32 max.
return simulationResult.unitsConsumed > 4_294_967_295n
? 4_294_967_295
: Number(simulationResult.unitsConsumed);
} catch (e) {
if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT)) {
throw e;
}
throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT, {
cause: e,
});
}
};
}
/**
* Returns a function that estimates the compute unit limit for a transaction message and sets it on
* the message. If the message already has an explicit compute unit limit set (one that is not the
* provisory value of 0, and not the maximum of 1,400,000), the message is returned unchanged.
*
* This is designed to work with {@link fillTransactionMessageProvisoryComputeUnitLimit}: first add a provisory limit
* during transaction construction, then later estimate and replace it before sending.
*
* @param estimateComputeUnitLimit - The estimator function, typically created by
* {@link estimateComputeUnitLimitFactory}. You can also pass a custom wrapper that adds a buffer
* (e.g. multiply the estimate by 1.1).
* @return A function that accepts a transaction message and returns it with the compute unit limit
* set to the estimated value.
*
* @example
* ```ts
* import { estimateAndSetComputeUnitLimitFactory, estimateComputeUnitLimitFactory } from '@solana/kit';
*
* const estimator = estimateComputeUnitLimitFactory({ rpc });
* const estimateAndSet = estimateAndSetComputeUnitLimitFactory(estimator);
* const updatedMessage = await estimateAndSet(transactionMessage);
* ```
*/
export function estimateAndSetComputeUnitLimitFactory(
estimateComputeUnitLimit: EstimateComputeUnitLimitFunction,
): <TTransactionMessage extends TransactionMessage & TransactionMessageWithFeePayer>(
transactionMessage: TTransactionMessage,
config?: EstimateComputeUnitLimitConfig,
) => Promise<TTransactionMessage> {
return async function estimateAndSetComputeUnitLimit(transactionMessage, config) {
const existingLimit = getTransactionMessageComputeUnitLimit(transactionMessage);
// If a non-provisory, non-max CU limit is already set, leave it as-is.
if (existingLimit && existingLimit !== MAX_COMPUTE_UNIT_LIMIT) {
return transactionMessage;
}
const estimatedUnits = await estimateComputeUnitLimit(transactionMessage, config);
return setTransactionMessageComputeUnitLimit(estimatedUnits, transactionMessage);
};
}
/**
* Sets the compute unit limit to a provisory value of 0 if no compute unit limit is currently set
* on the transaction message. If a limit is already set (any value, including 0), the message is
* returned unchanged.
*
* This is useful during transaction construction to reserve space for a compute unit limit that
* will later be replaced with an actual estimate via
* {@link estimateAndSetComputeUnitLimitFactory}.
*
* @param transactionMessage - The transaction message to add a provisory limit to.
* @return The transaction message with a provisory compute unit limit set, or unchanged if one was
* already present.
*
* @example
* ```ts
* import { fillTransactionMessageProvisoryComputeUnitLimit } from '@solana/kit';
*
* const messageWithProvisoryLimit = fillTransactionMessageProvisoryComputeUnitLimit(transactionMessage);
* ```
*/
export function fillTransactionMessageProvisoryComputeUnitLimit<TTransactionMessage extends TransactionMessage>(
transactionMessage: TTransactionMessage,
): TTransactionMessage {
if (getTransactionMessageComputeUnitLimit(transactionMessage) !== undefined) {
return transactionMessage;
}
return setTransactionMessageComputeUnitLimit(PROVISORY_COMPUTE_UNIT_LIMIT, transactionMessage);
}