Skip to content

Commit e576bee

Browse files
authored
Add compute unit helpers (#15)
1 parent f764763 commit e576bee

11 files changed

+1143
-0
lines changed

clients/js/src/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* A provisory compute unit limit is used to indicate that the transaction
3+
* should be estimated for compute units before being sent to the network.
4+
*
5+
* Setting it to zero ensures the transaction fails unless it is properly estimated.
6+
*/
7+
export const PROVISORY_COMPUTE_UNIT_LIMIT = 0;
8+
9+
/**
10+
* The maximum compute unit limit that can be set for a transaction.
11+
*/
12+
export const MAX_COMPUTE_UNIT_LIMIT = 1_400_000;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
CompilableTransactionMessage,
3+
ITransactionMessageWithFeePayer,
4+
TransactionMessage,
5+
} from '@solana/kit';
6+
import {
7+
MAX_COMPUTE_UNIT_LIMIT,
8+
PROVISORY_COMPUTE_UNIT_LIMIT,
9+
} from './constants';
10+
import {
11+
EstimateComputeUnitLimitFactoryFunction,
12+
EstimateComputeUnitLimitFactoryFunctionConfig,
13+
} from './estimateComputeLimitInternal';
14+
import { getSetComputeUnitLimitInstructionIndexAndUnits } from './internal';
15+
import { updateOrAppendSetComputeUnitLimitInstruction } from './setComputeLimit';
16+
17+
type EstimateAndUpdateProvisoryComputeUnitLimitFactoryFunction = <
18+
TTransactionMessage extends
19+
| CompilableTransactionMessage
20+
| (TransactionMessage & ITransactionMessageWithFeePayer),
21+
>(
22+
transactionMessage: TTransactionMessage,
23+
config?: EstimateComputeUnitLimitFactoryFunctionConfig
24+
) => Promise<TTransactionMessage>;
25+
26+
/**
27+
* Given a transaction message, if it does not have an explicit compute unit limit,
28+
* estimates the compute unit limit and updates the transaction message with
29+
* the estimated limit. Otherwise, returns the transaction message unchanged.
30+
*
31+
* It requires a function that estimates the compute unit limit.
32+
*
33+
* @example
34+
* ```ts
35+
* const estimateAndUpdateCUs = estimateAndUpdateProvisoryComputeUnitLimitFactory(
36+
* estimateComputeUnitLimitFactory({ rpc })
37+
* );
38+
*
39+
* const transactionMessageWithCUs = await estimateAndUpdateCUs(transactionMessage);
40+
* ```
41+
*
42+
* @see {@link estimateAndUpdateProvisoryComputeUnitLimitFactory}
43+
*/
44+
export function estimateAndUpdateProvisoryComputeUnitLimitFactory(
45+
estimateComputeUnitLimit: EstimateComputeUnitLimitFactoryFunction
46+
): EstimateAndUpdateProvisoryComputeUnitLimitFactoryFunction {
47+
return async function fn(transactionMessage, config) {
48+
const instructionDetails =
49+
getSetComputeUnitLimitInstructionIndexAndUnits(transactionMessage);
50+
51+
// If the transaction message already has a compute unit limit instruction
52+
// which is set to a specific value — i.e. not 0 or the maximum limit —
53+
// we don't need to estimate the compute unit limit.
54+
if (
55+
instructionDetails &&
56+
instructionDetails.units !== PROVISORY_COMPUTE_UNIT_LIMIT &&
57+
instructionDetails.units !== MAX_COMPUTE_UNIT_LIMIT
58+
) {
59+
return transactionMessage;
60+
}
61+
62+
return updateOrAppendSetComputeUnitLimitInstruction(
63+
await estimateComputeUnitLimit(transactionMessage, config),
64+
transactionMessage
65+
);
66+
};
67+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
estimateComputeUnitLimit,
3+
EstimateComputeUnitLimitFactoryConfig,
4+
EstimateComputeUnitLimitFactoryFunction,
5+
} from './estimateComputeLimitInternal';
6+
7+
/**
8+
* Use this utility to estimate the actual compute unit cost of a given transaction message.
9+
*
10+
* Correctly budgeting a compute unit limit for your transaction message can increase the
11+
* probability that your transaction will be accepted for processing. If you don't declare a compute
12+
* unit limit on your transaction, validators will assume an upper limit of 200K compute units (CU)
13+
* per instruction.
14+
*
15+
* Since validators have an incentive to pack as many transactions into each block as possible, they
16+
* may choose to include transactions that they know will fit into the remaining compute budget for
17+
* the current block over transactions that might not. For this reason, you should set a compute
18+
* unit limit on each of your transaction messages, whenever possible.
19+
*
20+
* > [!WARNING]
21+
* > The compute unit estimate is just that -- an estimate. The compute unit consumption of the
22+
* > actual transaction might be higher or lower than what was observed in simulation. Unless you
23+
* > are confident that your particular transaction message will consume the same or fewer compute
24+
* > units as was estimated, you might like to augment the estimate by either a fixed number of CUs
25+
* > or a multiplier.
26+
*
27+
* > [!NOTE]
28+
* > If you are preparing an _unsigned_ transaction, destined to be signed and submitted to the
29+
* > network by a wallet, you might like to leave it up to the wallet to determine the compute unit
30+
* > limit. Consider that the wallet might have a more global view of how many compute units certain
31+
* > types of transactions consume, and might be able to make better estimates of an appropriate
32+
* > compute unit budget.
33+
*
34+
* > [!INFO]
35+
* > In the event that a transaction message does not already have a `SetComputeUnitLimit`
36+
* > instruction, this function will add one before simulation. This ensures that the compute unit
37+
* > consumption of the `SetComputeUnitLimit` instruction itself is included in the estimate.
38+
*
39+
* @param config
40+
*
41+
* @example
42+
* ```ts
43+
* import { getSetComputeUnitLimitInstruction } from '@solana-program/compute-budget';
44+
* import { createSolanaRpc, estimateComputeUnitLimitFactory, pipe } from '@solana/kit';
45+
*
46+
* // Create an estimator function.
47+
* const rpc = createSolanaRpc('http://127.0.0.1:8899');
48+
* const estimateComputeUnitLimit = estimateComputeUnitLimitFactory({ rpc });
49+
*
50+
* // Create your transaction message.
51+
* const transactionMessage = pipe(
52+
* createTransactionMessage({ version: 'legacy' }),
53+
* /* ... *\/
54+
* );
55+
*
56+
* // Request an estimate of the actual compute units this message will consume. This is done by
57+
* // simulating the transaction and grabbing the estimated compute units from the result.
58+
* const estimatedUnits = await estimateComputeUnitLimit(transactionMessage);
59+
*
60+
* // Set the transaction message's compute unit budget.
61+
* const transactionMessageWithComputeUnitLimit = prependTransactionMessageInstruction(
62+
* getSetComputeUnitLimitInstruction({ units: estimatedUnits }),
63+
* transactionMessage,
64+
* );
65+
* ```
66+
*/
67+
export function estimateComputeUnitLimitFactory({
68+
rpc,
69+
}: EstimateComputeUnitLimitFactoryConfig): EstimateComputeUnitLimitFactoryFunction {
70+
return async function estimateComputeUnitLimitFactoryFunction(
71+
transactionMessage,
72+
config
73+
) {
74+
return await estimateComputeUnitLimit({
75+
...config,
76+
rpc,
77+
transactionMessage,
78+
});
79+
};
80+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import {
2+
Commitment,
3+
CompilableTransactionMessage,
4+
compileTransaction,
5+
getBase64EncodedWireTransaction,
6+
isDurableNonceTransaction,
7+
isSolanaError,
8+
ITransactionMessageWithFeePayer,
9+
pipe,
10+
Rpc,
11+
SimulateTransactionApi,
12+
Slot,
13+
SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT,
14+
SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT,
15+
SolanaError,
16+
Transaction,
17+
TransactionMessage,
18+
} from '@solana/kit';
19+
import { updateOrAppendSetComputeUnitLimitInstruction } from './setComputeLimit';
20+
import { MAX_COMPUTE_UNIT_LIMIT } from './constants';
21+
import { fillMissingTransactionMessageLifetimeUsingProvisoryBlockhash } from './internalMoveToKit';
22+
23+
export type EstimateComputeUnitLimitFactoryConfig = Readonly<{
24+
/** An object that supports the {@link SimulateTransactionApi} of the Solana RPC API */
25+
rpc: Rpc<SimulateTransactionApi>;
26+
}>;
27+
28+
export type EstimateComputeUnitLimitFactoryFunction = (
29+
transactionMessage:
30+
| CompilableTransactionMessage
31+
| (TransactionMessage & ITransactionMessageWithFeePayer),
32+
config?: EstimateComputeUnitLimitFactoryFunctionConfig
33+
) => Promise<number>;
34+
35+
export type EstimateComputeUnitLimitFactoryFunctionConfig = {
36+
abortSignal?: AbortSignal;
37+
/**
38+
* Compute the estimate as of the highest slot that has reached this level of commitment.
39+
*
40+
* @defaultValue Whichever default is applied by the underlying {@link RpcApi} in use. For
41+
* example, when using an API created by a `createSolanaRpc*()` helper, the default commitment
42+
* is `"confirmed"` unless configured otherwise. Unmitigated by an API layer on the client, the
43+
* default commitment applied by the server is `"finalized"`.
44+
*/
45+
commitment?: Commitment;
46+
/**
47+
* Prevents accessing stale data by enforcing that the RPC node has processed transactions up to
48+
* this slot
49+
*/
50+
minContextSlot?: Slot;
51+
};
52+
53+
type EstimateComputeUnitLimitConfig =
54+
EstimateComputeUnitLimitFactoryFunctionConfig &
55+
Readonly<{
56+
rpc: Rpc<SimulateTransactionApi>;
57+
transactionMessage:
58+
| CompilableTransactionMessage
59+
| (TransactionMessage & ITransactionMessageWithFeePayer);
60+
}>;
61+
62+
/**
63+
* Simulates a transaction message on the network and returns the number of compute units it
64+
* consumed during simulation.
65+
*
66+
* The estimate this function returns can be used to set a compute unit limit on the transaction.
67+
* Correctly budgeting a compute unit limit for your transaction message can increase the probability
68+
* that your transaction will be accepted for processing.
69+
*
70+
* If you don't declare a compute unit limit on your transaction, validators will assume an upper
71+
* limit of 200K compute units (CU) per instruction. Since validators have an incentive to pack as
72+
* many transactions into each block as possible, they may choose to include transactions that they
73+
* know will fit into the remaining compute budget for the current block over transactions that
74+
* might not. For this reason, you should set a compute unit limit on each of your transaction
75+
* messages, whenever possible.
76+
*
77+
* ## Example
78+
*
79+
* ```ts
80+
* import { getSetComputeLimitInstruction } from '@solana-program/compute-budget';
81+
* import { createSolanaRpc, getComputeUnitEstimateForTransactionMessageFactory, pipe } from '@solana/kit';
82+
*
83+
* // Create an estimator function.
84+
* const rpc = createSolanaRpc('http://127.0.0.1:8899');
85+
* const getComputeUnitEstimateForTransactionMessage =
86+
* getComputeUnitEstimateForTransactionMessageFactory({ rpc });
87+
*
88+
* // Create your transaction message.
89+
* const transactionMessage = pipe(
90+
* createTransactionMessage({ version: 'legacy' }),
91+
* /* ... *\/
92+
* );
93+
*
94+
* // Request an estimate of the actual compute units this message will consume.
95+
* const computeUnitsEstimate =
96+
* await getComputeUnitEstimateForTransactionMessage(transactionMessage);
97+
*
98+
* // Set the transaction message's compute unit budget.
99+
* const transactionMessageWithComputeUnitLimit = prependTransactionMessageInstruction(
100+
* getSetComputeLimitInstruction({ units: computeUnitsEstimate }),
101+
* transactionMessage,
102+
* );
103+
* ```
104+
*
105+
* > [!WARNING]
106+
* > The compute unit estimate is just that &ndash; an estimate. The compute unit consumption of the
107+
* > actual transaction might be higher or lower than what was observed in simulation. Unless you
108+
* > are confident that your particular transaction message will consume the same or fewer compute
109+
* > units as was estimated, you might like to augment the estimate by either a fixed number of CUs
110+
* > or a multiplier.
111+
*
112+
* > [!NOTE]
113+
* > If you are preparing an _unsigned_ transaction, destined to be signed and submitted to the
114+
* > network by a wallet, you might like to leave it up to the wallet to determine the compute unit
115+
* > limit. Consider that the wallet might have a more global view of how many compute units certain
116+
* > types of transactions consume, and might be able to make better estimates of an appropriate
117+
* > compute unit budget.
118+
*/
119+
export async function estimateComputeUnitLimit({
120+
transactionMessage,
121+
...configs
122+
}: EstimateComputeUnitLimitConfig): Promise<number> {
123+
const replaceRecentBlockhash = !isDurableNonceTransaction(transactionMessage);
124+
const transaction = pipe(
125+
transactionMessage,
126+
fillMissingTransactionMessageLifetimeUsingProvisoryBlockhash,
127+
(m) =>
128+
updateOrAppendSetComputeUnitLimitInstruction(MAX_COMPUTE_UNIT_LIMIT, m),
129+
compileTransaction
130+
);
131+
132+
return await simulateTransactionAndGetConsumedUnits({
133+
transaction,
134+
replaceRecentBlockhash,
135+
...configs,
136+
});
137+
}
138+
139+
type SimulateTransactionAndGetConsumedUnitsConfig = Omit<
140+
EstimateComputeUnitLimitConfig,
141+
'transactionMessage'
142+
> &
143+
Readonly<{ replaceRecentBlockhash?: boolean; transaction: Transaction }>;
144+
145+
async function simulateTransactionAndGetConsumedUnits({
146+
abortSignal,
147+
rpc,
148+
transaction,
149+
...simulateConfig
150+
}: SimulateTransactionAndGetConsumedUnitsConfig): Promise<number> {
151+
const wireTransactionBytes = getBase64EncodedWireTransaction(transaction);
152+
153+
try {
154+
const {
155+
value: { err: transactionError, unitsConsumed },
156+
} = await rpc
157+
.simulateTransaction(wireTransactionBytes, {
158+
...simulateConfig,
159+
encoding: 'base64',
160+
sigVerify: false,
161+
})
162+
.send({ abortSignal });
163+
if (unitsConsumed == null) {
164+
// This should never be hit, because all RPCs should support `unitsConsumed` by now.
165+
throw new SolanaError(
166+
SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT
167+
);
168+
}
169+
// FIXME(https://github.com/anza-xyz/agave/issues/1295): The simulation response returns
170+
// compute units as a u64, but the `SetComputeLimit` instruction only accepts a u32. Until
171+
// this changes, downcast it.
172+
const downcastUnitsConsumed =
173+
unitsConsumed > 4_294_967_295n ? 4_294_967_295 : Number(unitsConsumed);
174+
if (transactionError) {
175+
throw new SolanaError(
176+
SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT,
177+
{
178+
cause: transactionError,
179+
unitsConsumed: downcastUnitsConsumed,
180+
}
181+
);
182+
}
183+
return downcastUnitsConsumed;
184+
} catch (e) {
185+
if (
186+
isSolanaError(
187+
e,
188+
SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT
189+
)
190+
)
191+
throw e;
192+
throw new SolanaError(
193+
SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT,
194+
{ cause: e }
195+
);
196+
}
197+
}

clients/js/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export * from './generated';
2+
3+
export * from './constants';
4+
export * from './estimateAndSetComputeLimit';
5+
export * from './estimateComputeLimit';
6+
export * from './setComputeLimit';
7+
export * from './setComputePrice';

0 commit comments

Comments
 (0)