Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
Convert InstructionErrors to coded exceptions (#2220)
Browse files Browse the repository at this point in the history
# Summary

In this PR, we build atop the work in #2213 and introduce coded exceptions for each `InstructionError` returned from the RPC's `sendTransaction` method.

> [!NOTE]
> Because the RPC doesn't return structured errors or error codes, we had to break our own rules in this PR and hardcode a map between the error names and the code numbers. My [first crack](https://gist.github.com/steveluscher/aaa7cbbb5433b1197983908a40860c47#file-fml-ts-L12) at this employed a source code compression scheme that I later deemed too risky for the 250 gzipped bytes it saved. We might consider such a scheme in the future, especially since the next PR will add `InstructionError` to the mix.

# Test Plan

```shell
cd packages/errors
pnpm test:unit:browser
pnpm test:unit:node
```

Addresses #2118.
  • Loading branch information
steveluscher authored Feb 29, 2024
1 parent 4ff4425 commit c9b2705
Show file tree
Hide file tree
Showing 8 changed files with 639 additions and 41 deletions.
106 changes: 106 additions & 0 deletions packages/errors/src/__tests__/instruction-error-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
SOLANA_ERROR__INSTRUCTION_ERROR_BORSH_IO_ERROR,
SOLANA_ERROR__INSTRUCTION_ERROR_CUSTOM,
SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN,
SolanaErrorCode,
} from '../codes';
import { SolanaError } from '../error';
import { getSolanaErrorFromInstructionError } from '../instruction-error';

describe('getSolanaErrorFromInstructionError', () => {
it.each([
['GenericError', 4615001],
['InvalidArgument', 4615002],
['InvalidInstructionData', 4615003],
['InvalidAccountData', 4615004],
['AccountDataTooSmall', 4615005],
['InsufficientFunds', 4615006],
['IncorrectProgramId', 4615007],
['MissingRequiredSignature', 4615008],
['AccountAlreadyInitialized', 4615009],
['UninitializedAccount', 4615010],
['UnbalancedInstruction', 4615011],
['ModifiedProgramId', 4615012],
['ExternalAccountLamportSpend', 4615013],
['ExternalAccountDataModified', 4615014],
['ReadonlyLamportChange', 4615015],
['ReadonlyDataModified', 4615016],
['DuplicateAccountIndex', 4615017],
['ExecutableModified', 4615018],
['RentEpochModified', 4615019],
['NotEnoughAccountKeys', 4615020],
['AccountDataSizeChanged', 4615021],
['AccountNotExecutable', 4615022],
['AccountBorrowFailed', 4615023],
['AccountBorrowOutstanding', 4615024],
['DuplicateAccountOutOfSync', 4615025],
['InvalidError', 4615027],
['ExecutableDataModified', 4615028],
['ExecutableLamportChange', 4615029],
['ExecutableAccountNotRentExempt', 4615030],
['UnsupportedProgramId', 4615031],
['CallDepth', 4615032],
['MissingAccount', 4615033],
['ReentrancyNotAllowed', 4615034],
['MaxSeedLengthExceeded', 4615035],
['InvalidSeeds', 4615036],
['InvalidRealloc', 4615037],
['ComputationalBudgetExceeded', 4615038],
['PrivilegeEscalation', 4615039],
['ProgramEnvironmentSetupFailure', 4615040],
['ProgramFailedToComplete', 4615041],
['ProgramFailedToCompile', 4615042],
['Immutable', 4615043],
['IncorrectAuthority', 4615044],
['AccountNotRentExempt', 4615046],
['InvalidAccountOwner', 4615047],
['ArithmeticOverflow', 4615048],
['UnsupportedSysvar', 4615049],
['IllegalOwner', 4615050],
['MaxAccountsDataAllocationsExceeded', 4615051],
['MaxAccountsExceeded', 4615052],
['MaxInstructionTraceLengthExceeded', 4615053],
['BuiltinProgramsMustConsumeComputeUnits', 4615054],
])('produces the correct `SolanaError` for a `%s` error', (transactionError, expectedCode) => {
const error = getSolanaErrorFromInstructionError(123, transactionError);
expect(error).toEqual(new SolanaError(expectedCode as SolanaErrorCode, { index: 123 }));
});
it('produces the correct `SolanaError` for a `Custom` error', () => {
const error = getSolanaErrorFromInstructionError(123, { Custom: 789 });
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR_CUSTOM, {
code: 789,
index: 123,
}),
);
});
it('produces the correct `SolanaError` for a `BorshIoError` error', () => {
const error = getSolanaErrorFromInstructionError(123, { BorshIoError: 'abc' });
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR_BORSH_IO_ERROR, {
encodedData: 'abc',
index: 123,
}),
);
});
it("returns the unknown error when encountering an enum name that's missing from the map", () => {
const error = getSolanaErrorFromInstructionError(123, 'ThisDoesNotExist');
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN, {
errorName: 'ThisDoesNotExist',
index: 123,
}),
);
});
it("returns the unknown error when encountering an enum struct that's missing from the map", () => {
const expectedContext = {} as const;
const error = getSolanaErrorFromInstructionError(123, { ThisDoesNotExist: expectedContext });
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN, {
errorName: 'ThisDoesNotExist',
index: 123,
instructionErrorContext: expectedContext,
}),
);
});
});
11 changes: 11 additions & 0 deletions packages/errors/src/__tests__/transaction-error-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import {
SolanaErrorCode,
} from '../codes';
import { SolanaError } from '../error';
import { getSolanaErrorFromInstructionError } from '../instruction-error';
import { getSolanaErrorFromTransactionError } from '../transaction-error';

