Skip to content

Commit dd2096e

Browse files
authored
Convert SolanaRpcIntegerOverflowError to a coded error (#2090)
# 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 ```
1 parent 64d583b commit dd2096e

File tree

8 files changed

+160
-62
lines changed

8 files changed

+160
-62
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { SOLANA_ERROR__RPC_INTEGER_OVERFLOW } from '../codes';
2+
import { SolanaError } from '../error';
3+
4+
describe('SOLANA_ERROR__RPC_INTEGER_OVERFLOW', () => {
5+
beforeEach(() => {
6+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7+
(globalThis as any).__DEV__ = true;
8+
});
9+
it('features an informative error message for a path-less violation', () => {
10+
expect(
11+
new SolanaError(SOLANA_ERROR__RPC_INTEGER_OVERFLOW, {
12+
argumentLabel: '3rd',
13+
keyPath: [2 /* third argument */],
14+
methodName: 'someMethod',
15+
optionalPathLabel: '',
16+
value: 1n,
17+
}),
18+
).toHaveProperty(
19+
'message',
20+
'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`',
21+
);
22+
});
23+
it('features an informative error message for a violation with a deep path', () => {
24+
expect(
25+
new SolanaError(SOLANA_ERROR__RPC_INTEGER_OVERFLOW, {
26+
argumentLabel: '1st',
27+
keyPath: [0 /* first argument */, 'foo', 'bar'],
28+
methodName: 'someMethod',
29+
optionalPathLabel: ' at path `foo.bar`',
30+
path: 'foo.bar',
31+
value: 1n,
32+
}),
33+
).toHaveProperty(
34+
'message',
35+
'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`',
36+
);
37+
});
38+
});

packages/errors/src/codes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
export const SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES = 1 as const;
1010
export const SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE = 2 as const;
11+
export const SOLANA_ERROR__RPC_INTEGER_OVERFLOW = 3 as const;
1112

1213
/**
1314
* A union of every Solana error code
@@ -26,4 +27,5 @@ export const SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE = 2 as const;
2627
*/
2728
export type SolanaErrorCode =
2829
| typeof SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES
29-
| typeof SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE;
30+
| typeof SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE
31+
| typeof SOLANA_ERROR__RPC_INTEGER_OVERFLOW;

packages/errors/src/context.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES, SolanaErrorCode } from './codes';
1+
import {
2+
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
3+
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
4+
SolanaErrorCode,
5+
} from './codes';
26

37
export type DefaultUnspecifiedErrorContextToUndefined<T> = {
48
[P in SolanaErrorCode]: P extends keyof T ? T[P] : undefined;
@@ -15,4 +19,12 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
1519
[SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES]: {
1620
addresses: string[];
1721
};
22+
[SOLANA_ERROR__RPC_INTEGER_OVERFLOW]: {
23+
argumentLabel: string;
24+
keyPath: readonly (string | number | symbol)[];
25+
methodName: string;
26+
optionalPathLabel: string;
27+
path?: string;
28+
value: bigint;
29+
};
1830
}>;

