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

[experimental] Create an error that the RPC can throw when there's an integer overflow #1226

Merged
merged 3 commits into from
Apr 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ yarn-error.log*

# Sapling SCM
.sl

# `solana-test-validator` ledger location
test-ledger/
3 changes: 0 additions & 3 deletions packages/library-legacy/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,5 @@ doc
# VIM swap files
*.sw*

# `solana-test-validator` ledger location
test-ledger/

# TypeScript
declarations
2 changes: 1 addition & 1 deletion packages/library-legacy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"test:lint": "eslint src/ test/ --ext .js,.ts",
"test:lint:fix": "eslint src/ test/ --fix --ext .js,.ts",
"test:live": "TEST_LIVE=1 pnpm run test:unit:node",
"test:live-with-test-validator": "start-server-and-test '$HOME/.local/share/solana/install/active_release/bin/solana-test-validator --reset --quiet' http://127.0.0.1:8899/health test:live",
"test:live-with-test-validator": "start-server-and-test '../../scripts/start-shared-test-validator.sh' http://127.0.0.1:8899/health test:live",
"test:prettier": "prettier --check '{,{src,test}/**/}*.{j,t}s'",
"test:prettier:fix": "prettier --write '{,{src,test}/**/}*.{j,t}s'",
"test:typecheck": "tsc --noEmit",
Expand Down
23 changes: 23 additions & 0 deletions packages/library/src/__tests__/rpc-integer-overflow-error-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SolanaJsonRpcIntegerOverflowError } from '../rpc-integer-overflow-error';

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\`.]`
);
});
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('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,
});
});
});
39 changes: 39 additions & 0 deletions packages/library/src/rpc-integer-overflow-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export class SolanaJsonRpcIntegerOverflowError extends Error {
readonly methodName: string;
readonly keyPath: (number | string)[];
readonly value: bigint;
constructor(methodName: string, keyPath: (string | number)[], value: bigint) {
const argPosition = (typeof keyPath[0] === 'number' ? keyPath[0] : parseInt(keyPath[0], 10)) + 1;
let ordinal = '';
const lastDigit = argPosition % 10;
const lastTwoDigits = argPosition % 100;
if (lastDigit == 1 && lastTwoDigits != 11) {
ordinal = argPosition + 'st';
} else if (lastDigit == 2 && lastTwoDigits != 12) {
ordinal = argPosition + 'nd';
} else if (lastDigit == 3 && lastTwoDigits != 13) {
ordinal = argPosition + 'rd';
} else {
ordinal = argPosition + 'th';
}
const path =
keyPath.length > 1
? keyPath
.slice(1)
.map(pathPart => (typeof pathPart === 'number' ? `[${pathPart}]` : pathPart))
.join('.')
: null;
super(
`The ${ordinal} 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;
}
get name() {
return 'SolanaJsonRpcIntegerOverflowError';
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createJsonRpcTransport } from '../json-rpc-transport';
import { createJsonRpcTransport } from '..';
import { Transport } from '../json-rpc-transport-types';
import { patchParamsForSolanaLabsRpc } from '../params-patcher';
import { patchParamsForSolanaLabsRpc } from '../../params-patcher';

jest.mock('../params-patcher');
jest.mock('../../params-patcher');

interface TestRpcApi {
someMethod(...args: unknown[]): unknown;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { makeHttpRequest } from '../http-request';
import { makeHttpRequest } from '../../http-request';
import { SolanaJsonRpcError } from '../json-rpc-errors';
import { createJsonRpcMessage } from '../json-rpc-message';
import { getNextMessageId } from '../json-rpc-message-id';
import { createJsonRpcTransport } from '../json-rpc-transport';
import { createJsonRpcTransport } from '..';
import { Transport } from '../json-rpc-transport-types';

jest.mock('../http-request');
jest.mock('../../http-request');
jest.mock('../json-rpc-message-id');

interface TestRpcApi {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { makeHttpRequest } from './http-request';
import { makeHttpRequest } from '../http-request';
import { SolanaJsonRpcError } from './json-rpc-errors';
import { createJsonRpcMessage } from './json-rpc-message';
import { ArmedBatchTransport, ArmedTransport, Transport } from './json-rpc-transport-types';
import { patchParamsForSolanaLabsRpc } from './params-patcher';
import { patchParamsForSolanaLabsRpc } from '../params-patcher';

interface IHasIdentifier {
readonly id: number;
Expand Down
38 changes: 38 additions & 0 deletions scripts/start-shared-test-validator.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash

# Run this script when you want to start a test validator.
# Multiple callers can invoke this script.
# Only the first caller will start the test validator.
# Only the last caller to release the lock will shut the validator down.

EXCLUSIVE_LOCK_FILE="/var/lock/.solanatestvalidator.exclusivelock"
SHARED_LOCK_FILE="/var/lock/.solanatestvalidator.sharedlock"
TEST_VALIDATOR=$HOME/.local/share/solana/install/active_release/bin/solana-test-validator
TEST_VALIDATOR_LEDGER="$( cd "$(dirname "${BASH_SOURCE[0]}")/.." ; pwd -P )/test-ledger"

(
trap : INT # Resume execution any time we receive SIGINT
# Obtain lock on $SHARED_LOCK_FILE (fd 200)
flock -s 200 || exit 1
(
if flock -nx 200; then
$TEST_VALIDATOR --ledger $TEST_VALIDATOR_LEDGER --reset --quiet >/dev/null &
validator_pid=$!
echo "Started test validator (PID $validator_pid)"
wait
else
echo "Sharing lock on already running test validator (PID $(pidof $TEST_VALIDATOR))"
sleep infinity
fi
) 200>$EXCLUSIVE_LOCK_FILE
validator_pid=$(pidof $TEST_VALIDATOR)
if (exit $?) && ! lsof -p ^$BASHPID -p ^$validator_pid $SHARED_LOCK_FILE >/dev/null; then
# We are the last caller with a lock. Kill the validator now.
echo "Terminating test validator (PID $validator_pid)"
kill -n 15 $validator_pid
# Wait until we are the last caller with the lock. This way when we exit, we know the lock dies.
(flock -x 200) 200>$EXCLUSIVE_LOCK_FILE
else
echo "Leaving test validator (PID $validator_pid) running for other lock participants"
fi
) 200>$SHARED_LOCK_FILE