diff --git a/.changeset/large-moles-admire.md b/.changeset/large-moles-admire.md new file mode 100644 index 000000000..8995d05d6 --- /dev/null +++ b/.changeset/large-moles-admire.md @@ -0,0 +1,5 @@ +--- +'@solana/programs': patch +--- + +Fixed a bug with `isProgramError` that gives accurate results for errors produced after the introduction of `solana-transaction-error` 3.0.0 to validator clients, and eliminates the need to supply the original transaction message. Please remove `transactionMessage` from the list of arguments now, as it will be removed in `@solana/kit` 4.0.0. diff --git a/packages/programs/README.md b/packages/programs/README.md index f72fee0eb..8b39c6bac 100644 --- a/packages/programs/README.md +++ b/packages/programs/README.md @@ -20,7 +20,6 @@ This package contains helpers for identifying custom program errors. It can be u This function identifies whether an error -- typically caused by a transaction failure -- is a custom program error from the provided program address. It takes the following parameters: - The `error` to identify. -- The `transactionMessage` object that failed to execute. Since the RPC response only provides the index of the failed instruction, the transaction message is required to determine its program address. - The `programAddress` of the program from which the error is expected to have originated. - Optionally, the expected error `code` of the custom program error. When provided, the function will check that the custom program error code matches the given value. @@ -28,9 +27,9 @@ This function identifies whether an error -- typically caused by a transaction f try { // Send and confirm your transaction. } catch (error) { - if (isProgramError(error, transactionMessage, myProgramAddress, 42)) { + if (isProgramError(error, myProgramAddress, 42)) { // Handle custom program error 42 from this program. - } else if (isProgramError(error, transactionMessage, myProgramAddress)) { + } else if (isProgramError(error, myProgramAddress)) { // Handle all other custom program errors from this program. } else { throw error; diff --git a/packages/programs/src/__tests__/program-error-test.ts b/packages/programs/src/__tests__/program-error-test.ts index ca8b3a4c3..544247e08 100644 --- a/packages/programs/src/__tests__/program-error-test.ts +++ b/packages/programs/src/__tests__/program-error-test.ts @@ -1,103 +1,103 @@ import { Address } from '@solana/addresses'; import { SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, SolanaError } from '@solana/errors'; import { pipe } from '@solana/functional'; -import { appendTransactionMessageInstruction, createTransactionMessage } from '@solana/transaction-messages'; +import { + appendTransactionMessageInstruction, + createTransactionMessage, + TransactionMessage, +} from '@solana/transaction-messages'; import { isProgramError } from '../program-error'; describe('isProgramError', () => { - it('identifies an error as a custom program error', () => { - // Given a transaction message with a single instruction. - const programAddress = '1111' as Address; - const tx = pipe(createTransactionMessage({ version: 0 }), tx => - appendTransactionMessageInstruction({ programAddress }, tx), - ); - - // And a custom program error on the instruction. - const error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { - code: 42, - index: 0, + const programAddress = '1111' as Address; + describe('when the error carries a responsible program address', () => { + let error: SolanaError; + beforeEach(() => { + error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { + code: 42, + index: 0, + responsibleProgramAddress: programAddress, + }); }); - - // Then we expect the error to be identified as a program error. - expect(isProgramError(error, tx, programAddress)).toBe(true); - }); - - it('matches the provided custom program error code', () => { - // Given a transaction message with a single instruction. - const programAddress = '1111' as Address; - const tx = pipe(createTransactionMessage({ version: 0 }), tx => - appendTransactionMessageInstruction({ programAddress }, tx), - ); - - // And a custom program error with code 42. - const error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { - code: 42, - index: 0, + it('identifies the error as a custom program error', () => { + expect(isProgramError(error, programAddress)).toBe(true); + }); + it('matches the provided custom program error code', () => { + expect(isProgramError(error, programAddress, 42)).toBe(true); + }); + it('returns false if the program address does not match', () => { + expect(isProgramError(error, '2222' as Address)).toBe(false); + }); + it('returns false if the custom program error code does not match', () => { + expect(isProgramError(error, programAddress, 43)).toBe(false); }); - - // When we specify the custom program error code 42. - const result = isProgramError(error, tx, programAddress, 42); - - // Then we expect the result to be true. - expect(result).toBe(true); }); - - it('returns false if the program address does not match', () => { - // Given a transaction message with a program A instruction. - const programA = '1111' as Address; - const tx = pipe(createTransactionMessage({ version: 0 }), tx => - appendTransactionMessageInstruction({ programAddress: programA }, tx), - ); - - // And a custom program error on the instruction. - const error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { - code: 42, - index: 0, + describe('when the error does not carry a responsible program address but the user supplied a transaction message', () => { + let error: SolanaError; + let transactionMessage: TransactionMessage; + beforeEach(() => { + error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { + code: 42, + index: 0, + }); + transactionMessage = pipe(createTransactionMessage({ version: 0 }), m => + appendTransactionMessageInstruction({ programAddress }, m), + ); + }); + it('uses the transaction message to identify the error as a custom program error', () => { + expect(isProgramError(error, transactionMessage, programAddress)).toBe(true); + }); + it('uses the transaction message to match the provided custom program error code', () => { + expect(isProgramError(error, transactionMessage, programAddress, 42)).toBe(true); + }); + it('returns false if the program address does not match', () => { + expect(isProgramError(error, transactionMessage, '2222' as Address)).toBe(false); + }); + it('returns false if the custom program error code does not match', () => { + expect(isProgramError(error, transactionMessage, programAddress, 43)).toBe(false); }); - - // When we try to identify the error as a program error for program B. - const programB = '2222' as Address; - const result = isProgramError(error, tx, programB); - - // Then we expect the result to be false. - expect(result).toBe(false); }); - - it('returns false if the instruction is missing', () => { - // Given a transaction message with a single instruction. - const programAddress = '1111' as Address; - const tx = pipe(createTransactionMessage({ version: 0 }), tx => - appendTransactionMessageInstruction({ programAddress }, tx), - ); - - // And a custom program error pointing to a missing instruction. - const error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { - code: 42, - index: 999, + describe('when the error carries a responsible program address and the user supplied a transaction message', () => { + let error: SolanaError; + let transactionMessage: TransactionMessage; + beforeEach(() => { + error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { + code: 42, + index: 0, + responsibleProgramAddress: programAddress, + }); + transactionMessage = pipe(createTransactionMessage({ version: 0 }), m => + appendTransactionMessageInstruction({ programAddress: '2222' as Address }, m), + ); + }); + it('uses the responsible program address in the error context to identify the error as a custom program error', () => { + expect(isProgramError(error, transactionMessage, programAddress)).toBe(true); + }); + it('uses the responsible program address in the error context to match the provided custom program error code', () => { + expect(isProgramError(error, transactionMessage, programAddress, 42)).toBe(true); + }); + it('returns false if the program address does not match, ignoring the contents of the transaction message', () => { + expect(isProgramError(error, transactionMessage, '2222' as Address)).toBe(false); + }); + it('returns false if the custom program error code does not match', () => { + expect(isProgramError(error, transactionMessage, programAddress, 43)).toBe(false); }); - - // Then we expect the error not to be identified as a program error. - expect(isProgramError(error, tx, programAddress)).toBe(false); }); - - it('returns false if the custom program error code does not match', () => { - // Given a transaction message with a single instruction. - const programAddress = '1111' as Address; - const tx = pipe(createTransactionMessage({ version: 0 }), tx => - appendTransactionMessageInstruction({ programAddress }, tx), - ); - - // And a custom program error on the instruction with code 42. - const error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { - code: 42, - index: 0, + describe('when the error does not carry a responsible program address and the user supplied a transaction message in which the referenced top-level instruction can not be found', () => { + let error: SolanaError; + let transactionMessage: TransactionMessage; + beforeEach(() => { + error = new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, { + code: 42, + index: 999, + }); + transactionMessage = pipe(createTransactionMessage({ version: 0 }), m => + appendTransactionMessageInstruction({ programAddress }, m), + ); + }); + it('returns false', () => { + expect(isProgramError(error, transactionMessage, programAddress)).toBe(false); }); - - // When we try to identify the error as a program error with code 43. - const result = isProgramError(error, tx, programAddress, 43); - - // Then we expect the result to be false. - expect(result).toBe(false); }); }); diff --git a/packages/programs/src/program-error.ts b/packages/programs/src/program-error.ts index 2495b292f..5b5d5f0bb 100644 --- a/packages/programs/src/program-error.ts +++ b/packages/programs/src/program-error.ts @@ -1,6 +1,36 @@ import type { Address } from '@solana/addresses'; import { isSolanaError, SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, SolanaError } from '@solana/errors'; +/** + * Identifies whether an error -- typically caused by a transaction failure -- is a custom program + * error from the provided program address. + * + * @param programAddress The address of the program from which the error is expected to have + * originated + * @param code The expected error code of the custom program error. When provided, the function will + * check that the custom program error code matches the given value. + * + * @example + * ```ts + * try { + * // Send and confirm your transaction. + * } catch (error) { + * if (isProgramError(error, myProgramAddress, 42)) { + * // Handle custom program error 42 from this program. + * } else if (isProgramError(error, myProgramAddress)) { + * // Handle all other custom program errors from this program. + * } else { + * throw error; + * } + * } + * ``` + */ +export function isProgramError( + error: unknown, + programAddress: Address, + code?: TProgramErrorCode, +): error is Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> & + SolanaError; /** * Identifies whether an error -- typically caused by a transaction failure -- is a custom program * error from the provided program address. @@ -27,19 +57,47 @@ import { isSolanaError, SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, SolanaError } f * } * } * ``` + * + * @deprecated As of `solana-transaction-error` 3.0.0 the validator adds the address of the program + * responsible for the error to the error itself, making it unnecessary to supply + * `transactionMessage` here. Remove the `transactionMessage` argument. That argument will be + * removed in version 4.0.0 of `@solana/kit`. */ export function isProgramError( error: unknown, transactionMessage: { instructions: Record }, programAddress: Address, code?: TProgramErrorCode, +): error is Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> & + SolanaError; +export function isProgramError( + error: unknown, + transactionMessageOrProgramAddress: Address | { instructions: Record }, + programAddressOrCode: Address | TProgramErrorCode, + codeOrUndefined?: TProgramErrorCode, ): error is Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> & SolanaError { + let transactionMessage; + let programAddress; + let code; + if (typeof transactionMessageOrProgramAddress === 'string') { + programAddress = transactionMessageOrProgramAddress as unknown as Address; + code = programAddressOrCode as unknown as TProgramErrorCode; + } else { + transactionMessage = transactionMessageOrProgramAddress as unknown as { + instructions: Record; + }; + programAddress = programAddressOrCode as unknown as Address; + code = codeOrUndefined as TProgramErrorCode | null | undefined; + } if (!isSolanaError(error, SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM)) { return false; } - const instructionProgramAddress = transactionMessage.instructions[error.context.index]?.programAddress; - if (!instructionProgramAddress || instructionProgramAddress !== programAddress) { + const responsibleProgramAddress = + 'responsibleProgramAddress' in error.context + ? error.context.responsibleProgramAddress + : transactionMessage?.instructions[error.context.index]?.programAddress; + if (!responsibleProgramAddress || responsibleProgramAddress !== programAddress) { return false; } return typeof code === 'undefined' || error.context.code === code;