mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-04-30 13:52:28 +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