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

Commit

Permalink
Convert SolanaRpcIntegerOverflowError to a coded error (#2090)
Browse files Browse the repository at this point in the history
# Summary

A whole lot of code here, just to evict the error message from the bundle. Saved 88 whole gzipped bytes.

# Test plan

```shell
cd packages/library/
pnpm turbo test:unit:browser test:unit:node
cd ../errors/
pnpm turbo test:unit:browser test:unit:node
```
  • Loading branch information
steveluscher authored Feb 5, 2024
1 parent 64d583b commit dd2096e
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 62 deletions.
38 changes: 38 additions & 0 deletions packages/errors/src/__tests__/RPC_INTEGER_OVERFLOW-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { SOLANA_ERROR__RPC_INTEGER_OVERFLOW } from '../codes';
import { SolanaError } from '../error';

describe('SOLANA_ERROR__RPC_INTEGER_OVERFLOW', () => {
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).__DEV__ = true;
});
it('features an informative error message for a path-less violation', () => {
expect(
new SolanaError(SOLANA_ERROR__RPC_INTEGER_OVERFLOW, {
argumentLabel: '3rd',
keyPath: [2 /* third argument */],
methodName: 'someMethod',
optionalPathLabel: '',
value: 1n,
}),
).toHaveProperty(
'message',
'The 3rd argument to the `someMethod` RPC method was `1`. This number is unsafe for use with the Solana JSON-RPC because it exceeds `Number.MAX_SAFE_INTEGER`',
);
});
it('features an informative error message for a violation with a deep path', () => {
expect(
new SolanaError(SOLANA_ERROR__RPC_INTEGER_OVERFLOW, {
argumentLabel: '1st',
keyPath: [0 /* first argument */, 'foo', 'bar'],
methodName: 'someMethod',
optionalPathLabel: ' at path `foo.bar`',
path: 'foo.bar',
value: 1n,
}),
).toHaveProperty(
'message',
'The 1st argument to the `someMethod` RPC method at path `foo.bar` was `1`. This number is unsafe for use with the Solana JSON-RPC because it exceeds `Number.MAX_SAFE_INTEGER`',
);
});
});
4 changes: 3 additions & 1 deletion packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
export const SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES = 1 as const;
export const SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE = 2 as const;
export const SOLANA_ERROR__RPC_INTEGER_OVERFLOW = 3 as const;

/**
* A union of every Solana error code
Expand All @@ -26,4 +27,5 @@ export const SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE = 2 as const;
*/
export type SolanaErrorCode =
| typeof SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES
| typeof SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE;
| typeof SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE
| typeof SOLANA_ERROR__RPC_INTEGER_OVERFLOW;
14 changes: 13 additions & 1 deletion packages/errors/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES, SolanaErrorCode } from './codes';
import {
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
SolanaErrorCode,
} from './codes';