packages/errors/src/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
23
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
34
SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE,
45
SolanaErrorCode,
@@ -16,6 +17,10 @@ export const SolanaErrorMessages: Readonly<{
1617
// TypeScript will fail to build this project if add an error code without a message.
1718
[P in SolanaErrorCode]: string;
1819
}> = {
20+
[SOLANA_ERROR__RPC_INTEGER_OVERFLOW]:
21+
'The $argumentLabel argument to the `$methodName` RPC method$optionalPathLabel was ' +
22+
'`$value`. This number is unsafe for use with the Solana JSON-RPC because it exceeds ' +
23+
'`Number.MAX_SAFE_INTEGER`',
1924
[SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES]: 'Transaction is missing signatures for addresses: $addresses',
2025
[SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE]:
2126
"Could not determine this transaction's signature. Make sure that the transaction has " +
Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,64 @@
1-
import { SolanaJsonRpcIntegerOverflowError } from '../rpc-integer-overflow-error';
1+
import { SOLANA_ERROR__RPC_INTEGER_OVERFLOW, SolanaError } from '@solana/errors';
22

3-
describe('SolanaJsonRpcIntegerOverflowError', () => {
4-
it('features an informative error message', () => {
5-
expect(new SolanaJsonRpcIntegerOverflowError('someMethod', [2 /* third argument */], 1n)).toMatchInlineSnapshot(
6-
`[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\`.]`,
7-
);
3+
import { createSolanaJsonRpcIntegerOverflowError } from '../rpc-integer-overflow-error';
4+
5+
describe('createSolanaJsonRpcIntegerOverflowError()', () => {
6+
it('creates a `SolanaError`', () => {
7+
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [2 /* third argument */], 1n);
8+
expect(error).toBeInstanceOf(SolanaError);
9+
});
10+
it('creates a `SolanaError` with the code `SOLANA_ERROR__RPC_INTEGER_OVERFLOW`', () => {
11+
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [2 /* third argument */], 1n);
12+
expect(error).toHaveProperty('context.__code', SOLANA_ERROR__RPC_INTEGER_OVERFLOW);
813
});
9-
it('includes the full path to the value in the error message', () => {
10-
expect(
11-
new SolanaJsonRpcIntegerOverflowError('someMethod', [0 /* first argument */, 'foo', 'bar'], 1n),
12-
).toMatchInlineSnapshot(
13-
`[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\`.]`,
14+
it('creates a `SolanaError` with the correct context for a path-less violation', () => {
15+
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [2 /* third argument */], 1n);
16+
expect(error).toEqual(
17+
new SolanaError(SOLANA_ERROR__RPC_INTEGER_OVERFLOW, {
18+
argumentLabel: '3rd',
19+
keyPath: [2],
20+
methodName: 'someMethod',
21+
optionalPathLabel: '',
22+
value: 1n,
23+
}),
1424
);
1525
});
16-
it('exposes the method name, key path, and the value that overflowed', () => {
17-
expect(new SolanaJsonRpcIntegerOverflowError('someMethod', [0, 'foo', 'bar'], 1n)).toMatchObject({
18-
keyPath: [0, 'foo', 'bar'],
19-
methodName: 'someMethod',
20-
value: 1n,
21-
});
26+
it('creates a `SolanaError` with the correct context for a violation with a deep path', () => {
27+
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [0 /* first argument */, 'foo', 'bar'], 1n);
28+
expect(error).toHaveProperty('context.optionalPathLabel', ' at path `foo.bar`');
29+
expect(error).toHaveProperty('context.path', 'foo.bar');
30+
});
31+
it('omits the error factory function itself from the stack trace', () => {
32+
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [0 /* first argument */, 'foo', 'bar'], 1n);
33+
expect(error.stack).not.toMatch(/createSolanaJsonRpcIntegerOverflowError/);
34+
});
35+
it.each(
36+
Object.entries({
37+
...(() => {
38+
const out: Record<number, string> = {};
39+
Array.from({ length: 100 }).forEach((_, ii) => {
40+
const lastDigit = ii % 10;
41+
// eslint-disable-next-line jest/no-conditional-in-test
42+
if (lastDigit === 0) {
43+
out[ii] = `${ii + 1}st`;
44+
// eslint-disable-next-line jest/no-conditional-in-test
45+
} else if (lastDigit === 1) {
46+
out[ii] = `${ii + 1}nd`;
47+
// eslint-disable-next-line jest/no-conditional-in-test
48+
} else if (lastDigit === 2) {
49+
out[ii] = `${ii + 1}rd`;
50+
} else {
51+
out[ii] = `${ii + 1}th`;
52+
}
53+
});
54+
return out;
55+
})(),
56+
10: '11th',
57+
11: '12th',
58+
12: '13th',
59+
}),
60+
)('computes the correct ordinal when crafting the argument label', (index, expectedLabel) => {
61+
const error = createSolanaJsonRpcIntegerOverflowError('someMethod', [parseInt(index, 10)], 1n);
62+
expect(error).toHaveProperty('context.argumentLabel', expectedLabel);
2263
});
2364
});

packages/library/src/__tests__/rpc-integer-overflow-test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { SolanaError } from '@solana/errors';
12
import type { IRpcTransport } from '@solana/rpc-transport';
23

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

66
describe('RPC integer overflow behavior', () => {
77
let rpc: ReturnType<typeof createSolanaRpc>;
@@ -27,11 +27,11 @@ describe('RPC integer overflow behavior', () => {
2727
it('throws when called with a value greater than `Number.MAX_SAFE_INTEGER`', () => {
2828
expect(() => {
2929
rpc.getBlocks(BigInt(Number.MAX_SAFE_INTEGER) + 1n);
30-
}).toThrow(SolanaJsonRpcIntegerOverflowError);
30+
}).toThrow(SolanaError);
3131
});
3232
it('throws when called with a value less than `-Number.MAX_SAFE_INTEGER`', () => {
3333
expect(() => {
3434
rpc.getBlocks(BigInt(-Number.MAX_SAFE_INTEGER) - 1n);
35-
}).toThrow(SolanaJsonRpcIntegerOverflowError);
35+
}).toThrow(SolanaError);
3636
});
3737
});
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { createSolanaRpcApi } from '@solana/rpc-core';
22

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

55
export const DEFAULT_RPC_CONFIG: Partial<Parameters<typeof createSolanaRpcApi>[0]> = {
66
defaultCommitment: 'confirmed',
77
onIntegerOverflow(methodName, keyPath, value) {
8-
throw new SolanaJsonRpcIntegerOverflowError(methodName, keyPath, value);
8+
throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value);
99
},
1010
};
Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,45 @@
1+
import { SOLANA_ERROR__RPC_INTEGER_OVERFLOW, SolanaError } from '@solana/errors';
12
import { KeyPath } from '@solana/rpc-core/dist/types/tree-traversal';
23

3-
export class SolanaJsonRpcIntegerOverflowError extends Error {
4-
readonly methodName: string;
5-
readonly keyPath: KeyPath;
6-
readonly value: bigint;
7-
constructor(methodName: string, keyPath: KeyPath, value: bigint) {
8-
let argumentLabel = '';
9-
if (typeof keyPath[0] === 'number') {
10-
const argPosition = keyPath[0] + 1;
11-
const lastDigit = argPosition % 10;
12-
const lastTwoDigits = argPosition % 100;
13-
if (lastDigit == 1 && lastTwoDigits != 11) {
14-
argumentLabel = argPosition + 'st';
15-
} else if (lastDigit == 2 && lastTwoDigits != 12) {
16-
argumentLabel = argPosition + 'nd';
17-
} else if (lastDigit == 3 && lastTwoDigits != 13) {
18-
argumentLabel = argPosition + 'rd';
19-
} else {
20-
argumentLabel = argPosition + 'th';
21-
}
4+
export function createSolanaJsonRpcIntegerOverflowError(
5+
methodName: string,
6+
keyPath: KeyPath,
7+
value: bigint,
8+
): SolanaError<typeof SOLANA_ERROR__RPC_INTEGER_OVERFLOW> {
9+
let argumentLabel = '';
10+
if (typeof keyPath[0] === 'number') {
11+
const argPosition = keyPath[0] + 1;
12+
const lastDigit = argPosition % 10;
13+
const lastTwoDigits = argPosition % 100;
14+
if (lastDigit == 1 && lastTwoDigits != 11) {
15+
argumentLabel = argPosition + 'st';
16+
} else if (lastDigit == 2 && lastTwoDigits != 12) {
17+
argumentLabel = argPosition + 'nd';
18+
} else if (lastDigit == 3 && lastTwoDigits != 13) {
19+
argumentLabel = argPosition + 'rd';
2220
} else {
23-
argumentLabel = `\`${keyPath[0].toString()}\``;
21+
argumentLabel = argPosition + 'th';
2422
}
25-
const path =
26-
keyPath.length > 1
27-
? keyPath
28-
.slice(1)
29-
.map(pathPart => (typeof pathPart === 'number' ? `[${pathPart}]` : pathPart))
30-
.join('.')
31-
: null;
32-
super(
33-
`The ${argumentLabel} argument to the \`${methodName}\` RPC method` +
34-
`${path ? ` at path \`${path}\`` : ''} was \`${value}\`. This number is ` +
35-
'unsafe for use with the Solana JSON-RPC because it exceeds ' +
36-
'`Number.MAX_SAFE_INTEGER`.',
37-
);
38-
this.keyPath = keyPath;
39-
this.methodName = methodName;
40-
this.value = value;
23+
} else {
24+
argumentLabel = `\`${keyPath[0].toString()}\``;
4125
}
42-
get name() {
43-
return 'SolanaJsonRpcIntegerOverflowError';
26+
const path =
27+
keyPath.length > 1
28+
? keyPath
29+
.slice(1)
30+
.map(pathPart => (typeof pathPart === 'number' ? `[${pathPart}]` : pathPart))
31+
.join('.')
32+
: undefined;
33+
const error = new SolanaError(SOLANA_ERROR__RPC_INTEGER_OVERFLOW, {
34+
argumentLabel,
35+
keyPath: keyPath as readonly (string | number | symbol)[],
36+
methodName,
37+
optionalPathLabel: path ? ` at path \`${path}\`` : '',
38+
value,
39+
...(path !== undefined ? { path } : undefined),
40+
});
41+
if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') {
42+
Error.captureStackTrace(error, createSolanaJsonRpcIntegerOverflowError);
4443
}
44+
return error;
4545
}

0 commit comments

Comments
 (0)