Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Websockets on the http rest port #436

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 0 additions & 1 deletion docker/devnet.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ alephium.node.event-log.index-by-tx-id = true
alephium.node.event-log.index-by-block-hash = true

alephium.network.rest-port = 22973
alephium.network.ws-port = 21973
alephium.network.miner-api-port = 20973
alephium.api.network-interface = "0.0.0.0"
alephium.api.api-key-enabled = false
Expand Down
1 change: 0 additions & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ services:
- 19973:19973/tcp
- 19973:19973/udp
- 127.0.0.1:20973:20973
- 127.0.0.1:21973:21973
- 127.0.0.1:22973:22973
environment:
- ALEPHIUM_LOG_LEVEL=DEBUG
Expand Down
2 changes: 2 additions & 0 deletions packages/web3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
},
"type": "commonjs",
"dependencies": {
"ws": "^8.5.4",
"@noble/secp256k1": "1.7.1",
"base-x": "4.0.0",
"bignumber.js": "^9.1.1",
Expand All @@ -67,6 +68,7 @@
"@types/mock-fs": "^4.13.1",
"@types/node": "^16.18.23",
"@types/rewire": "^2.5.28",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"clean-webpack-plugin": "4.0.0",
Expand Down
2 changes: 0 additions & 2 deletions packages/web3/src/api/api-alephium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,8 +911,6 @@ export interface PeerAddress {
/** @format int32 */
restPort: number
/** @format int32 */
wsPort: number
/** @format int32 */
minerApiPort: number
}

Expand Down
1 change: 0 additions & 1 deletion packages/web3/src/fixtures/self-clique.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
{
"address": "127.0.0.1",
"restPort": 12973,
"wsPort": 11973,
"minerApiPort": 10973
}
],
Expand Down
1 change: 1 addition & 0 deletions packages/web3/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from './signer'
export * from './utils'
export * from './transaction'
export * from './token'
export * from './ws'

export * from './constants'
export * as web3 from './global'
Expand Down
19 changes: 19 additions & 0 deletions packages/web3/src/ws/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Copyright 2018 - 2022 The Alephium Authors
This file is part of the alephium project.

The library is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

export { WebSocketClient } from './websocket-client'
142 changes: 142 additions & 0 deletions packages/web3/src/ws/websocket-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
Copyright 2018 - 2022 The Alephium Authors
This file is part of the alephium project.

The library is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/
import WebSocket from 'ws';
import { EventEmitter } from 'eventemitter3';

export class WebSocketClient extends EventEmitter {
private ws: WebSocket;
private requestId: number;
private isConnected: boolean;
private notifications: any[];

constructor(url: string) {
super();
this.ws = new WebSocket(url);
this.requestId = 0;
this.isConnected = false;
this.notifications = [];

this.ws.on('open', () => {
this.isConnected = true;
this.emit('connected');
});

this.ws.on('message', (data: WebSocket.Data) => {
try {
const message = JSON.parse(data.toString());
if (message.method === 'subscription') {
// Emit and store notifications
const params = message.params;
this.notifications.push(params);
this.emit('notification', params);
} else {
this.emit(`response_${message.id}`, message);
}
} catch (error) {
this.emit('error', error);
}
});

this.ws.on('close', () => {
this.isConnected = false;
this.emit('disconnected');
});
}

public subscribe(method: string, params: unknown[] = []): Promise<string> {
const id = this.getRequestId();
const request = {
jsonrpc: '2.0',
id,
method,
params
};

return new Promise((resolve, reject) => {
this.once(`response_${id}`, (response) => {
if (response.result) {
resolve(response.result);
} else {
reject(response.error);
}
});
this.ws.send(JSON.stringify(request));
});
}

public unsubscribe(subscriptionId: string): Promise<boolean> {
const id = this.getRequestId();
const request = {
jsonrpc: '2.0',
id,
method: 'unsubscribe',
params: [subscriptionId]
};

return new Promise((resolve, reject) => {
this.once(`response_${id}`, (response) => {
if (response.result === true) {
resolve(true);
} else {
reject(response.error);
}
});
this.ws.send(JSON.stringify(request));
});
}

public async subscribeToBlock(): Promise<string> {
return this.subscribe('subscribe', ['block']);
}

public async subscribeToTx(): Promise<string> {
return this.subscribe('subscribe', ['tx']);
}

public async subscribeToContractEvents(addresses: string[]): Promise<string> {
return this.subscribe('subscribe', ['contract', {addresses: addresses}]);
}

public async subscribeToFilteredContractEvents(eventIndex: number, addresses: string[]): Promise<string> {
return this.subscribe('subscribe', ['contract', {eventIndex: eventIndex, addresses: addresses}]);
}

public onConnected(callback: () => void) {
if (this.isConnected) {
callback();
} else {
this.on('connected', callback);
}
}

public onNotification(callback: (params: any) => void) {
for (const notification of this.notifications) {
callback(notification);
}

this.on('notification', callback);
}

public disconnect() {
this.ws.close();
}

private getRequestId(): number {
return ++this.requestId;
}
}
94 changes: 94 additions & 0 deletions test/websocket-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
Copyright 2018 - 2022 The Alephium Authors
This file is part of the alephium project.

The library is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import {ONE_ALPH, SignerProviderSimple, SignTransferTxResult, utils, web3, WebSocketClient} from '@alephium/web3';
import {getSigner, randomContractAddress} from '@alephium/web3-test';

const NODE_PROVIDER = 'http://127.0.0.1:22973'
const WS_ENDPOINT = 'ws://127.0.0.1:22973/ws';

describe('WebSocketClient', () => {
let client: WebSocketClient;
let signer: SignerProviderSimple;

async function signAndSubmitTx(): Promise<SignTransferTxResult> {
const address = (await signer.getSelectedAccount()).address;
const attoAlphAmount = ONE_ALPH;
return await signer.signAndSubmitTransferTx({
signerAddress: address,
destinations: [{ address, attoAlphAmount }],
});
}

beforeEach(async () => {
client = new WebSocketClient(WS_ENDPOINT);
signer = await getSigner();
web3.setCurrentNodeProvider(NODE_PROVIDER, undefined, fetch);
});

afterEach(() => {
client.disconnect();
});

test('should subscribe, receive notifications and unsubscribe', (done) => {
let notificationCount = 0;
let blockNotificationReceived = false;
let txNotificationReceived = false;
let blockSubscriptionId: string;
let txSubscriptionId: string;
let contractEventsSubscriptionId: string;


client.onConnected(async () => {
try {
blockSubscriptionId = await client.subscribeToBlock();
txSubscriptionId = await client.subscribeToTx();
contractEventsSubscriptionId = await client.subscribeToContractEvents([randomContractAddress()]);
await signAndSubmitTx();
} catch (error) {
done(error);
}
});

client.onNotification(async (params) => {
expect(params).toBeDefined();
if (params.result.block) {
blockNotificationReceived = true;
} else if (params.result.unsigned) {
txNotificationReceived = true;
}

notificationCount += 1;
if (notificationCount === 2) {
try {
expect(blockNotificationReceived).toBe(true);
expect(txNotificationReceived).toBe(true);
const blockUnsubscriptionResponse = await client.unsubscribe(blockSubscriptionId);
expect(blockUnsubscriptionResponse).toBe(true);
const txUnsubscriptionResponse = await client.unsubscribe(txSubscriptionId);
expect(txUnsubscriptionResponse).toBe(true);
const contractEventsUnsubscriptionResponse = await client.unsubscribe(contractEventsSubscriptionId);
expect(contractEventsUnsubscriptionResponse).toBe(true);
done();
} catch (error) {
done(error);
}
}
});
});
});
Loading