export type DefaultUnspecifiedErrorContextToUndefined<T> = {
[P in SolanaErrorCode]: P extends keyof T ? T[P] : undefined;
Expand All @@ -15,4 +19,12 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
[SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES]: {
addresses: string[];
};
[SOLANA_ERROR__RPC_INTEGER_OVERFLOW]: {
argumentLabel: string;
keyPath: readonly (string | number | symbol)[];
methodName: string;
optionalPathLabel: string;
path?: string;
value: bigint;
};
}>;
5 changes: 5 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE,
SolanaErrorCode,
Expand All @@ -16,6 +17,10 @@ export const SolanaErrorMessages: Readonly<{
// TypeScript will fail to build this project if add an error code without a message.
[P in SolanaErrorCode]: string;
}> = {
[SOLANA_ERROR__RPC_INTEGER_OVERFLOW]:
'The $argumentLabel argument to the `$methodName` RPC method$optionalPathLabel was ' +
'`$value`. This number is unsafe for use with the Solana JSON-RPC because it exceeds ' +
'`Number.MAX_SAFE_INTEGER`',
[SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES]: 'Transaction is missing signatures for addresses: $addresses',
[SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE]:
"Could not determine this transaction's signature. Make sure that the transaction has " +
Expand Down
75 changes: 58 additions & 17 deletions packages/library/src/__tests__/rpc-integer-overflow-error-test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,64 @@
import { SolanaJsonRpcIntegerOverflowError } from '../rpc-integer-overflow-error';
import { SOLANA_ERROR__RPC_INTEGER_OVERFLOW, SolanaError } from '@solana/errors';

describe('SolanaJsonRpcIntegerOverflowError', () => {
it('features an informative error message', () => {
expect(new SolanaJsonRpcIntegerOverflowError('someMethod', [2 /* third argument */], 1n)).toMatchInlineSnapshot(
`[SolanaJsonRpcIntegerOverflowError: The 3rd argument to the \`someMethod\` RPC method was \`1\`. This number is unsafe for use with the Solana JSON-RPC because it exceeds \`Number.MAX_SAFE_INTEGER\`.]`,
);
import { createSolanaJsonRpcIntegerOverflowError } from '../rpc-integer-overflow-error';

describe('createSolanaJsonRpcIntegerOverflowError()', () => {
it('creates a `SolanaError`', () => {
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [2 /* third argument */], 1n);
expect(error).toBeInstanceOf(SolanaError);
});
it('creates a `SolanaError` with the code `SOLANA_ERROR__RPC_INTEGER_OVERFLOW`', () => {
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [2 /* third argument */], 1n);
expect(error).toHaveProperty('context.__code', SOLANA_ERROR__RPC_INTEGER_OVERFLOW);
});
it('includes the full path to the value in the error message', () => {
expect(
new SolanaJsonRpcIntegerOverflowError('someMethod', [0 /* first argument */, 'foo', 'bar'], 1n),
).toMatchInlineSnapshot(
`[SolanaJsonRpcIntegerOverflowError: The 1st argument to the \`someMethod\` RPC method at path \`foo.bar\` was \`1\`. This number is unsafe for use with the Solana JSON-RPC because it exceeds \`Number.MAX_SAFE_INTEGER\`.]`,
it('creates a `SolanaError` with the correct context for a path-less violation', () => {
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [2 /* third argument */], 1n);
expect(error).toEqual(
new SolanaError(SOLANA_ERROR__RPC_INTEGER_OVERFLOW, {
argumentLabel: '3rd',
keyPath: [2],
methodName: 'someMethod',
optionalPathLabel: '',
value: 1n,
}),
);
});
it('exposes the method name, key path, and the value that overflowed', () => {
expect(new SolanaJsonRpcIntegerOverflowError('someMethod', [0, 'foo', 'bar'], 1n)).toMatchObject({
keyPath: [0, 'foo', 'bar'],
methodName: 'someMethod',
value: 1n,
});
it('creates a `SolanaError` with the correct context for a violation with a deep path', () => {
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [0 /* first argument */, 'foo', 'bar'], 1n);
expect(error).toHaveProperty('context.optionalPathLabel', ' at path `foo.bar`');
expect(error).toHaveProperty('context.path', 'foo.bar');
});
it('omits the error factory function itself from the stack trace', () => {
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [0 /* first argument */, 'foo', 'bar'], 1n);
expect(error.stack).not.toMatch(/createSolanaJsonRpcIntegerOverflowError/);
});
it.each(
Object.entries({
...(() => {
const out: Record<number, string> = {};
Array.from({ length: 100 }).forEach((_, ii) => {
const lastDigit = ii % 10;
// eslint-disable-next-line jest/no-conditional-in-test
if (lastDigit === 0) {
out[ii] = `${ii + 1}st`;
// eslint-disable-next-line jest/no-conditional-in-test
} else if (lastDigit === 1) {
out[ii] = `${ii + 1}nd`;
// eslint-disable-next-line jest/no-conditional-in-test
} else if (lastDigit === 2) {
out[ii] = `${ii + 1}rd`;
} else {
out[ii] = `${ii + 1}th`;
}
});
return out;
})(),
10: '11th',
11: '12th',
12: '13th',
}),
)('computes the correct ordinal when crafting the argument label', (index, expectedLabel) => {
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [parseInt(index, 10)], 1n);
expect(error).toHaveProperty('context.argumentLabel', expectedLabel);
});
});
6 changes: 3 additions & 3 deletions packages/library/src/__tests__/rpc-integer-overflow-test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SolanaError } from '@solana/errors';
import type { IRpcTransport } from '@solana/rpc-transport';

import { createSolanaRpc } from '../rpc';
import { SolanaJsonRpcIntegerOverflowError } from '../rpc-integer-overflow-error';

describe('RPC integer overflow behavior', () => {
let rpc: ReturnType<typeof createSolanaRpc>;
Expand All @@ -27,11 +27,11 @@ describe('RPC integer overflow behavior', () => {
it('throws when called with a value greater than `Number.MAX_SAFE_INTEGER`', () => {
expect(() => {
rpc.getBlocks(BigInt(Number.MAX_SAFE_INTEGER) + 1n);
}).toThrow(SolanaJsonRpcIntegerOverflowError);
}).toThrow(SolanaError);
});
it('throws when called with a value less than `-Number.MAX_SAFE_INTEGER`', () => {
expect(() => {
rpc.getBlocks(BigInt(-Number.MAX_SAFE_INTEGER) - 1n);
}).toThrow(SolanaJsonRpcIntegerOverflowError);
}).toThrow(SolanaError);
});
});
4 changes: 2 additions & 2 deletions packages/library/src/rpc-default-config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createSolanaRpcApi } from '@solana/rpc-core';

