mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-01-12 22:43:34 +08:00
feat: websocket rpc client lib
This commit is contained in:
16
ws-rpc-client/.eslintrc.js
Normal file
16
ws-rpc-client/.eslintrc.js
Normal 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
12
ws-rpc-client/index.html
Normal 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
5348
ws-rpc-client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
ws-rpc-client/package.json
Normal file
45
ws-rpc-client/package.json
Normal 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
146
ws-rpc-client/src/index.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
7
ws-rpc-client/tsconfig.browser.json
Normal file
7
ws-rpc-client/tsconfig.browser.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext"
|
||||
}
|
||||
}
|
||||
18
ws-rpc-client/tsconfig.json
Normal file
18
ws-rpc-client/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user