Skip to content

Commit

Permalink
Create coded errors to cover all TransactionErrors from the RPC (#2213
Browse files Browse the repository at this point in the history
)

# Summary

Wouldn't it be nice if you could `catch` _particular_ transaction errors in your application, like `BlockhashNotFound`, and choose the correct mitigation based on the type of transaction error?

In this PR, we introduce coded exceptions for each `TransactionError` 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 committed Feb 29, 2024
1 parent 65af153 commit 8541c2e
Show file tree
Hide file tree
Showing 8 changed files with 397 additions and 6 deletions.
94 changes: 94 additions & 0 deletions packages/errors/src/__tests__/transaction-error-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION,
SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT,
SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED,
SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN,
SolanaErrorCode,
} from '../codes';
import { SolanaError } from '../error';
import { getSolanaErrorFromTransactionError } from '../transaction-error';

describe('getSolanaErrorFromTransactionError', () => {
it.each([
['AccountInUse', 7050001],
['AccountLoadedTwice', 7050002],
['AccountNotFound', 7050003],
['ProgramAccountNotFound', 7050004],
['InsufficientFundsForFee', 7050005],
['InvalidAccountForFee', 7050006],
['AlreadyProcessed', 7050007],
['BlockhashNotFound', 7050008],
['CallChainTooDeep', 7050009],
['MissingSignatureForFee', 7050010],
['InvalidAccountIndex', 7050011],
['SignatureFailure', 7050012],
['InvalidProgramForExecution', 7050013],
['SanitizeFailure', 7050014],
['ClusterMaintenance', 7050015],
['AccountBorrowOutstanding', 7050016],
['WouldExceedMaxBlockCostLimit', 7050017],
['UnsupportedVersion', 7050018],
['InvalidWritableAccount', 7050019],
['WouldExceedMaxAccountCostLimit', 7050020],
['WouldExceedAccountDataBlockLimit', 7050021],
['TooManyAccountLocks', 7050022],
['AddressLookupTableNotFound', 7050023],
['InvalidAddressLookupTableOwner', 7050024],
['InvalidAddressLookupTableData', 7050025],
['InvalidAddressLookupTableIndex', 7050026],
['InvalidRentPayingAccount', 7050027],
['WouldExceedMaxVoteCostLimit', 7050028],
['WouldExceedAccountDataTotalLimit', 7050029],
['MaxLoadedAccountsDataSizeExceeded', 7050032],
['InvalidLoadedAccountsDataSizeLimit', 7050033],
['ResanitizationNeeded', 7050034],
['UnbalancedTransaction', 7050036],
])('produces the correct `SolanaError` for a `%s` error', (transactionError, expectedCode) => {
const error = getSolanaErrorFromTransactionError(transactionError);
expect(error).toEqual(new SolanaError(expectedCode as SolanaErrorCode, undefined));
});
it('produces the correct `SolanaError` for a `DuplicateInstruction` error', () => {
const error = getSolanaErrorFromTransactionError({ DuplicateInstruction: 1 });
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION, {
index: 1,
}),
);
});
it('produces the correct `SolanaError` for a `InsufficientFundsForRent` error', () => {
const error = getSolanaErrorFromTransactionError({ InsufficientFundsForRent: { account_index: 1 } });
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT, {
accountIndex: 1,
}),
);
});
it('produces the correct `SolanaError` for a `ProgramExecutionTemporarilyRestricted` error', () => {
const error = getSolanaErrorFromTransactionError({
ProgramExecutionTemporarilyRestricted: { account_index: 1 },
});
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED, {
accountIndex: 1,
}),
);
});
it("returns the unknown error when encountering an enum name that's missing from the map", () => {
const error = getSolanaErrorFromTransactionError('ThisDoesNotExist');
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN, {
errorName: 'ThisDoesNotExist',
}),
);
});
it("returns the unknown error when encountering an enum struct that's missing from the map", () => {
const expectedContext = {} as const;
const error = getSolanaErrorFromTransactionError({ ThisDoesNotExist: expectedContext });
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN, {
errorName: 'ThisDoesNotExist',
transactionErrorContext: expectedContext,
}),
);
});
});
78 changes: 77 additions & 1 deletion packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,45 @@ 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 [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;
export const SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_LOADED_TWICE = 7050002 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_NOT_FOUND = 7050003 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_ACCOUNT_NOT_FOUND = 7050004 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_FEE = 7050005 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ACCOUNT_FOR_FEE = 7050006 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_ALREADY_PROCESSED = 7050007 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_BLOCKHASH_NOT_FOUND = 7050008 as const;
// `InstructionError` intentionally omitted
export const SOLANA_ERROR__TRANSACTION_ERROR_CALL_CHAIN_TOO_DEEP = 7050009 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_MISSING_SIGNATURE_FOR_FEE = 7050010 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ACCOUNT_INDEX = 7050011 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_SIGNATURE_FAILURE = 7050012 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_PROGRAM_FOR_EXECUTION = 7050013 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_SANITIZE_FAILURE = 7050014 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_CLUSTER_MAINTENANCE = 7050015 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_BORROW_OUTSTANDING = 7050016 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_MAX_BLOCK_COST_LIMIT = 7050017 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_UNSUPPORTED_VERSION = 7050018 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_WRITABLE_ACCOUNT = 7050019 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_MAX_ACCOUNT_COST_LIMIT = 7050020 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_ACCOUNT_DATA_BLOCK_LIMIT = 7050021 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_TOO_MANY_ACCOUNT_LOCKS = 7050022 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_ADDRESS_LOOKUP_TABLE_NOT_FOUND = 7050023 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ADDRESS_LOOKUP_TABLE_OWNER = 7050024 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ADDRESS_LOOKUP_TABLE_DATA = 7050025 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ADDRESS_LOOKUP_TABLE_INDEX = 7050026 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_RENT_PAYING_ACCOUNT = 7050027 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_MAX_VOTE_COST_LIMIT = 7050028 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_ACCOUNT_DATA_TOTAL_LIMIT = 7050029 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION = 7050030 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT = 7050031 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_MAX_LOADED_ACCOUNTS_DATA_SIZE_EXCEEDED = 7050032 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_INVALID_LOADED_ACCOUNTS_DATA_SIZE_LIMIT = 7050033 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_RESANITIZATION_NEEDED = 7050034 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED = 7050035 as const;
export const SOLANA_ERROR__TRANSACTION_ERROR_UNBALANCED_TRANSACTION = 7050036 as const;

/**
* A union of every Solana error code
Expand All @@ -36,4 +75,41 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__INVALID_KEYPAIR_BYTES
| typeof SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED
| typeof SOLANA_ERROR__NONCE_INVALID
| typeof SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND;
| typeof SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND
| typeof SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN
| typeof SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_IN_USE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_LOADED_TWICE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_NOT_FOUND
| typeof SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_ACCOUNT_NOT_FOUND
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_FEE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ACCOUNT_FOR_FEE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_ALREADY_PROCESSED
| typeof SOLANA_ERROR__TRANSACTION_ERROR_BLOCKHASH_NOT_FOUND
| typeof SOLANA_ERROR__TRANSACTION_ERROR_CALL_CHAIN_TOO_DEEP
| typeof SOLANA_ERROR__TRANSACTION_ERROR_MISSING_SIGNATURE_FOR_FEE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ACCOUNT_INDEX
| typeof SOLANA_ERROR__TRANSACTION_ERROR_SIGNATURE_FAILURE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_PROGRAM_FOR_EXECUTION
| typeof SOLANA_ERROR__TRANSACTION_ERROR_SANITIZE_FAILURE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_CLUSTER_MAINTENANCE
| typeof SOLANA_ERROR__TRANSACTION_ERROR_ACCOUNT_BORROW_OUTSTANDING
| typeof SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_MAX_BLOCK_COST_LIMIT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_UNSUPPORTED_VERSION
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_WRITABLE_ACCOUNT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_MAX_ACCOUNT_COST_LIMIT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_ACCOUNT_DATA_BLOCK_LIMIT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_TOO_MANY_ACCOUNT_LOCKS
| typeof SOLANA_ERROR__TRANSACTION_ERROR_ADDRESS_LOOKUP_TABLE_NOT_FOUND
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ADDRESS_LOOKUP_TABLE_OWNER
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ADDRESS_LOOKUP_TABLE_DATA
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_ADDRESS_LOOKUP_TABLE_INDEX
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_RENT_PAYING_ACCOUNT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_MAX_VOTE_COST_LIMIT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_WOULD_EXCEED_ACCOUNT_DATA_TOTAL_LIMIT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_MAX_LOADED_ACCOUNTS_DATA_SIZE_EXCEEDED
| typeof SOLANA_ERROR__TRANSACTION_ERROR_INVALID_LOADED_ACCOUNTS_DATA_SIZE_LIMIT
| typeof SOLANA_ERROR__TRANSACTION_ERROR_RESANITIZATION_NEEDED
| typeof SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED
| typeof SOLANA_ERROR__TRANSACTION_ERROR_UNBALANCED_TRANSACTION;
17 changes: 17 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND,
SOLANA_ERROR__NONCE_INVALID,
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION,
SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT,
SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED,
SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN,
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
SolanaErrorCode,
} from './codes';
Expand Down Expand Up @@ -45,4 +49,17 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
actualNonceValue: string;
expectedNonceValue: string;
};
[SOLANA_ERROR__TRANSACTION_ERROR_UNKNOWN]: {
errorName: string;
transactionErrorContext?: unknown;
};
[SOLANA_ERROR__TRANSACTION_ERROR_DUPLICATE_INSTRUCTION]: {
index: number;
};
[SOLANA_ERROR__TRANSACTION_ERROR_INSUFFICIENT_FUNDS_FOR_RENT]: {
accountIndex: number;
};
[SOLANA_ERROR__TRANSACTION_ERROR_PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED]: {
accountIndex: number;
};
}>;
1 change: 1 addition & 0 deletions packages/errors/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './codes';
export * from './error';
export * from './transaction-error';

0 comments on commit 8541c2e

Please sign in to comment.