import { SolanaJsonRpcIntegerOverflowError } from './rpc-integer-overflow-error';
import { createSolanaJsonRpcIntegerOverflowError } from './rpc-integer-overflow-error';

export const DEFAULT_RPC_CONFIG: Partial<Parameters<typeof createSolanaRpcApi>[0]> = {
defaultCommitment: 'confirmed',
onIntegerOverflow(methodName, keyPath, value) {
throw new SolanaJsonRpcIntegerOverflowError(methodName, keyPath, value);
throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value);
},
};
76 changes: 38 additions & 38 deletions packages/library/src/rpc-integer-overflow-error.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import { SOLANA_ERROR__RPC_INTEGER_OVERFLOW, SolanaError } from '@solana/errors';
import { KeyPath } from '@solana/rpc-core/dist/types/tree-traversal';

export class SolanaJsonRpcIntegerOverflowError extends Error {
readonly methodName: string;
readonly keyPath: KeyPath;
readonly value: bigint;
constructor(methodName: string, keyPath: KeyPath, value: bigint) {
let argumentLabel = '';
if (typeof keyPath[0] === 'number') {
const argPosition = keyPath[0] + 1;
const lastDigit = argPosition % 10;
const lastTwoDigits = argPosition % 100;
if (lastDigit == 1 && lastTwoDigits != 11) {
argumentLabel = argPosition + 'st';
} else if (lastDigit == 2 && lastTwoDigits != 12) {
argumentLabel = argPosition + 'nd';
} else if (lastDigit == 3 && lastTwoDigits != 13) {
argumentLabel = argPosition + 'rd';
} else {
argumentLabel = argPosition + 'th';
}
export function createSolanaJsonRpcIntegerOverflowError(
methodName: string,
keyPath: KeyPath,
value: bigint,
): SolanaError<typeof SOLANA_ERROR__RPC_INTEGER_OVERFLOW> {
let argumentLabel = '';
if (typeof keyPath[0] === 'number') {
const argPosition = keyPath[0] + 1;
const lastDigit = argPosition % 10;
const lastTwoDigits = argPosition % 100;
if (lastDigit == 1 && lastTwoDigits != 11) {
argumentLabel = argPosition + 'st';
} else if (lastDigit == 2 && lastTwoDigits != 12) {
argumentLabel = argPosition + 'nd';
} else if (lastDigit == 3 && lastTwoDigits != 13) {
argumentLabel = argPosition + 'rd';
} else {
argumentLabel = `\`${keyPath[0].toString()}\``;
argumentLabel = argPosition + 'th';
}
const path =
keyPath.length > 1
? keyPath
.slice(1)
.map(pathPart => (typeof pathPart === 'number' ? `[${pathPart}]` : pathPart))
.join('.')
: null;
super(
`The ${argumentLabel} argument to the \`${methodName}\` RPC method` +
`${path ? ` at path \`${path}\`` : ''} was \`${value}\`. This number is ` +
'unsafe for use with the Solana JSON-RPC because it exceeds ' +
'`Number.MAX_SAFE_INTEGER`.',
);
this.keyPath = keyPath;
this.methodName = methodName;
this.value = value;
} else {
argumentLabel = `\`${keyPath[0].toString()}\``;
}
get name() {
return 'SolanaJsonRpcIntegerOverflowError';
const path =
keyPath.length > 1
? keyPath
.slice(1)
.map(pathPart => (typeof pathPart === 'number' ? `[${pathPart}]` : pathPart))
.join('.')
: undefined;
const error = new SolanaError(SOLANA_ERROR__RPC_INTEGER_OVERFLOW, {
argumentLabel,
keyPath: keyPath as readonly (string | number | symbol)[],
methodName,
optionalPathLabel: path ? ` at path \`${path}\`` : '',
value,
...(path !== undefined ? { path } : undefined),
});
if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(error, createSolanaJsonRpcIntegerOverflowError);
}
return error;
}

0 comments on commit dd2096e

Please sign in to comment.