Add mango-fills client library
This commit is contained in:
parent
8e0ec88d2c
commit
dfd3cf8527
|
@ -4,3 +4,6 @@
|
|||
.DS_Store
|
||||
.idea/
|
||||
*.pem
|
||||
|
||||
node_modules
|
||||
dist
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "@blockworks-foundation/mango-feeds",
|
||||
"version": "0.9.12",
|
||||
"description": "Typescript Client for mango-feeds.",
|
||||
"repository": "https://github.com/blockworks-foundation/mango-feeds",
|
||||
"author": {
|
||||
"name": "Blockworks Foundation",
|
||||
"email": "hello@blockworks.foundation",
|
||||
"url": "https://blockworks.foundation"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"main": "./dist/cjs/src/index.js",
|
||||
"module": "./dist/esm/src/index.js",
|
||||
"types": "./dist/types/src/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rimraf dist && yarn build:browser && yarn build:node && yarn build:types",
|
||||
"build:node": " tsc -p tsconfig.cjs.json --noEmit false",
|
||||
"build:browser": "tsc -p tsconfig.esm.json --noEmit false",
|
||||
"build:types": "tsc -p tsconfig.types.json --noEmit false",
|
||||
"test": "ts-mocha ts/client/**/*.spec.ts --timeout 300000",
|
||||
"lint": "eslint ./ts/client/src --ext ts --ext tsx --ext js --quiet",
|
||||
"typecheck": "tsc --noEmit --pretty",
|
||||
"prepublishOnly": "yarn validate && yarn build",
|
||||
"validate": "yarn lint && yarn format"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/recommended": "^1.0.1",
|
||||
"@types/node": "^18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
||||
"@typescript-eslint/parser": "^5.32.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.22.5",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { FillsFeed } from '../src';
|
||||
|
||||
const RECONNECT_INTERVAL_MS = 1000;
|
||||
const RECONNECT_ATTEMPTS_MAX = -1;
|
||||
|
||||
// Subscribe on connection
|
||||
const fillsFeed = new FillsFeed('ws://localhost:8080', {
|
||||
reconnectIntervalMs: RECONNECT_INTERVAL_MS,
|
||||
reconnectionMaxAttempts: RECONNECT_ATTEMPTS_MAX,
|
||||
subscriptions: {
|
||||
accountIds: ['9XJt2tvSZghsMAhWto1VuPBrwXsiimPtsTR8XwGgDxK2'],
|
||||
},
|
||||
});
|
||||
|
||||
// Subscribe after connection
|
||||
fillsFeed.onConnect(() => {
|
||||
console.log('connected');
|
||||
fillsFeed.subscribe({
|
||||
marketIds: ['ESdnpnNLgTkBCZRuTJkZLi5wKEZ2z47SG3PJrhundSQ2'],
|
||||
headUpdates: true,
|
||||
});
|
||||
});
|
||||
|
||||
fillsFeed.onDisconnect(() => {
|
||||
console.log(`disconnected, reconnecting in ${RECONNECT_INTERVAL_MS}...`);
|
||||
});
|
||||
|
||||
fillsFeed.onFill((update) => {
|
||||
console.log('fill', update);
|
||||
});
|
||||
|
||||
fillsFeed.onHead((update) => {
|
||||
console.log('head', update);
|
||||
});
|
||||
|
||||
fillsFeed.onStatus((update) => {
|
||||
console.log('status', update);
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
import WebSocket from 'ws';
|
||||
|
||||
interface FillsFeedOptions {
|
||||
subscriptions?: FillsFeedSubscriptionParams;
|
||||
reconnectIntervalMs?: number;
|
||||
reconnectionMaxAttempts?: number;
|
||||
}
|
||||
|
||||
interface FillsFeedSubscriptionParams {
|
||||
marketId?: string;
|
||||
marketIds?: string[];
|
||||
accountIds?: string[];
|
||||
headUpdates?: boolean;
|
||||
}
|
||||
|
||||
interface FillEventUpdate {
|
||||
status: 'new' | 'revoke';
|
||||
marketKey: 'string';
|
||||
marketName: 'string';
|
||||
slot: number;
|
||||
writeVersion: number;
|
||||
event: {
|
||||
eventType: 'spot' | 'perp';
|
||||
maker: 'string';
|
||||
taker: 'string';
|
||||
takerSide: 'bid' | 'ask';
|
||||
timestamp: 'string'; // DateTime
|
||||
seqNum: number;
|
||||
makerClientOrderId: number;
|
||||
takerClientOrderId: number;
|
||||
makerFee: number;
|
||||
takerFee: number;
|
||||
price: number;
|
||||
quantity: number;
|
||||
};
|
||||
}
|
||||
|
||||
function isFillEventUpdate(obj: any): obj is FillEventUpdate {
|
||||
return obj.event !== undefined;
|
||||
}
|
||||
|
||||
interface HeadUpdate {
|
||||
head: number;
|
||||
previousHead: number;
|
||||
headSeqNum: number;
|
||||
previousHeadSeqNum: number;
|
||||
status: 'new' | 'revoke';
|
||||
marketKey: 'string';
|
||||
marketName: 'string';
|
||||
slot: number;
|
||||
writeVersion: number;
|
||||
}
|
||||
|
||||
function isHeadUpdate(obj: any): obj is HeadUpdate {
|
||||
return obj.head !== undefined;
|
||||
}
|
||||
|
||||
interface StatusMessage {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function isStatusMessage(obj: any): obj is StatusMessage {
|
||||
return obj.success !== undefined;
|
||||
}
|
||||
|
||||
export class FillsFeed {
|
||||
private _url: string;
|
||||
private _socket: WebSocket;
|
||||
private _subscriptions?: FillsFeedSubscriptionParams;
|
||||
private _connected: boolean;
|
||||
private _reconnectionIntervalMs;
|
||||
private _reconnectionAttempts;
|
||||
private _reconnectionMaxAttempts;
|
||||
|
||||
private _onConnect: (() => void) | null = null;
|
||||
private _onDisconnect: (() => void) | null = null;
|
||||
private _onFill: ((update: FillEventUpdate) => void) | null = null;
|
||||
private _onHead: ((update: HeadUpdate) => void) | null = null;
|
||||
private _onStatus: ((update: StatusMessage) => void) | null = null;
|
||||
|
||||
constructor(url: string, options?: FillsFeedOptions) {
|
||||
this._url = url;
|
||||
this._subscriptions = options?.subscriptions;
|
||||
this._reconnectionIntervalMs = options?.reconnectIntervalMs ?? 5000;
|
||||
this._reconnectionAttempts = 0;
|
||||
this._reconnectionMaxAttempts = options?.reconnectionMaxAttempts ?? -1;
|
||||
|
||||
this._connect();
|
||||
}
|
||||
|
||||
private _reconnectionAttemptsExhausted(): boolean {
|
||||
return this._reconnectionMaxAttempts != -1 && this._reconnectionAttempts >= this._reconnectionMaxAttempts
|
||||
}
|
||||
|
||||
private _connect() {
|
||||
this._socket = new WebSocket(this._url);
|
||||
|
||||
this._socket.addEventListener('error', (err) => {
|
||||
console.warn(`[FillsFeed] connection error: ${err.message}`);
|
||||
if (this._reconnectionAttemptsExhausted()) {
|
||||
console.error('[FillsFeed] fatal connection error')
|
||||
throw err.error;
|
||||
}
|
||||
});
|
||||
|
||||
this._socket.addEventListener('open', () => {
|
||||
if (this._subscriptions !== undefined) {
|
||||
this._connected = true;
|
||||
this._reconnectionAttempts = 0;
|
||||
this.subscribe(this._subscriptions);
|
||||
}
|
||||
if (this._onConnect) this._onConnect();
|
||||
});
|
||||
|
||||
this._socket.addEventListener('close', () => {
|
||||
this._connected = false;
|
||||
setTimeout(() => {
|
||||
if (!this._reconnectionAttemptsExhausted()) {
|
||||
this._reconnectionAttempts++;
|
||||
this._connect();
|
||||
}
|
||||
}, this._reconnectionIntervalMs);
|
||||
if (this._onDisconnect) this._onDisconnect();
|
||||
});
|
||||
|
||||
this._socket.addEventListener('message', (msg: any) => {
|
||||
try {
|
||||
const data = JSON.parse(msg.data);
|
||||
if (isFillEventUpdate(data) && this._onFill) {
|
||||
this._onFill(data);
|
||||
} else if (isHeadUpdate(data) && this._onHead) {
|
||||
this._onHead(data);
|
||||
} else if (isStatusMessage(data) && this._onStatus) {
|
||||
this._onStatus(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[FillsFeed] error deserializing message', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public subscribe(subscriptions: FillsFeedSubscriptionParams) {
|
||||
if (this._connected) {
|
||||
this._socket.send(
|
||||
JSON.stringify({
|
||||
command: 'subscribe',
|
||||
...subscriptions,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.warn('[FillsFeed] attempt to subscribe when not connected');
|
||||
}
|
||||
}
|
||||
|
||||
public connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
public onConnect(callback: () => void) {
|
||||
this._onConnect = callback;
|
||||
}
|
||||
|
||||
public onDisconnect(callback: () => void) {
|
||||
this._onDisconnect = callback;
|
||||
}
|
||||
|
||||
public onFill(callback: (update: FillEventUpdate) => void) {
|
||||
this._onFill = callback;
|
||||
}
|
||||
|
||||
public onHead(callback: (update: HeadUpdate) => void) {
|
||||
this._onHead = callback;
|
||||
}
|
||||
|
||||
public onStatus(callback: (update: StatusMessage) => void) {
|
||||
this._onStatus = callback;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './fills';
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"module": "commonjs",
|
||||
"outDir": "dist/cjs",
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": [
|
||||
"ts/client/src",
|
||||
"ts/client/scripts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"module": "esnext",
|
||||
"outDir": "dist/esm",
|
||||
"sourceMap": false,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "esnext",
|
||||
},
|
||||
"ts-node": {
|
||||
// these options are overrides used only by ts-node
|
||||
// same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"ts/client/src"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "./dist/types",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue