From 23681637fa3ee0e2242b3b6bf087a066393bcbd8 Mon Sep 17 00:00:00 2001 From: Callum McIntyre Date: Wed, 2 Oct 2024 14:14:23 +0100 Subject: [PATCH] Throw if the transaction fails when simulated to estimate CUs (#3290) * Return the transaction error when estimating CUs * Add changeset * Throw an error when the transaction fails in simulation instead --- .changeset/swift-clocks-occur.md | 5 +++++ packages/errors/src/codes.ts | 2 ++ packages/errors/src/context.ts | 4 ++++ packages/errors/src/messages.ts | 6 ++++++ .../__tests__/compute-limit-internal-test.ts | 20 +++++++++++++++++-- .../library/src/compute-limit-internal.ts | 16 +++++++++++++-- 6 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 .changeset/swift-clocks-occur.md diff --git a/.changeset/swift-clocks-occur.md b/.changeset/swift-clocks-occur.md new file mode 100644 index 000000000000..821a11560b8a --- /dev/null +++ b/.changeset/swift-clocks-occur.md @@ -0,0 +1,5 @@ +--- +'@solana/web3.js': patch +--- + +Throw an error if a transaction fails when being simulated to estimate CUs diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 8f9d3af722f6..4bc231c97c73 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -205,6 +205,7 @@ export const SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION = 5663 export const SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES = 5663016 as const; export const SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH = 5663017 as const; export const SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT = 5663018 as const; +export const SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT = 5663019 as const; // Transaction errors. // Reserve error codes starting with [7050000-7050999] for the Rust enum `TransactionError`. @@ -492,6 +493,7 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING | typeof SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND | typeof SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT + | typeof SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT | typeof SOLANA_ERROR__TRANSACTION__FEE_PAYER_MISSING | typeof SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING | typeof SOLANA_ERROR__TRANSACTION__INVALID_NONCE_TRANSACTION_FIRST_INSTRUCTION_MUST_BE_ADVANCE_NONCE diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 47d329d4e6de..180f652de9c4 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -140,6 +140,7 @@ import { SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE, SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND, + SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT, SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_CANNOT_PAY_FEES, SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE, SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH, @@ -570,6 +571,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< [SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND]: { index: number; }; + [SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT]: { + unitsConsumed: number; + }; [SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_CANNOT_PAY_FEES]: { programAddress: string; }; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 6e8ee1c41625..73eae915f9b1 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -175,6 +175,7 @@ import { SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING, SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND, SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT, + SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT, SOLANA_ERROR__TRANSACTION__FEE_PAYER_MISSING, SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING, SOLANA_ERROR__TRANSACTION__INVALID_NONCE_TRANSACTION_FIRST_INSTRUCTION_MUST_BE_ADVANCE_NONCE, @@ -583,6 +584,11 @@ export const SolanaErrorMessages: Readonly<{ 'Failed to estimate the compute unit consumption for this transaction message. This is ' + 'likely because simulating the transaction failed. Inspect the `cause` property of this ' + 'error to learn more', + [SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT]: + 'Transaction failed when it was simulated in order to estimate the compute unit consumption. ' + + 'The compute unit estimate provided is for a transaction that failed when simulated and may not ' + + 'be representative of the compute units this transaction would consume if successful. Inspect the ' + + '`cause` property of this error to learn more', [SOLANA_ERROR__TRANSACTION__FEE_PAYER_MISSING]: 'Transaction is missing a fee payer.', [SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING]: "Could not determine this transaction's signature. Make sure that the transaction has " + diff --git a/packages/library/src/__tests__/compute-limit-internal-test.ts b/packages/library/src/__tests__/compute-limit-internal-test.ts index 7ad8acf13503..682ea0081451 100644 --- a/packages/library/src/__tests__/compute-limit-internal-test.ts +++ b/packages/library/src/__tests__/compute-limit-internal-test.ts @@ -2,11 +2,12 @@ import { Address } from '@solana/addresses'; import { SOLANA_ERROR__INSTRUCTION_ERROR__INSUFFICIENT_FUNDS, SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT, + SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT, SolanaError, } from '@solana/errors'; import { AccountRole } from '@solana/instructions'; import { Rpc, SimulateTransactionApi } from '@solana/rpc'; -import { Blockhash } from '@solana/rpc-types'; +import { Blockhash, TransactionError } from '@solana/rpc-types'; import { ITransactionMessageWithFeePayer, Nonce, TransactionMessage } from '@solana/transaction-messages'; import { getComputeUnitEstimateForTransactionMessage_INTERNAL_ONLY_DO_NOT_EXPORT } from '../compute-limit-internal'; @@ -242,7 +243,22 @@ describe('getComputeUnitEstimateForTransactionMessage_INTERNAL_ONLY_DO_NOT_EXPOR rpc, transactionMessage: mockTransactionMessage, }); - await expect(estimatePromise).resolves.toBe(1400000 /* MAX_COMPUTE_UNITS */); + await expect(estimatePromise).resolves.toBe(1400000); + }); + it('throws with the transaction error as cause when the transaction fails in simulation', async () => { + expect.assertions(1); + const transactionError: TransactionError = 'AccountNotFound'; + sendSimulateTransactionRequest.mockResolvedValue({ value: { err: transactionError, unitsConsumed: 42n } }); + const estimatePromise = getComputeUnitEstimateForTransactionMessage_INTERNAL_ONLY_DO_NOT_EXPORT({ + rpc, + transactionMessage: mockTransactionMessage, + }); + await expect(estimatePromise).rejects.toThrow( + new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT, { + cause: transactionError, + unitsConsumed: 42, + }), + ); }); it('throws with the cause when simulation fails', async () => { expect.assertions(1); diff --git a/packages/library/src/compute-limit-internal.ts b/packages/library/src/compute-limit-internal.ts index 17fe252e303e..251406286062 100644 --- a/packages/library/src/compute-limit-internal.ts +++ b/packages/library/src/compute-limit-internal.ts @@ -1,6 +1,11 @@ import { Address } from '@solana/addresses'; import { getU32Encoder } from '@solana/codecs'; -import { SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT, SolanaError } from '@solana/errors'; +import { + isSolanaError, + SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT, + SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT, + SolanaError, +} from '@solana/errors'; import { IInstruction, IInstructionWithData, @@ -162,7 +167,7 @@ export async function getComputeUnitEstimateForTransactionMessage_INTERNAL_ONLY_ const wireTransactionBytes = getBase64EncodedWireTransaction(compiledTransaction); try { const { - value: { unitsConsumed }, + value: { err: transactionError, unitsConsumed }, } = await rpc .simulateTransaction(wireTransactionBytes, { ...simulateConfig, @@ -179,8 +184,15 @@ export async function getComputeUnitEstimateForTransactionMessage_INTERNAL_ONLY_ // compute units as a u64, but the `SetComputeLimit` instruction only accepts a u32. Until // this changes, downcast it. const downcastUnitsConsumed = unitsConsumed > 4_294_967_295n ? 4_294_967_295 : Number(unitsConsumed); + if (transactionError) { + throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT, { + cause: transactionError, + unitsConsumed: downcastUnitsConsumed, + }); + } return downcastUnitsConsumed; } 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, });