-
Notifications
You must be signed in to change notification settings - Fork 911
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace
RpcError
with a coded exception (#2286)
# Summary So wow. I completely missed these as part of #2118, and it turns out they're a really big deal and required a ton of changes. There are: * Errors that have enough data to format a message * Errors that have no data, but also no context in the message * Errors that have no data, but really should, because you can't format a message without it * Preflight errors in which is nested a `TransactionError` (see #2213) In this PR we create a helper that takes in the `RpcSimulateTransactionResult` from the RPC and reformats it as a coded `SolanaError`. As always, everything you need to know is in the `packages/errors/src/__tests__/json-rpc-error-test.ts`. # Test Plan ``` pnpm turbo test:unit:browser pnpm turbo test:unit:node ```
- Loading branch information
1 parent
34ecac6
commit 52a5d3d
Showing
45 changed files
with
957 additions
and
350 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { | ||
SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR, | ||
SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, | ||
SOLANA_ERROR__JSON_RPC__INVALID_REQUEST, | ||
SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND, | ||
SOLANA_ERROR__JSON_RPC__PARSE_ERROR, | ||
SOLANA_ERROR__JSON_RPC__SCAN_ERROR, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SNAPSHOT, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION, | ||
SolanaErrorCode, | ||
} from '../codes'; | ||
import { SolanaErrorContext } from '../context'; | ||
import { SolanaError } from '../error'; | ||
import { getSolanaErrorFromJsonRpcError } from '../json-rpc-error'; | ||
import { getSolanaErrorFromTransactionError } from '../transaction-error'; | ||
|
||
jest.mock('../transaction-error.ts'); | ||
|
||
describe('getSolanaErrorFromJsonRpcError', () => { | ||
it('produces a `SolanaError` with the same code as the one given', () => { | ||
const code = 123 as SolanaErrorCode; | ||
const error = getSolanaErrorFromJsonRpcError({ code, message: 'o no' }); | ||
expect(error).toHaveProperty('context.__code', 123); | ||
}); | ||
describe.each([ | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY, | ||
])('given a %s JSON-RPC error known to have data', jsonRpcErrorCode => { | ||
const expectedData = { baz: 'bat', foo: 'bar' } as unknown as SolanaErrorContext[SolanaErrorCode]; | ||
it('does not set the server message on context', () => { | ||
const error = getSolanaErrorFromJsonRpcError({ | ||
code: jsonRpcErrorCode, | ||
data: expectedData, | ||
message: 'o no', | ||
}); | ||
expect(error).not.toHaveProperty('context.__serverMessage'); | ||
}); | ||
it('produces a `SolanaError` with that data as context', () => { | ||
const error = getSolanaErrorFromJsonRpcError({ | ||
code: jsonRpcErrorCode, | ||
data: expectedData, | ||
message: 'o no', | ||
}); | ||
expect(error).toHaveProperty('context', expect.objectContaining(expectedData)); | ||
}); | ||
}); | ||
describe.each([ | ||
SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR, | ||
SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, | ||
SOLANA_ERROR__JSON_RPC__INVALID_REQUEST, | ||
SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND, | ||
SOLANA_ERROR__JSON_RPC__PARSE_ERROR, | ||
SOLANA_ERROR__JSON_RPC__SCAN_ERROR, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION, | ||
])( | ||
'given a %s JSON-RPC error known to have no data but important context in the server message', | ||
jsonRpcErrorCode => { | ||
it('produces a `SolanaError` with the server message on the context', () => { | ||
const error = getSolanaErrorFromJsonRpcError({ code: jsonRpcErrorCode, message: 'o no' }); | ||
expect(error).toHaveProperty('context.__serverMessage', 'o no'); | ||
}); | ||
}, | ||
); | ||
describe.each([ | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SNAPSHOT, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH, | ||
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE, | ||
])( | ||
'given a %s JSON-RPC error known to have neither data nor important context in the server message', | ||
jsonRpcErrorCode => { | ||
it('produces a `SolanaError` without the server message on the context', () => { | ||
const error = getSolanaErrorFromJsonRpcError({ code: jsonRpcErrorCode, message: 'o no' }); | ||
expect(error).not.toHaveProperty('context.__serverMessage', 'o no'); | ||
}); | ||
}, | ||
); | ||
describe.each([[1, 2, 3], Symbol('a symbol'), 1, 1n, true, false])('when given non-object data like `%s`', data => { | ||
it('does not add the data to `context`', () => { | ||
const error = getSolanaErrorFromJsonRpcError({ | ||
code: 123, | ||
data, | ||
message: 'o no', | ||
}); | ||
expect(error).toHaveProperty( | ||
'context', | ||
// Implies exact match; `context` contains nothing but the `__code` | ||
{ __code: 123 }, | ||
); | ||
}); | ||
}); | ||
describe('when passed a preflight failure', () => { | ||
it('produces a `SolanaError` with the transaction error as the `cause`', () => { | ||
const mockErrorResult = Symbol() as unknown as SolanaError; | ||
jest.mocked(getSolanaErrorFromTransactionError).mockReturnValue(mockErrorResult); | ||
const error = getSolanaErrorFromJsonRpcError({ | ||
code: SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, | ||
data: { err: Symbol() }, | ||
message: 'o no', | ||
}); | ||
expect(error.cause).toBe(mockErrorResult); | ||
}); | ||
it('produces a `SolanaError` with the preflight failure data (minus the `err` property) as the context', () => { | ||
const preflightErrorData = { bar: 2, baz: 3, foo: 1 }; | ||
const error = getSolanaErrorFromJsonRpcError({ | ||
code: SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, | ||
data: { ...preflightErrorData, err: Symbol() }, | ||
message: 'o no', | ||
}); | ||
expect(error.context).toEqual({ | ||
__code: SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, | ||
...preflightErrorData, | ||
}); | ||
}); | ||
it('delegates `err` to the transaction error getter', () => { | ||
const transactionError = Symbol(); | ||
getSolanaErrorFromJsonRpcError({ | ||
code: SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, | ||
data: { err: transactionError }, | ||
message: 'o no', | ||
}); | ||
expect(getSolanaErrorFromTransactionError).toHaveBeenCalledWith(transactionError); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export * from './codes'; | ||
export * from './error'; | ||
export * from './json-rpc-error'; | ||
export * from './instruction-error'; | ||
export * from './transaction-error'; |
Oops, something went wrong.