Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/large-moles-admire.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 2 additions & 3 deletions packages/programs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,16 @@ 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.

```ts
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;
Expand Down
172 changes: 86 additions & 86 deletions packages/programs/src/__tests__/program-error-test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
62 changes: 60 additions & 2 deletions packages/programs/src/program-error.ts
Original file line number Diff line number Diff line change
@@ -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<TProgramErrorCode extends number>(
error: unknown,
programAddress: Address,
code?: TProgramErrorCode,
): error is Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> &
SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM>;
/**
* Identifies whether an error -- typically caused by a transaction failure -- is a custom program
* error from the provided program address.
Expand All @@ -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<TProgramErrorCode extends number>(
error: unknown,
transactionMessage: { instructions: Record<number, { programAddress: Address }> },
programAddress: Address,
code?: TProgramErrorCode,
): error is Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> &
SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM>;
export function isProgramError<TProgramErrorCode extends number>(
error: unknown,
transactionMessageOrProgramAddress: Address | { instructions: Record<number, { programAddress: Address }> },
programAddressOrCode: Address | TProgramErrorCode,
codeOrUndefined?: TProgramErrorCode,
): error is Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> &
SolanaError<typeof SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM> {
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<number, { programAddress: Address }>;
};
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;
Expand Down