Add mango-fills client library

This commit is contained in:
Riordan Panayides 2023-04-08 04:04:36 +01:00
parent 8e0ec88d2c
commit dfd3cf8527
11 changed files with 1557 additions and 0 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@
.DS_Store
.idea/
*.pem
node_modules
dist

49
package.json Normal file
View File

@ -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"
}

View File

@ -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);
});

179
ts/client/src/fills.ts Normal file
View File

@ -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;
}
}

1
ts/client/src/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './fills';

View File

14
tsconfig.cjs.json Normal file
View File

@ -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"
]
}

10
tsconfig.esm.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"declaration": false,
"declarationMap": false,
"module": "esnext",
"outDir": "dist/esm",
"sourceMap": false,
}
}

23
tsconfig.json Normal file
View File

@ -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"
]
}

10
tsconfig.types.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"noEmit": false,
"outDir": "./dist/types",
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true
}
}

1230
yarn.lock Normal file

File diff suppressed because it is too large Load Diff