Skip to content

Commit b216b6d

Browse files
authored
Set CU limit by simulating transactions (#5)
1 parent c281352 commit b216b6d

6 files changed

Lines changed: 135 additions & 38 deletions

File tree

clients/js/src/createMetadata.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function getCreateMetadataInstructionPlanUsingInstructionData(
8080
instructions: [
8181
...getComputeUnitInstructions({
8282
computeUnitPrice: input.priorityFees,
83-
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
83+
computeUnitLimit: 'simulated',
8484
}),
8585
getTransferSolInstruction({
8686
source: input.payer,
@@ -106,7 +106,7 @@ export function getCreateMetadataInstructionPlanUsingBuffer(
106106
instructions: [
107107
...getComputeUnitInstructions({
108108
computeUnitPrice: input.priorityFees,
109-
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
109+
computeUnitLimit: 'simulated',
110110
}),
111111
getTransferSolInstruction({
112112
source: input.payer,
@@ -145,7 +145,7 @@ export function getCreateMetadataInstructionPlanUsingBuffer(
145145
instructions: [
146146
...getComputeUnitInstructions({
147147
computeUnitPrice: input.priorityFees,
148-
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
148+
computeUnitLimit: 'simulated',
149149
}),
150150
getInitializeInstruction({
151151
...input,
Lines changed: 116 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,57 @@
11
import {
2-
appendTransactionMessageInstructions,
2+
COMPUTE_BUDGET_PROGRAM_ADDRESS,
3+
ComputeBudgetInstruction,
4+
getSetComputeUnitLimitInstruction,
5+
identifyComputeBudgetInstruction,
6+
} from '@solana-program/compute-budget';
7+
import {
8+
Commitment,
39
CompilableTransactionMessage,
10+
compileTransaction,
411
FullySignedTransaction,
12+
getBase64EncodedWireTransaction,
13+
GetEpochInfoApi,
14+
GetSignatureStatusesApi,
515
pipe,
16+
Rpc,
17+
RpcSubscriptions,
18+
sendAndConfirmTransactionFactory,
19+
SendTransactionApi,
20+
SignatureNotificationsApi,
621
signTransactionMessageWithSigners,
22+
SimulateTransactionApi,
23+
SlotNotificationsApi,
724
TransactionMessageWithBlockhashLifetime,
825
TransactionMessageWithDurableNonceLifetime,
9-
TransactionWithLifetime,
26+
TransactionWithBlockhashLifetime,
1027
} from '@solana/web3.js';
11-
import { MessageInstructionPlan } from './instructionPlan';
28+
import {
29+
getTransactionMessageFromPlan,
30+
MessageInstructionPlan,
31+
} from './instructionPlan';
1232
import {
1333
chunkParallelInstructionPlans,
1434
createInstructionPlanExecutor,
1535
InstructionPlanExecutor,
1636
} from './instructionPlanExecutor';
1737

1838
export type DefaultInstructionPlanExecutorConfig = Readonly<{
39+
rpc: Rpc<
40+
GetEpochInfoApi &
41+
GetSignatureStatusesApi &
42+
SendTransactionApi &
43+
SimulateTransactionApi
44+
>;
45+
46+
rpcSubscriptions: RpcSubscriptions<
47+
SignatureNotificationsApi & SlotNotificationsApi
48+
>;
49+
50+
/**
51+
* The commitment to use when confirming transactions.
52+
*/
53+
commitment?: Commitment;
54+
1955
/**
2056
* When provided, chunks the plans inside a {@link ParallelInstructionPlan}.
2157
* Each chunk is executed sequentially but each plan within a chunk is
@@ -29,7 +65,7 @@ export type DefaultInstructionPlanExecutorConfig = Readonly<{
2965
* simulate the transaction to determine the optimal compute unit limit
3066
* before updating the compute budget instruction with the computed value.
3167
*/
32-
simulateComputeUnitLimit?: boolean; // TODO
68+
simulateComputeUnitLimit?: boolean;
3369

3470
/**
3571
* Returns the default transaction message used to send transactions.
@@ -45,33 +81,40 @@ export type DefaultInstructionPlanExecutorConfig = Readonly<{
4581
| TransactionMessageWithDurableNonceLifetime
4682
)
4783
>;
48-
49-
/**
50-
* Sends and confirms a constructed transaction.
51-
*/
52-
sendAndConfirm: (
53-
transaction: FullySignedTransaction & TransactionWithLifetime,
54-
config?: { abortSignal?: AbortSignal }
55-
) => Promise<void>;
5684
}>;
5785

5886
export function getDefaultInstructionPlanExecutor(
5987
config: DefaultInstructionPlanExecutorConfig
6088
): InstructionPlanExecutor {
6189
const {
90+
rpc,
91+
commitment,
6292
getDefaultMessage,
6393
parallelChunkSize: chunkSize,
64-
sendAndConfirm,
94+
simulateComputeUnitLimit: shouldSimulateComputeUnitLimit,
6595
} = config;
96+
const sendAndConfirm = sendAndConfirmTransactionFactory(config);
6697

6798
return async (plan, config) => {
6899
const handleMessage = async (plan: MessageInstructionPlan) => {
69-
const tx = await pipe(
70-
await getDefaultMessage(config),
71-
(tx) => appendTransactionMessageInstructions(plan.instructions, tx),
72-
(tx) => signTransactionMessageWithSigners(tx)
73-
);
74-
await sendAndConfirm(tx, config);
100+
const defaultMessage = await getDefaultMessage(config);
101+
let message = getTransactionMessageFromPlan(defaultMessage, plan);
102+
103+
if (shouldSimulateComputeUnitLimit) {
104+
message = await setComputeUnitLimitBySimulatingTransaction(
105+
message,
106+
rpc
107+
);
108+
}
109+
110+
const tx = (await signTransactionMessageWithSigners(
111+
message
112+
)) as FullySignedTransaction & TransactionWithBlockhashLifetime;
113+
await sendAndConfirm(tx, {
114+
...config,
115+
commitment: commitment ?? 'confirmed',
116+
skipPreflight: shouldSimulateComputeUnitLimit,
117+
});
75118
};
76119

77120
const executor = pipe(createInstructionPlanExecutor(handleMessage), (e) =>
@@ -81,3 +124,57 @@ export function getDefaultInstructionPlanExecutor(
81124
return await executor(plan, config);
82125
};
83126
}
127+
128+
async function setComputeUnitLimitBySimulatingTransaction<
129+
TTransactionMessage extends
130+
CompilableTransactionMessage = CompilableTransactionMessage,
131+
>(
132+
message: TTransactionMessage,
133+
rpc: Rpc<SimulateTransactionApi>
134+
): Promise<TTransactionMessage> {
135+
const instructionIndex = message.instructions.findIndex((instruction) => {
136+
return (
137+
instruction.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS &&
138+
identifyComputeBudgetInstruction(instruction.data as Uint8Array) ===
139+
ComputeBudgetInstruction.SetComputeUnitLimit
140+
);
141+
});
142+
143+
// Ignore if no compute unit limit instruction is found.
144+
if (instructionIndex === -1) {
145+
return message;
146+
}
147+
148+
const limit = await getComputeUnitLimitBySimulatingTransaction(message, rpc);
149+
150+
// Ignore if the limit is not found.
151+
if (limit === undefined) {
152+
return message;
153+
}
154+
155+
return Object.freeze({
156+
...message,
157+
instructions: [
158+
...message.instructions.slice(0, instructionIndex),
159+
getSetComputeUnitLimitInstruction({
160+
// Use a 1.1x multiplier to the computed limit.
161+
units: Number((limit * 110n) / 100n),
162+
}),
163+
...message.instructions.slice(instructionIndex + 1),
164+
],
165+
} as TTransactionMessage);
166+
}
167+
168+
async function getComputeUnitLimitBySimulatingTransaction<
169+
TTransactionMessage extends
170+
CompilableTransactionMessage = CompilableTransactionMessage,
171+
>(
172+
message: TTransactionMessage,
173+
rpc: Rpc<SimulateTransactionApi>
174+
): Promise<bigint | undefined> {
175+
const tx = getBase64EncodedWireTransaction(compileTransaction(message));
176+
const result = await rpc
177+
.simulateTransaction(tx, { encoding: 'base64' })
178+
.send();
179+
return result.value.unitsConsumed;
180+
}

clients/js/src/internals.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
CompilableTransactionMessage,
99
compileTransaction,
1010
createTransactionMessage,
11-
FullySignedTransaction,
1211
GetAccountInfoApi,
1312
GetLatestBlockhashApi,
1413
getTransactionEncoder,
@@ -17,7 +16,6 @@ import {
1716
pipe,
1817
ReadonlyUint8Array,
1918
Rpc,
20-
sendAndConfirmTransactionFactory,
2119
setTransactionMessageFeePayerSigner,
2220
setTransactionMessageLifetimeUsingBlockhash,
2321
Transaction,
@@ -129,9 +127,11 @@ function getTimedCacheFunction<T>(
129127
};
130128
}
131129

130+
const MAX_COMPUTE_UNIT_LIMIT = 1_400_000;
131+
132132
export function getComputeUnitInstructions(input: {
133133
computeUnitPrice?: MicroLamports;
134-
computeUnitLimit?: number;
134+
computeUnitLimit?: number | 'simulated';
135135
}) {
136136
const instructions: IInstruction[] = [];
137137
if (input.computeUnitPrice !== undefined) {
@@ -144,7 +144,10 @@ export function getComputeUnitInstructions(input: {
144144
if (input.computeUnitLimit !== undefined) {
145145
instructions.push(
146146
getSetComputeUnitLimitInstruction({
147-
units: input.computeUnitLimit,
147+
units:
148+
input.computeUnitLimit === 'simulated'
149+
? MAX_COMPUTE_UNIT_LIMIT
150+
: input.computeUnitLimit,
148151
})
149152
);
150153
}
@@ -198,7 +201,7 @@ export function getWriteInstructionPlan(input: {
198201
instructions: [
199202
...getComputeUnitInstructions({
200203
computeUnitPrice: input.priorityFees,
201-
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
204+
computeUnitLimit: 'simulated',
202205
}),
203206
getWriteInstruction(input),
204207
],
@@ -220,15 +223,10 @@ export function getMetadataInstructionPlanExecutor(
220223
plan: InstructionPlan,
221224
config?: { abortSignal?: AbortSignal }
222225
) => Promise<MetadataResponse> {
223-
const sendAndConfirm = sendAndConfirmTransactionFactory(input);
224226
const executor = getDefaultInstructionPlanExecutor({
227+
...input,
228+
simulateComputeUnitLimit: true,
225229
getDefaultMessage: input.getDefaultMessage,
226-
sendAndConfirm: async (tx, config) => {
227-
await sendAndConfirm(
228-
tx as FullySignedTransaction & TransactionMessageWithBlockhashLifetime,
229-
{ commitment: input.commitment ?? 'confirmed', ...config }
230-
);
231-
},
232230
});
233231

234232
return async (plan, config) => {

clients/js/src/updateMetadata.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function getUpdateMetadataInstructionPlanUsingInstructionData(
116116
instructions: [
117117
...getComputeUnitInstructions({
118118
computeUnitPrice: input.priorityFees,
119-
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
119+
computeUnitLimit: 'simulated',
120120
}),
121121
],
122122
};
@@ -164,7 +164,7 @@ export function getUpdateMetadataInstructionPlanUsingBuffer(
164164
instructions: [
165165
...getComputeUnitInstructions({
166166
computeUnitPrice: input.priorityFees,
167-
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
167+
computeUnitLimit: 'simulated',
168168
}),
169169
],
170170
};
@@ -216,7 +216,7 @@ export function getUpdateMetadataInstructionPlanUsingBuffer(
216216
instructions: [
217217
...getComputeUnitInstructions({
218218
computeUnitPrice: input.priorityFees,
219-
computeUnitLimit: undefined, // TODO: Add max CU for each instruction.
219+
computeUnitLimit: 'simulated',
220220
}),
221221
getSetDataInstruction({
222222
...input,

clients/js/src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
RpcSubscriptions,
2222
SendTransactionApi,
2323
SignatureNotificationsApi,
24+
SimulateTransactionApi,
2425
SlotNotificationsApi,
2526
Transaction,
2627
TransactionSigner,
@@ -51,6 +52,7 @@ export type MetadataInput = {
5152
GetEpochInfoApi &
5253
GetSignatureStatusesApi &
5354
SendTransactionApi &
55+
SimulateTransactionApi &
5456
GetAccountInfoApi &
5557
GetMinimumBalanceForRentExemptionApi
5658
>;

clients/js/test/createMetadata.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ test('it creates a canonical metadata account', async (t) => {
5555
});
5656
});
5757

58-
test.only('it creates a canonical metadata account with data larger than a transaction size', async (t) => {
58+
test('it creates a canonical metadata account with data larger than a transaction size', async (t) => {
5959
// Given the following authority and deployed program.
6060
const client = createDefaultSolanaClient();
6161
const authority = await generateKeyPairSignerWithSol(client);

0 commit comments

Comments
 (0)