fix: [web3] only ever send RPC socket messages when the socket is open (#29195)
This commit is contained in:
parent
1ca78845bb
commit
8921b0a35a
|
@ -110,6 +110,10 @@ function generateConfig(configType, format) {
|
||||||
'jayson/lib/client/browser',
|
'jayson/lib/client/browser',
|
||||||
'node-fetch',
|
'node-fetch',
|
||||||
'rpc-websockets',
|
'rpc-websockets',
|
||||||
|
'rpc-websockets/dist/lib/client',
|
||||||
|
'rpc-websockets/dist/lib/client/client.types',
|
||||||
|
'rpc-websockets/dist/lib/client/websocket',
|
||||||
|
'rpc-websockets/dist/lib/client/websocket.browser',
|
||||||
'superstruct',
|
'superstruct',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -179,6 +183,10 @@ function generateConfig(configType, format) {
|
||||||
'node-fetch',
|
'node-fetch',
|
||||||
'react-native-url-polyfill',
|
'react-native-url-polyfill',
|
||||||
'rpc-websockets',
|
'rpc-websockets',
|
||||||
|
'rpc-websockets/dist/lib/client',
|
||||||
|
'rpc-websockets/dist/lib/client/client.types',
|
||||||
|
'rpc-websockets/dist/lib/client/websocket',
|
||||||
|
'rpc-websockets/dist/lib/client/websocket.browser',
|
||||||
'superstruct',
|
'superstruct',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export {default} from 'rpc-websockets/dist/lib/client/websocket.browser';
|
|
@ -0,0 +1 @@
|
||||||
|
export {default} from 'rpc-websockets/dist/lib/client/websocket.browser';
|
|
@ -26,7 +26,6 @@ import {
|
||||||
any,
|
any,
|
||||||
} from 'superstruct';
|
} from 'superstruct';
|
||||||
import type {Struct} from 'superstruct';
|
import type {Struct} from 'superstruct';
|
||||||
import {Client as RpcWebSocketClient} from 'rpc-websockets';
|
|
||||||
import RpcClient from 'jayson/lib/client/browser';
|
import RpcClient from 'jayson/lib/client/browser';
|
||||||
import {JSONRPCError} from 'jayson';
|
import {JSONRPCError} from 'jayson';
|
||||||
|
|
||||||
|
@ -36,6 +35,7 @@ import fetchImpl, {Response} from './fetch-impl';
|
||||||
import {DurableNonce, NonceAccount} from './nonce-account';
|
import {DurableNonce, NonceAccount} from './nonce-account';
|
||||||
import {PublicKey} from './publickey';
|
import {PublicKey} from './publickey';
|
||||||
import {Signer} from './keypair';
|
import {Signer} from './keypair';
|
||||||
|
import RpcWebSocketClient from './rpc-websocket';
|
||||||
import {MS_PER_SLOT} from './timing';
|
import {MS_PER_SLOT} from './timing';
|
||||||
import {
|
import {
|
||||||
Transaction,
|
Transaction,
|
||||||
|
@ -5803,7 +5803,12 @@ export class Connection {
|
||||||
this._rpcWebSocketConnected = true;
|
this._rpcWebSocketConnected = true;
|
||||||
this._rpcWebSocketHeartbeat = setInterval(() => {
|
this._rpcWebSocketHeartbeat = setInterval(() => {
|
||||||
// Ping server every 5s to prevent idle timeouts
|
// Ping server every 5s to prevent idle timeouts
|
||||||
this._rpcWebSocket.notify('ping').catch(() => {});
|
(async () => {
|
||||||
|
try {
|
||||||
|
await this._rpcWebSocket.notify('ping');
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch {}
|
||||||
|
})();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
this._updateSubscriptions();
|
this._updateSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import {ICommonWebSocketFactory} from 'rpc-websockets/dist/lib/client/client.types';
|
||||||
|
import WebsocketFactory from 'rpc-websockets/dist/lib/client/websocket';
|
||||||
|
|
||||||
|
export default WebsocketFactory as ICommonWebSocketFactory;
|
|
@ -0,0 +1,79 @@
|
||||||
|
import RpcWebSocketCommonClient from 'rpc-websockets/dist/lib/client';
|
||||||
|
import RpcWebSocketBrowserFactory from 'rpc-websockets/dist/lib/client/websocket.browser';
|
||||||
|
import {
|
||||||
|
ICommonWebSocket,
|
||||||
|
IWSClientAdditionalOptions,
|
||||||
|
NodeWebSocketType,
|
||||||
|
NodeWebSocketTypeOptions,
|
||||||
|
} from 'rpc-websockets/dist/lib/client/client.types';
|
||||||
|
|
||||||
|
import createRpc from './rpc-websocket-factory';
|
||||||
|
|
||||||
|
interface IHasReadyState {
|
||||||
|
readyState: WebSocket['readyState'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RpcWebSocketClient extends RpcWebSocketCommonClient {
|
||||||
|
private underlyingSocket: IHasReadyState | undefined;
|
||||||
|
constructor(
|
||||||
|
address?: string,
|
||||||
|
options?: IWSClientAdditionalOptions & NodeWebSocketTypeOptions,
|
||||||
|
generate_request_id?: (
|
||||||
|
method: string,
|
||||||
|
params: object | Array<any>,
|
||||||
|
) => number,
|
||||||
|
) {
|
||||||
|
const webSocketFactory = (url: string) => {
|
||||||
|
const rpc = createRpc(url, {
|
||||||
|
autoconnect: true,
|
||||||
|
max_reconnects: 5,
|
||||||
|
reconnect: true,
|
||||||
|
reconnect_interval: 1000,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
if ('socket' in rpc) {
|
||||||
|
this.underlyingSocket = (
|
||||||
|
rpc as ReturnType<typeof RpcWebSocketBrowserFactory>
|
||||||
|
).socket;
|
||||||
|
} else {
|
||||||
|
this.underlyingSocket = rpc as NodeWebSocketType;
|
||||||
|
}
|
||||||
|
return rpc as ICommonWebSocket;
|
||||||
|
};
|
||||||
|
super(webSocketFactory, address, options, generate_request_id);
|
||||||
|
}
|
||||||
|
call(
|
||||||
|
...args: Parameters<RpcWebSocketCommonClient['call']>
|
||||||
|
): ReturnType<RpcWebSocketCommonClient['call']> {
|
||||||
|
const readyState = this.underlyingSocket?.readyState;
|
||||||
|
if (readyState === 1 /* WebSocket.OPEN */) {
|
||||||
|
return super.call(...args);
|
||||||
|
}
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
'Tried to call a JSON-RPC method `' +
|
||||||
|
args[0] +
|
||||||
|
'` but the socket was not `CONNECTING` or `OPEN` (`readyState` was ' +
|
||||||
|
readyState +
|
||||||
|
')',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
notify(
|
||||||
|
...args: Parameters<RpcWebSocketCommonClient['notify']>
|
||||||
|
): ReturnType<RpcWebSocketCommonClient['notify']> {
|
||||||
|
const readyState = this.underlyingSocket?.readyState;
|
||||||
|
if (readyState === 1 /* WebSocket.OPEN */) {
|
||||||
|
return super.notify(...args);
|
||||||
|
}
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
'Tried to send a JSON-RPC notification `' +
|
||||||
|
args[0] +
|
||||||
|
'` but the socket was not `CONNECTING` or `OPEN` (`readyState` was ' +
|
||||||
|
readyState +
|
||||||
|
')',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import chai from 'chai';
|
import chai from 'chai';
|
||||||
import {Client} from 'rpc-websockets';
|
|
||||||
import {stub, SinonStubbedInstance, SinonSpy, spy} from 'sinon';
|
import {stub, SinonStubbedInstance, SinonSpy, spy} from 'sinon';
|
||||||
import sinonChai from 'sinon-chai';
|
import sinonChai from 'sinon-chai';
|
||||||
|
|
||||||
|
@ -15,6 +14,7 @@ import {
|
||||||
SlotChangeCallback,
|
SlotChangeCallback,
|
||||||
SlotUpdateCallback,
|
SlotUpdateCallback,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
import type Client from '../src/rpc-websocket';
|
||||||
import {url} from './url';
|
import {url} from './url';
|
||||||
|
|
||||||
chai.use(sinonChai);
|
chai.use(sinonChai);
|
||||||
|
|
|
@ -58,7 +58,7 @@ import {
|
||||||
stubRpcWebSocket,
|
stubRpcWebSocket,
|
||||||
restoreRpcWebSocket,
|
restoreRpcWebSocket,
|
||||||
mockRpcMessage,
|
mockRpcMessage,
|
||||||
} from './mocks/rpc-websockets';
|
} from './mocks/rpc-websocket';
|
||||||
import {
|
import {
|
||||||
NonceInformation,
|
NonceInformation,
|
||||||
TransactionInstruction,
|
TransactionInstruction,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import bs58 from 'bs58';
|
||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
import * as mockttp from 'mockttp';
|
import * as mockttp from 'mockttp';
|
||||||
|
|
||||||
import {mockRpcMessage} from './rpc-websockets';
|
import {mockRpcMessage} from './rpc-websocket';
|
||||||
import {Connection, PublicKey, Transaction, Signer} from '../../src';
|
import {Connection, PublicKey, Transaction, Signer} from '../../src';
|
||||||
import invariant from '../../src/utils/assert';
|
import invariant from '../../src/utils/assert';
|
||||||
import type {Commitment, HttpHeaders, RpcParams} from '../../src/connection';
|
import type {Commitment, HttpHeaders, RpcParams} from '../../src/connection';
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {Client as LiveClient} from 'rpc-websockets';
|
|
||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {createSandbox} from 'sinon';
|
import {createSandbox} from 'sinon';
|
||||||
|
|
||||||
import {Connection} from '../../src';
|
import {Connection} from '../../src';
|
||||||
|
import LiveClient from '../../src/rpc-websocket';
|
||||||
|
|
||||||
type RpcRequest = {
|
type RpcRequest = {
|
||||||
method: string;
|
method: string;
|
||||||
|
@ -55,6 +55,7 @@ export const stubRpcWebSocket = (connection: Connection) => {
|
||||||
.callsFake((method: string, params: any) => {
|
.callsFake((method: string, params: any) => {
|
||||||
return mockClient.call(method, params);
|
return mockClient.call(method, params);
|
||||||
});
|
});
|
||||||
|
sandbox.stub(rpcWebSocket, 'notify');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restoreRpcWebSocket = (connection: Connection) => {
|
export const restoreRpcWebSocket = (connection: Connection) => {
|
|
@ -12,7 +12,7 @@ import {
|
||||||
import {NONCE_ACCOUNT_LENGTH} from '../src/nonce-account';
|
import {NONCE_ACCOUNT_LENGTH} from '../src/nonce-account';
|
||||||
import {MOCK_PORT, url} from './url';
|
import {MOCK_PORT, url} from './url';
|
||||||
import {helpers, mockRpcResponse, mockServer} from './mocks/rpc-http';
|
import {helpers, mockRpcResponse, mockServer} from './mocks/rpc-http';
|
||||||
import {stubRpcWebSocket, restoreRpcWebSocket} from './mocks/rpc-websockets';
|
import {stubRpcWebSocket, restoreRpcWebSocket} from './mocks/rpc-websocket';
|
||||||
|
|
||||||
const expectedData = (authorizedPubkey: PublicKey): [string, string] => {
|
const expectedData = (authorizedPubkey: PublicKey): [string, string] => {
|
||||||
const expectedData = Buffer.alloc(NONCE_ACCOUNT_LENGTH);
|
const expectedData = Buffer.alloc(NONCE_ACCOUNT_LENGTH);
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
import invariant from '../src/utils/assert';
|
import invariant from '../src/utils/assert';
|
||||||
import {MOCK_PORT, url} from './url';
|
import {MOCK_PORT, url} from './url';
|
||||||
import {helpers, mockRpcResponse, mockServer} from './mocks/rpc-http';
|
import {helpers, mockRpcResponse, mockServer} from './mocks/rpc-http';
|
||||||
import {stubRpcWebSocket, restoreRpcWebSocket} from './mocks/rpc-websockets';
|
import {stubRpcWebSocket, restoreRpcWebSocket} from './mocks/rpc-websocket';
|
||||||
|
|
||||||
describe('Transaction Payer', () => {
|
describe('Transaction Payer', () => {
|
||||||
let connection: Connection;
|
let connection: Connection;
|
||||||
|
|
|
@ -23,7 +23,15 @@ if (process.env.TEST_LIVE) {
|
||||||
expect(connection._rpcWebSocketHeartbeat).not.to.eq(null);
|
expect(connection._rpcWebSocketHeartbeat).not.to.eq(null);
|
||||||
|
|
||||||
// test if socket is open
|
// test if socket is open
|
||||||
|
let open = false;
|
||||||
|
while (!open) {
|
||||||
|
try {
|
||||||
await connection._rpcWebSocket.notify('ping');
|
await connection._rpcWebSocket.notify('ping');
|
||||||
|
open = true;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await connection.removeSignatureListener(id);
|
await connection.removeSignatureListener(id);
|
||||||
expect(connection._rpcWebSocketConnected).to.eq(false);
|
expect(connection._rpcWebSocketConnected).to.eq(false);
|
||||||
|
@ -38,7 +46,7 @@ if (process.env.TEST_LIVE) {
|
||||||
|
|
||||||
// test if socket is closed
|
// test if socket is closed
|
||||||
await expect(connection._rpcWebSocket.notify('ping')).to.be.rejectedWith(
|
await expect(connection._rpcWebSocket.notify('ping')).to.be.rejectedWith(
|
||||||
'socket not ready',
|
'Tried to send a JSON-RPC notification `ping` but the socket was not `CONNECTING` or `OPEN` (`readyState` was 3)',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue