feat: websocket rpc client lib

This commit is contained in:
Matthew Little
2020-08-05 13:35:23 -06:00
parent d2026b9fdd
commit 0a67a11043
7 changed files with 5592 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
module.exports = {
root: true,
extends: ['@blockstack/eslint-config'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json',
ecmaVersion: 2019,
sourceType: 'module',
},
ignorePatterns: [
'lib/*',
],
rules: {
}
};

12
ws-rpc-client/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>stacks-ws-rpc testing</title>
</head>
<body>
<script src="lib/index.umd.js"></script>
</body>
</html>

5348
ws-rpc-client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"name": "@blockstack/ws-rpc-client",
"version": "0.1.0",
"access": "public",
"description": "",
"main": "lib/index.js",
"scripts": {
"build": "rimraf ./lib && npm run build:node && npm run build:browser",
"build:node": "tsc",
"build:browser": "microbundle -i src/index.ts -o lib/index.umd.js --no-pkg-main -f umd --external none --globals none --tsconfig tsconfig.browser.json --name StacksApiWebSocketClient",
"open": "http-server -o 9222 -o test/index.html",
"test": "echo \"Error: no test specified\" && exit 1",
"prepare": "npm run build",
"postinstall": "npm run build"
},
"keywords": [],
"author": "@blockstack",
"license": "ISC",
"prettier": "@blockstack/prettier-config",
"files": [
"lib/**/*",
"src/**/*"
],
"dependencies": {
"@blockstack/stacks-blockchain-api-types": "^0.4.0",
"@types/ws": "^7.2.6",
"eventemitter3": "^4.0.4",
"jsonrpc-lite": "^2.1.0",
"strict-event-emitter-types": "^2.0.0",
"ws": "^7.3.1"
},
"devDependencies": {
"@blockstack/prettier-config": "0.0.6",
"@typescript-eslint/eslint-plugin": "^3.8.0",
"@typescript-eslint/parser": "^3.8.0",
"eslint": "^7.6.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"http-server": "^0.12.3",
"microbundle": "^0.12.3",
"prettier": "^2.0.5",
"rimraf": "^3.0.2",
"typescript": "^3.9.7"
}
}

146
ws-rpc-client/src/index.ts Normal file
View File

@@ -0,0 +1,146 @@
import * as JsonRpcLite from 'jsonrpc-lite';
import EventEmitter from 'eventemitter3';
import StrictEventEmitter from 'strict-event-emitter-types';
import {
RpcTxUpdateSubscriptionParams,
RpcTxUpdateNotificationParams,
RpcAddressTxSubscriptionParams,
RpcAddressTxNotificationParams,
RpcAddressBalanceSubscriptionParams,
RpcAddressBalanceNotificationParams,
RpcSubscriptionType,
} from '@blockstack/stacks-blockchain-api-types';
type IWebSocket = import('ws') | WebSocket;
interface Events {
txUpdate: (event: RpcTxUpdateNotificationParams) => void;
addressTxUpdate: (event: RpcAddressTxNotificationParams) => void;
addressBalanceUpdate: (event: RpcAddressBalanceNotificationParams) => void;
}
type StacksApiEventEmitter = StrictEventEmitter<EventEmitter, Events>;
export class StacksApiWebSocketClient extends (EventEmitter as {
new (): StacksApiEventEmitter;
}) {
webSocket: IWebSocket;
idCursor = 0;
pendingRequests = new Map<
JsonRpcLite.ID,
{ resolve: (result: any) => void; reject: (error: any) => void }
>();
constructor(webSocket: IWebSocket) {
super();
this.webSocket = webSocket;
(webSocket as WebSocket).addEventListener('message', event => {
const parsed = JsonRpcLite.parse(event.data);
const rpcObjects = Array.isArray(parsed) ? parsed : [parsed];
rpcObjects.forEach(obj => {
if (obj.type === JsonRpcLite.RpcStatusType.notification) {
this.handleNotification(obj.payload);
} else if (obj.type === JsonRpcLite.RpcStatusType.success) {
const req = this.pendingRequests.get(obj.payload.id);
if (req) {
this.pendingRequests.delete(obj.payload.id);
req.resolve(obj.payload.result);
}
} else if (obj.type === JsonRpcLite.RpcStatusType.error) {
const req = this.pendingRequests.get(obj.payload.id);
if (req) {
this.pendingRequests.delete(obj.payload.id);
req.reject(obj.payload.error);
}
}
});
});
}
handleNotification(data: JsonRpcLite.NotificationObject): void {
const method = data.method as RpcSubscriptionType;
switch (method) {
case 'tx_update':
this.emit('txUpdate', data.params as RpcTxUpdateNotificationParams);
break;
case 'address_tx_update':
this.emit('addressTxUpdate', data.params as RpcAddressTxNotificationParams);
break;
case 'address_balance_update':
this.emit('addressBalanceUpdate', data.params as RpcAddressBalanceNotificationParams);
break;
}
}
private rpcCall(method: string, params: any): Promise<void> {
const rpcReq = JsonRpcLite.request(++this.idCursor, method, params);
return new Promise((resolve, reject) => {
this.pendingRequests.set(rpcReq.id, { resolve, reject });
this.webSocket.send(rpcReq.serialize());
});
}
subscribeTxUpdates(txId: string): Promise<void> {
const params: RpcTxUpdateSubscriptionParams = { event: 'tx_update', tx_id: txId };
return this.rpcCall('subscribe', params);
}
unsubscribeTxUpdates(txId: string): Promise<void> {
const params: RpcTxUpdateSubscriptionParams = { event: 'tx_update', tx_id: txId };
return this.rpcCall('unsubscribe', params);
}
subscribeAddressTransactions(address: string): Promise<void> {
const params: RpcAddressTxSubscriptionParams = { event: 'address_tx_update', address };
return this.rpcCall('subscribe', params);
}
unsubscribeAddressTransactions(address: string): Promise<void> {
const params: RpcAddressTxSubscriptionParams = { event: 'address_tx_update', address };
return this.rpcCall('unsubscribe', params);
}
subscribeAddressBalanceUpdates(address: string): Promise<void> {
const params: RpcAddressBalanceSubscriptionParams = {
event: 'address_balance_update',
address,
};
return this.rpcCall('subscribe', params);
}
unsubscribeAddressBalanceUpdates(address: string): Promise<void> {
const params: RpcAddressBalanceSubscriptionParams = {
event: 'address_balance_update',
address,
};
return this.rpcCall('unsubscribe', params);
}
}
export async function connect(url: string): Promise<StacksApiWebSocketClient> {
const webSocket = await new Promise<IWebSocket>((resolve, reject) => {
const webSocket = new (createWebSocket())(url);
webSocket.onopen = () => resolve(webSocket);
webSocket.onerror = error => reject(error);
});
return new StacksApiWebSocketClient(webSocket);
}
/**
* Simple isomorphic WebSocket class lookup.
* Uses global WebSocket (browsers) if available, otherwise, uses the Node.js `ws` lib.
*/
function createWebSocket(): typeof WebSocket {
if (typeof WebSocket !== 'undefined') {
return WebSocket;
} else if (typeof global !== 'undefined' && global.WebSocket) {
return global.WebSocket;
} else if (typeof window !== 'undefined' && window.WebSocket) {
return window.WebSocket;
} else if (typeof self !== 'undefined' && self.WebSocket) {
return self.WebSocket;
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return require('ws');
}
}

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext"
}
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"strict": true,
"outDir": "lib",
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@blockstack/stacks-blockchain-api-types": ["../docs"]
}
},
"include": ["src/**/*"],
"exclude": ["lib"]
}