Skip to content

Commit f88ffc1

Browse files
committed
Add network info fetching from WS
1 parent f37c17d commit f88ffc1

File tree

5 files changed

+136
-6
lines changed

5 files changed

+136
-6
lines changed

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
module.exports = {
33
preset: 'ts-jest',
44
testEnvironment: 'node',
5+
modulePathIgnorePatterns: ['bin', 'node_modules'],
56
};

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"devDependencies": {
1414
"@types/jest": "^29.5.0",
1515
"@types/node": "^18.15.11",
16+
"@types/ws": "^8.5.4",
1617
"@typescript-eslint/eslint-plugin": "^5.57.0",
1718
"@typescript-eslint/parser": "^5.57.0",
1819
"eslint": "^8.37.0",
@@ -25,7 +26,8 @@
2526
"typescript": "^5.0.3"
2627
},
2728
"dependencies": {
28-
"axios": "^1.3.4"
29+
"axios": "^1.3.4",
30+
"ws": "^8.13.0"
2931
},
3032
"scripts": {
3133
"tsc": "tsc",

src/provider/test/ws.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { NetworkInfo } from '../types';
2+
import { WebSocketServer } from 'ws';
3+
import { WSProvider } from '../websocket/websocket';
4+
import { newResponse } from '../spec/utility';
5+
6+
describe('WS Provider', () => {
7+
const wsPort = 8545;
8+
const wsHost = 'localhost';
9+
const wsURL = `ws://${wsHost}:${wsPort}`;
10+
11+
let wsProvider: WSProvider;
12+
let server: WebSocketServer;
13+
14+
beforeEach(async () => {
15+
server = new WebSocketServer({
16+
host: wsHost,
17+
port: wsPort,
18+
});
19+
wsProvider = new WSProvider(wsURL);
20+
});
21+
22+
afterEach(() => {
23+
wsProvider.closeConnection();
24+
server.close();
25+
});
26+
27+
test('getNetwork', async () => {
28+
const mockInfo: NetworkInfo = {
29+
listening: false,
30+
listeners: [],
31+
n_peers: '0',
32+
peers: [],
33+
};
34+
35+
server.on('connection', (socket) => {
36+
socket.on('message', (data) => {
37+
const request = JSON.parse(data.toString());
38+
const response = newResponse<NetworkInfo>(mockInfo);
39+
response.id = request.id;
40+
41+
socket.send(JSON.stringify(response));
42+
});
43+
});
44+
45+
const info = await wsProvider.getNetwork();
46+
expect(info).toEqual(mockInfo);
47+
});
48+
});

src/provider/websocket/websocket.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Provider } from '../provider';
22
import { ConsensusParams, NetworkInfo, Status } from '../types';
33
import { RPCRequest, RPCResponse } from '../spec/jsonrpc';
4+
import { newRequest } from '../spec/utility';
5+
import { ConsensusEndpoint } from '../endpoints';
6+
import { WebSocket } from 'ws';
47

58
/**
69
* Provider based on WS JSON-RPC HTTP requests
@@ -24,8 +27,9 @@ export class WSProvider implements Provider {
2427
*/
2528
constructor(baseURL: string, requestTimeout?: number) {
2629
this.ws = new WebSocket(baseURL);
30+
2731
this.ws.addEventListener('message', (event) => {
28-
const response = JSON.parse(event.data) as RPCResponse<any>;
32+
const response = JSON.parse(event.data as string) as RPCResponse<any>;
2933
const request = this.requestMap.get(response.id);
3034
if (request) {
3135
this.requestMap.delete(response.id);
@@ -39,15 +43,28 @@ export class WSProvider implements Provider {
3943
});
4044
}
4145

46+
/**
47+
* Closes the WS connection. Required when done working
48+
* with the WS provider
49+
*/
50+
closeConnection() {
51+
this.ws.close();
52+
}
53+
4254
/**
4355
* Sends a request to the WS connection, and resolves
4456
* upon receiving the response
4557
* @param {RPCRequest} request the RPC request
4658
* @returns {Promise<RPCResponse<any>>} the RPC response
4759
*/
48-
sendRequest(request: RPCRequest): Promise<RPCResponse<any>> {
60+
async sendRequest<Result>(request: RPCRequest): Promise<RPCResponse<Result>> {
61+
// Make sure the connection is open
62+
if (this.ws.readyState != WebSocket.OPEN) {
63+
await this.waitForOpenConnection();
64+
}
65+
4966
// The promise will resolve as soon as the response is received
50-
const promise = new Promise<RPCResponse<any>>((resolve, reject) => {
67+
const promise = new Promise<RPCResponse<Result>>((resolve, reject) => {
5168
const timeout = setTimeout(() => {
5269
this.requestMap.delete(request.id);
5370

@@ -62,6 +79,52 @@ export class WSProvider implements Provider {
6279
return promise;
6380
}
6481

82+
/**
83+
* Parses the result from the response
84+
* @param {RPCResponse<Result>} response the response to be parsed
85+
* @returns {Result} the result of the response
86+
*/
87+
parseResponse<Result>(response: RPCResponse<Result>): Result {
88+
if (!response) {
89+
throw new Error('invalid response');
90+
}
91+
92+
if (response.error) {
93+
throw new Error(response.error?.message);
94+
}
95+
96+
if (!response.result) {
97+
throw new Error('invalid response returned');
98+
}
99+
100+
return response.result;
101+
}
102+
103+
/**
104+
* Waits for the WS connection to be established
105+
* @returns {Promise<null>} resolve / reject indicating success
106+
*/
107+
waitForOpenConnection = () => {
108+
return new Promise((resolve, reject) => {
109+
const maxNumberOfAttempts = 10;
110+
const intervalTime = 200; //ms
111+
112+
let currentAttempt = 0;
113+
const interval = setInterval(() => {
114+
if (this.ws.readyState === WebSocket.OPEN) {
115+
clearInterval(interval);
116+
resolve(null);
117+
}
118+
119+
currentAttempt++;
120+
if (currentAttempt > maxNumberOfAttempts - 1) {
121+
clearInterval(interval);
122+
reject(new Error('Maximum number of attempts exceeded'));
123+
}
124+
}, intervalTime);
125+
});
126+
};
127+
65128
estimateGas(tx: any): Promise<number> {
66129
return Promise.reject('implement me');
67130
}
@@ -94,8 +157,12 @@ export class WSProvider implements Provider {
94157
return Promise.reject('implement me');
95158
}
96159

97-
getNetwork(): Promise<NetworkInfo> {
98-
return Promise.reject('implement me');
160+
async getNetwork(): Promise<NetworkInfo> {
161+
const response = await this.sendRequest<NetworkInfo>(
162+
newRequest(ConsensusEndpoint.NET_INFO)
163+
);
164+
165+
return this.parseResponse<NetworkInfo>(response);
99166
}
100167

101168
getSequence(address: string, height?: number): Promise<number> {

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,13 @@
727727
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
728728
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
729729

730+
"@types/ws@^8.5.4":
731+
version "8.5.4"
732+
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"
733+
integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==
734+
dependencies:
735+
"@types/node" "*"
736+
730737
"@types/yargs-parser@*":
731738
version "21.0.0"
732739
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@@ -2850,6 +2857,11 @@ write-file-atomic@^4.0.2:
28502857
imurmurhash "^0.1.4"
28512858
signal-exit "^3.0.7"
28522859

2860+
ws@^8.13.0:
2861+
version "8.13.0"
2862+
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
2863+
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
2864+
28532865
y18n@^5.0.5:
28542866
version "5.0.8"
28552867
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"

0 commit comments

Comments
 (0)