jest.mock('../instruction-error.ts');

describe('getSolanaErrorFromTransactionError', () => {
it.each([
['AccountInUse', 7050001],
Expand Down Expand Up @@ -91,4 +94,12 @@ describe('getSolanaErrorFromTransactionError', () => {
}),
);
});
it('delegates `InstructionError` to the instruction error getter', () => {
const instructionError = Symbol();
const mockErrorResult = Symbol() as unknown as SolanaError;
jest.mocked(getSolanaErrorFromInstructionError).mockReturnValue(mockErrorResult);
const error = getSolanaErrorFromTransactionError({ InstructionError: [123, instructionError] });
expect(getSolanaErrorFromInstructionError).toHaveBeenCalledWith(123, instructionError);
expect(error).toBe(mockErrorResult);
});
});
111 changes: 111 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,62 @@ export const SOLANA_ERROR__INVALID_KEYPAIR_BYTES = 4 as const;
export const SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED = 5 as const;
export const SOLANA_ERROR__NONCE_INVALID = 6 as const;
export const SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND = 7 as const;
// Reserve error codes starting with [4615000-4615999] for the Rust enum `InstructionError`
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN = 4615000 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_GENERIC_ERROR = 4615001 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ARGUMENT = 4615002 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_INSTRUCTION_DATA = 4615003 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ACCOUNT_DATA = 4615004 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_DATA_TOO_SMALL = 4615005 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INSUFFICIENT_FUNDS = 4615006 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INCORRECT_PROGRAM_ID = 4615007 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_MISSING_REQUIRED_SIGNATURE = 4615008 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_ALREADY_INITIALIZED = 4615009 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNINITIALIZED_ACCOUNT = 4615010 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNBALANCED_INSTRUCTION = 4615011 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_MODIFIED_PROGRAM_ID = 4615012 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_EXTERNAL_ACCOUNT_LAMPORT_SPEND = 4615013 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_EXTERNAL_ACCOUNT_DATA_MODIFIED = 4615014 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_READONLY_LAMPORT_CHANGE = 4615015 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_READONLY_DATA_MODIFIED = 4615016 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_DUPLICATE_ACCOUNT_INDEX = 4615017 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_EXECUTABLE_MODIFIED = 4615018 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_RENT_EPOCH_MODIFIED = 4615019 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_NOT_ENOUGH_ACCOUNT_KEYS = 4615020 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_DATA_SIZE_CHANGED = 4615021 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_NOT_EXECUTABLE = 4615022 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_FAILED = 4615023 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_OUTSTANDING = 4615024 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_DUPLICATE_ACCOUNT_OUT_OF_SYNC = 4615025 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_CUSTOM = 4615026 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ERROR = 4615027 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_EXECUTABLE_DATA_MODIFIED = 4615028 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_EXECUTABLE_LAMPORT_CHANGE = 4615029 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_EXECUTABLE_ACCOUNT_NOT_RENT_EXEMPT = 4615030 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_PROGRAM_ID = 4615031 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_CALL_DEPTH = 4615032 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_MISSING_ACCOUNT = 4615033 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_REENTRANCY_NOT_ALLOWED = 4615034 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_MAX_SEED_LENGTH_EXCEEDED = 4615035 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_SEEDS = 4615036 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_REALLOC = 4615037 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_COMPUTATIONAL_BUDGET_EXCEEDED = 4615038 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_PRIVILEGE_ESCALATION = 4615039 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_PROGRAM_ENVIRONMENT_SETUP_FAILURE = 4615040 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_PROGRAM_FAILED_TO_COMPLETE = 4615041 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_PROGRAM_FAILED_TO_COMPILE = 4615042 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_IMMUTABLE = 4615043 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INCORRECT_AUTHORITY = 4615044 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_BORSH_IO_ERROR = 4615045 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_NOT_RENT_EXEMPT = 4615046 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ACCOUNT_OWNER = 4615047 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ARITHMETIC_OVERFLOW = 4615048 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR = 4615049 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_ILLEGAL_OWNER = 4615050 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_MAX_ACCOUNTS_DATA_ALLOCATIONS_EXCEEDED = 4615051 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_MAX_ACCOUNTS_EXCEEDED = 4615052 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_MAX_INSTRUCTION_TRACE_LENGTH_EXCEEDED = 4615053 as const;
export const SOLANA_ERROR__INSTRUCTION_ERROR_BUILTIN_PROGRAMS_MUST_CONSUME_COMPUTE_UNITS = 4615054 as const;
// Reserve error codes starting with [7050000-7050999] for the Rust enum `TransactionError`
export const SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN = 7050000 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_IN_USE = 7050001 as const;
Expand Down Expand Up @@ -76,6 +132,61 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED
| typeof SOLANA_ERROR__NONCE_INVALID
| typeof SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_UNKNOWN
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_GENERIC_ERROR
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ARGUMENT
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_INSTRUCTION_DATA
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ACCOUNT_DATA
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_DATA_TOO_SMALL
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INSUFFICIENT_FUNDS
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INCORRECT_PROGRAM_ID
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MISSING_REQUIRED_SIGNATURE
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_ALREADY_INITIALIZED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_UNINITIALIZED_ACCOUNT
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_UNBALANCED_INSTRUCTION
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MODIFIED_PROGRAM_ID
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_EXTERNAL_ACCOUNT_LAMPORT_SPEND
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_EXTERNAL_ACCOUNT_DATA_MODIFIED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_READONLY_LAMPORT_CHANGE
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_READONLY_DATA_MODIFIED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_DUPLICATE_ACCOUNT_INDEX
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_EXECUTABLE_MODIFIED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_RENT_EPOCH_MODIFIED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_NOT_ENOUGH_ACCOUNT_KEYS
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_DATA_SIZE_CHANGED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_NOT_EXECUTABLE
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_FAILED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_BORROW_OUTSTANDING
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_DUPLICATE_ACCOUNT_OUT_OF_SYNC
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_CUSTOM
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ERROR
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_EXECUTABLE_DATA_MODIFIED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_EXECUTABLE_LAMPORT_CHANGE
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_EXECUTABLE_ACCOUNT_NOT_RENT_EXEMPT
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_PROGRAM_ID
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_CALL_DEPTH
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MISSING_ACCOUNT
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_REENTRANCY_NOT_ALLOWED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_SEED_LENGTH_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_SEEDS
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_REALLOC
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_COMPUTATIONAL_BUDGET_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_PRIVILEGE_ESCALATION
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_PROGRAM_ENVIRONMENT_SETUP_FAILURE
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_PROGRAM_FAILED_TO_COMPLETE
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_PROGRAM_FAILED_TO_COMPILE
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_IMMUTABLE
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INCORRECT_AUTHORITY
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_BORSH_IO_ERROR
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ACCOUNT_NOT_RENT_EXEMPT
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_INVALID_ACCOUNT_OWNER
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ARITHMETIC_OVERFLOW
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_UNSUPPORTED_SYSVAR
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_ILLEGAL_OWNER
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_ACCOUNTS_DATA_ALLOCATIONS_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_ACCOUNTS_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_MAX_INSTRUCTION_TRACE_LENGTH_EXCEEDED
| typeof SOLANA_ERROR__INSTRUCTION_ERROR_BUILTIN_PROGRAMS_MUST_CONSUME_COMPUTE_UNITS
| typeof SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN
| typeof SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_IN_USE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_LOADED_TWICE
Expand Down
Loading

0 comments on commit c9b2705

Please sign in to comment.