@n1xyz/nord-ts 0.0.1 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +11 -0
- package/README.md +148 -65
- package/dist/bridge/NordUser.d.ts +78 -0
- package/dist/bridge/NordUser.js +196 -0
- package/dist/bridge/client.d.ts +150 -0
- package/dist/bridge/client.js +394 -0
- package/dist/bridge/const.d.ts +23 -0
- package/dist/bridge/const.js +47 -0
- package/dist/bridge/index.d.ts +5 -0
- package/dist/bridge/index.js +23 -0
- package/dist/bridge/types.d.ts +118 -0
- package/dist/bridge/types.js +16 -0
- package/dist/bridge/utils.d.ts +64 -0
- package/dist/bridge/utils.js +131 -0
- package/dist/client.d.ts +70 -0
- package/dist/client.js +129 -0
- package/dist/const.d.ts +2 -5
- package/dist/const.js +18 -22
- package/dist/constants/endpoints.d.ts +65 -0
- package/dist/constants/endpoints.js +68 -0
- package/dist/gen/common.d.ts +6 -1
- package/dist/gen/common.js +19 -9
- package/dist/gen/nord.d.ts +75 -17
- package/dist/gen/nord.js +987 -423
- package/dist/idl/bridge.d.ts +2 -0
- package/dist/idl/bridge.js +703 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.js +18 -2
- package/dist/models/account.d.ts +58 -0
- package/dist/models/account.js +6 -0
- package/dist/models/index.d.ts +8 -0
- package/dist/models/index.js +28 -0
- package/dist/models/market.d.ts +137 -0
- package/dist/models/market.js +6 -0
- package/dist/models/order.d.ts +211 -0
- package/dist/models/order.js +6 -0
- package/dist/models/token.d.ts +50 -0
- package/dist/models/token.js +6 -0
- package/dist/nord/Nord.d.ts +222 -49
- package/dist/nord/Nord.js +290 -278
- package/dist/nord/NordError.d.ts +23 -0
- package/dist/nord/NordError.js +48 -0
- package/dist/nord/NordImpl.d.ts +6 -2
- package/dist/nord/NordImpl.js +21 -1
- package/dist/nord/NordUser.d.ts +208 -42
- package/dist/nord/NordUser.js +389 -157
- package/dist/nord/Subscriber.d.ts +37 -0
- package/dist/nord/Subscriber.js +29 -0
- package/dist/nord/api/actions.d.ts +101 -0
- package/dist/nord/api/actions.js +250 -0
- package/dist/nord/api/core.d.ts +49 -0
- package/dist/nord/api/core.js +121 -0
- package/dist/nord/api/index.d.ts +1 -0
- package/dist/nord/api/index.js +17 -0
- package/dist/nord/api/market.d.ts +36 -0
- package/dist/nord/api/market.js +98 -0
- package/dist/nord/api/metrics.d.ts +67 -0
- package/dist/nord/api/metrics.js +132 -0
- package/dist/nord/api/orderFunctions.d.ts +168 -0
- package/dist/nord/api/orderFunctions.js +133 -0
- package/dist/nord/api/queries.d.ts +81 -0
- package/dist/nord/api/queries.js +187 -0
- package/dist/nord/client/Nord.d.ts +335 -0
- package/dist/nord/client/Nord.js +532 -0
- package/dist/nord/client/NordUser.d.ts +320 -0
- package/dist/nord/client/NordUser.js +701 -0
- package/dist/nord/core.d.ts +48 -0
- package/dist/nord/core.js +97 -0
- package/dist/nord/index.d.ts +9 -2
- package/dist/nord/index.js +30 -6
- package/dist/nord/market.d.ts +36 -0
- package/dist/nord/market.js +90 -0
- package/dist/nord/metrics.d.ts +67 -0
- package/dist/nord/metrics.js +124 -0
- package/dist/nord/models/Subscriber.d.ts +37 -0
- package/dist/nord/models/Subscriber.js +29 -0
- package/dist/nord/queries.d.ts +81 -0
- package/dist/nord/queries.js +181 -0
- package/dist/nord/types.d.ts +88 -0
- package/dist/nord/types.js +2 -0
- package/dist/nord/utils/NordError.d.ts +35 -0
- package/dist/nord/utils/NordError.js +46 -0
- package/dist/nord/websocket.d.ts +49 -0
- package/dist/nord/websocket.js +107 -0
- package/dist/operations/account.d.ts +58 -0
- package/dist/operations/account.js +112 -0
- package/dist/operations/market.d.ts +65 -0
- package/dist/operations/market.js +131 -0
- package/dist/operations/orders.d.ts +57 -0
- package/dist/operations/orders.js +129 -0
- package/dist/solana/NordUser.d.ts +78 -0
- package/dist/solana/NordUser.js +196 -0
- package/dist/solana/client.d.ts +139 -0
- package/dist/solana/client.js +360 -0
- package/dist/solana/const.d.ts +23 -0
- package/dist/solana/const.js +47 -0
- package/dist/solana/index.d.ts +5 -0
- package/dist/solana/index.js +23 -0
- package/dist/solana/types.d.ts +118 -0
- package/dist/solana/types.js +16 -0
- package/dist/solana/utils.d.ts +64 -0
- package/dist/solana/utils.js +131 -0
- package/dist/types/api.d.ts +152 -0
- package/dist/types/api.js +6 -0
- package/dist/types/config.d.ts +34 -0
- package/dist/types/config.js +6 -0
- package/dist/types.d.ts +144 -87
- package/dist/types.js +13 -2
- package/dist/utils/errors.d.ts +96 -0
- package/dist/utils/errors.js +132 -0
- package/dist/utils/http.d.ts +35 -0
- package/dist/utils/http.js +105 -0
- package/dist/utils.d.ts +14 -5
- package/dist/utils.js +26 -7
- package/dist/websocket/NordWebSocketClient.d.ts +71 -0
- package/dist/websocket/NordWebSocketClient.js +343 -0
- package/dist/websocket/client.d.ts +93 -0
- package/dist/websocket/client.js +222 -0
- package/dist/websocket/events.d.ts +19 -0
- package/dist/websocket/events.js +2 -0
- package/dist/websocket/index.d.ts +2 -0
- package/dist/websocket/index.js +5 -0
- package/dist/websocket.d.ts +55 -0
- package/dist/websocket.js +211 -0
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/Nord.html +2 -15
- package/docs/classes/NordUser.html +4 -4
- package/docs/enums/FillMode.html +2 -2
- package/docs/enums/KeyType.html +2 -2
- package/docs/enums/PeakTpsPeriodUnit.html +2 -2
- package/docs/enums/Side.html +2 -2
- package/docs/functions/assert.html +1 -1
- package/docs/functions/bigIntToProtoU128.html +1 -1
- package/docs/functions/checkPubKeyLength.html +1 -1
- package/docs/functions/checkedFetch.html +1 -1
- package/docs/functions/decodeLengthDelimited.html +1 -1
- package/docs/functions/encodeLengthDelimited.html +1 -1
- package/docs/functions/fillModeToProtoFillMode.html +1 -1
- package/docs/functions/findMarket.html +1 -1
- package/docs/functions/findToken.html +1 -1
- package/docs/functions/makeWalletSignFn.html +1 -1
- package/docs/functions/optExpect.html +1 -1
- package/docs/functions/optMap.html +1 -1
- package/docs/functions/optUnwrap.html +1 -1
- package/docs/functions/panic.html +1 -1
- package/docs/functions/signAction.html +1 -1
- package/docs/functions/toScaledU128.html +1 -1
- package/docs/functions/toScaledU64.html +1 -1
- package/docs/interfaces/Account.html +2 -2
- package/docs/interfaces/ActionInfo.html +2 -2
- package/docs/interfaces/ActionQuery.html +2 -2
- package/docs/interfaces/ActionResponse.html +2 -2
- package/docs/interfaces/ActionsExtendedInfo.html +2 -2
- package/docs/interfaces/ActionsQuery.html +2 -2
- package/docs/interfaces/ActionsResponse.html +2 -2
- package/docs/interfaces/AggregateMetrics.html +2 -2
- package/docs/interfaces/BlockQuery.html +2 -2
- package/docs/interfaces/BlockResponse.html +2 -2
- package/docs/interfaces/BlockSummary.html +2 -2
- package/docs/interfaces/BlockSummaryResponse.html +2 -2
- package/docs/interfaces/DeltaEvent.html +2 -2
- package/docs/interfaces/ERC20TokenInfo.html +2 -2
- package/docs/interfaces/Info.html +2 -2
- package/docs/interfaces/Market.html +2 -2
- package/docs/interfaces/MarketStats.html +2 -2
- package/docs/interfaces/MarketsStatsResponse.html +2 -2
- package/docs/interfaces/NordConfig.html +2 -2
- package/docs/interfaces/Order.html +2 -2
- package/docs/interfaces/OrderInfo.html +2 -2
- package/docs/interfaces/PerpMarketStats.html +2 -2
- package/docs/interfaces/RollmanActionExtendedInfo.html +2 -2
- package/docs/interfaces/RollmanActionInfo.html +2 -2
- package/docs/interfaces/RollmanActionResponse.html +2 -2
- package/docs/interfaces/RollmanActionsResponse.html +2 -2
- package/docs/interfaces/RollmanBlockResponse.html +2 -2
- package/docs/interfaces/SubscriberConfig.html +2 -2
- package/docs/interfaces/Token.html +2 -2
- package/docs/interfaces/Trade.html +2 -2
- package/docs/interfaces/Trades.html +2 -2
- package/docs/modules.html +0 -7
- package/docs/types/BigIntValue.html +1 -1
- package/docs/variables/DEBUG_KEYS.html +1 -1
- package/docs/variables/DEFAULT_FUNDING_AMOUNTS.html +1 -1
- package/docs/variables/DEV_CONTRACT_ADDRESS.html +1 -1
- package/docs/variables/DEV_TOKEN_INFOS.html +1 -1
- package/docs/variables/DEV_URL.html +1 -1
- package/docs/variables/ERC20_ABI.html +1 -1
- package/docs/variables/EVM_DEV_URL.html +1 -1
- package/docs/variables/FAUCET_PRIVATE_ADDRESS.html +1 -1
- package/docs/variables/MAX_BUFFER_LEN.html +1 -1
- package/docs/variables/NORD_GETTERS_FACET_ABI.html +1 -1
- package/docs/variables/NORD_RAMP_FACET_ABI.html +1 -1
- package/docs/variables/SESSION_TTL.html +1 -1
- package/docs/variables/WEBSERVER_DEV_URL.html +1 -1
- package/docs/variables/ZERO_DECIMAL.html +1 -1
- package/package.json +10 -12
- package/src/bridge/client.ts +487 -0
- package/src/bridge/const.ts +53 -0
- package/src/bridge/index.ts +7 -0
- package/src/bridge/types.ts +127 -0
- package/src/bridge/utils.ts +140 -0
- package/src/const.ts +20 -25
- package/src/gen/common.ts +27 -10
- package/src/gen/nord.ts +1044 -483
- package/src/idl/bridge.ts +702 -0
- package/src/index.ts +24 -5
- package/src/nord/{actions.ts → api/actions.ts} +33 -37
- package/src/nord/api/core.ts +130 -0
- package/src/nord/api/market.ts +125 -0
- package/src/nord/api/metrics.ts +154 -0
- package/src/nord/api/queries.ts +236 -0
- package/src/nord/client/Nord.ts +652 -0
- package/src/nord/client/NordUser.ts +1105 -0
- package/src/nord/index.ts +16 -2
- package/src/nord/models/Subscriber.ts +57 -0
- package/src/nord/utils/NordError.ts +72 -0
- package/src/types.ts +170 -99
- package/src/utils.ts +40 -19
- package/src/websocket/NordWebSocketClient.ts +432 -0
- package/src/websocket/events.ts +31 -0
- package/src/websocket/index.ts +2 -0
- package/tests/utils.spec.ts +24 -24
- package/docs/classes/Subscriber.html +0 -6
- package/docs/functions/createWebSocketSubscription.html +0 -12
- package/docs/interfaces/OrderbookOrder.html +0 -6
- package/docs/interfaces/OrderbookResponse.html +0 -10
- package/docs/interfaces/TradeInfo.html +0 -20
- package/docs/interfaces/TradesQueryParams.html +0 -10
- package/docs/interfaces/TradesResponse.html +0 -12
- package/src/abis/ERC20_ABI.ts +0 -310
- package/src/abis/NORD_GETTERS_FACET_ABI.ts +0 -192
- package/src/abis/NORD_RAMP_FACET_ABI.ts +0 -141
- package/src/abis/index.ts +0 -3
- package/src/nord/Nord.ts +0 -504
- package/src/nord/NordImpl.ts +0 -8
- package/src/nord/NordUser.ts +0 -469
package/dist/utils.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Decimal } from
|
|
2
|
-
import { KeyType, type Market, type Token } from
|
|
3
|
-
import * as proto from
|
|
4
|
-
import { ethers } from
|
|
5
|
-
import { RequestInfo, RequestInit, Response } from
|
|
1
|
+
import { Decimal } from 'decimal.js';
|
|
2
|
+
import { KeyType, type Market, type Token } from './types';
|
|
3
|
+
import * as proto from './gen/nord';
|
|
4
|
+
import { ethers } from 'ethers';
|
|
5
|
+
import { RequestInfo, RequestInit, Response } from 'node-fetch';
|
|
6
|
+
import { BN } from '@coral-xyz/anchor';
|
|
6
7
|
export declare const SESSION_TTL: bigint;
|
|
7
8
|
export declare const ZERO_DECIMAL: Decimal;
|
|
8
9
|
export declare const MAX_BUFFER_LEN = 10000;
|
|
@@ -112,3 +113,11 @@ export declare function decodeLengthDelimited<T, M extends proto.MessageFns<T>>(
|
|
|
112
113
|
export declare function checkPubKeyLength(keyType: KeyType, len: number): void;
|
|
113
114
|
export declare function findMarket(markets: Market[], marketId: number): Market;
|
|
114
115
|
export declare function findToken(tokens: Token[], tokenId: number): Token;
|
|
116
|
+
/**
|
|
117
|
+
* Convert a number to a BN with the specified number of decimals
|
|
118
|
+
*
|
|
119
|
+
* @param amount Amount as a number
|
|
120
|
+
* @param decimals Number of decimal places
|
|
121
|
+
* @returns Amount as a BN
|
|
122
|
+
*/
|
|
123
|
+
export declare function toBN(amount: number, decimals: number): BN;
|
package/dist/utils.js
CHANGED
|
@@ -18,6 +18,7 @@ exports.decodeLengthDelimited = decodeLengthDelimited;
|
|
|
18
18
|
exports.checkPubKeyLength = checkPubKeyLength;
|
|
19
19
|
exports.findMarket = findMarket;
|
|
20
20
|
exports.findToken = findToken;
|
|
21
|
+
exports.toBN = toBN;
|
|
21
22
|
const decimal_js_1 = require("decimal.js");
|
|
22
23
|
const ed25519_1 = require("@noble/curves/ed25519");
|
|
23
24
|
const bls12_381_1 = require("@noble/curves/bls12-381");
|
|
@@ -27,7 +28,8 @@ const types_1 = require("./types");
|
|
|
27
28
|
const wire_1 = require("@bufbuild/protobuf/wire");
|
|
28
29
|
const ethers_1 = require("ethers");
|
|
29
30
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
30
|
-
|
|
31
|
+
const anchor_1 = require("@coral-xyz/anchor");
|
|
32
|
+
exports.SESSION_TTL = 60n * 60n * 24n * 30n;
|
|
31
33
|
exports.ZERO_DECIMAL = new decimal_js_1.Decimal(0);
|
|
32
34
|
exports.MAX_BUFFER_LEN = 10000;
|
|
33
35
|
const MAX_PAYLOAD_SIZE = 100 * 1024; // 100 kB
|
|
@@ -36,7 +38,7 @@ function panic(message) {
|
|
|
36
38
|
}
|
|
37
39
|
function assert(predicate, message) {
|
|
38
40
|
if (!predicate)
|
|
39
|
-
panic(message ??
|
|
41
|
+
panic(message ?? 'Assertion violated');
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Extracts value out of optional if it's defined, or throws error if it's not
|
|
@@ -55,7 +57,7 @@ function optExpect(value, message) {
|
|
|
55
57
|
* @returns
|
|
56
58
|
*/
|
|
57
59
|
function optUnwrap(value) {
|
|
58
|
-
return optExpect(value,
|
|
60
|
+
return optExpect(value, 'Optional contains no value');
|
|
59
61
|
}
|
|
60
62
|
/**
|
|
61
63
|
* Applies function to value if it's defined, or passes `undefined` through
|
|
@@ -97,7 +99,7 @@ function signAction(action, sk, keyType) {
|
|
|
97
99
|
sig = secp256k1_1.secp256k1.sign((0, sha256_1.sha256)(action), sk).toCompactRawBytes();
|
|
98
100
|
}
|
|
99
101
|
else {
|
|
100
|
-
throw new Error(
|
|
102
|
+
throw new Error('Invalid key type');
|
|
101
103
|
}
|
|
102
104
|
return new Uint8Array([...action, ...sig]);
|
|
103
105
|
}
|
|
@@ -234,13 +236,13 @@ function decodeLengthDelimited(bytes, coder) {
|
|
|
234
236
|
}
|
|
235
237
|
function checkPubKeyLength(keyType, len) {
|
|
236
238
|
if (keyType === types_1.KeyType.Bls12_381) {
|
|
237
|
-
throw new Error(
|
|
239
|
+
throw new Error('Cannot create a user using Bls12_381, use Ed25119 or Secp256k1 instead.');
|
|
238
240
|
}
|
|
239
241
|
if (len !== 32 && keyType === types_1.KeyType.Ed25519) {
|
|
240
|
-
throw new Error(
|
|
242
|
+
throw new Error('Ed25519 pubkeys must be 32 length.');
|
|
241
243
|
}
|
|
242
244
|
if (len !== 33 && keyType === types_1.KeyType.Secp256k1) {
|
|
243
|
-
throw new Error(
|
|
245
|
+
throw new Error('Secp256k1 pubkeys must be 33 length.');
|
|
244
246
|
}
|
|
245
247
|
}
|
|
246
248
|
function findMarket(markets, marketId) {
|
|
@@ -255,3 +257,20 @@ function findToken(tokens, tokenId) {
|
|
|
255
257
|
}
|
|
256
258
|
return tokens[tokenId];
|
|
257
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Convert a number to a BN with the specified number of decimals
|
|
262
|
+
*
|
|
263
|
+
* @param amount Amount as a number
|
|
264
|
+
* @param decimals Number of decimal places
|
|
265
|
+
* @returns Amount as a BN
|
|
266
|
+
*/
|
|
267
|
+
function toBN(amount, decimals) {
|
|
268
|
+
const amountString = amount.toFixed(decimals);
|
|
269
|
+
const [whole, fraction] = amountString.split('.');
|
|
270
|
+
// Convert to smallest units (no decimals)
|
|
271
|
+
const wholeBN = new anchor_1.BN(whole).mul(new anchor_1.BN(10).pow(new anchor_1.BN(decimals)));
|
|
272
|
+
const fractionBN = fraction
|
|
273
|
+
? new anchor_1.BN(fraction.padEnd(decimals, '0').slice(0, decimals))
|
|
274
|
+
: new anchor_1.BN(0);
|
|
275
|
+
return wholeBN.add(fractionBN);
|
|
276
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { NordWebSocketClientEvents } from './events';
|
|
3
|
+
/**
|
|
4
|
+
* WebSocket client for Nord exchange
|
|
5
|
+
*
|
|
6
|
+
* This client connects to one of the specific Nord WebSocket endpoints:
|
|
7
|
+
* - /ws/trades - For trade updates
|
|
8
|
+
* - /ws/deltas - For orderbook delta updates
|
|
9
|
+
* - /ws/user - For user-specific updates
|
|
10
|
+
*
|
|
11
|
+
* Each endpoint handles a specific type of data and subscriptions must match
|
|
12
|
+
* the endpoint type (e.g., only 'trades@BTCUSDC' subscriptions are valid on
|
|
13
|
+
* the /ws/trades endpoint).
|
|
14
|
+
*/
|
|
15
|
+
export declare class NordWebSocketClient extends EventEmitter implements NordWebSocketClientEvents {
|
|
16
|
+
private ws;
|
|
17
|
+
private url;
|
|
18
|
+
private subscriptions;
|
|
19
|
+
private reconnectAttempts;
|
|
20
|
+
private maxReconnectAttempts;
|
|
21
|
+
private reconnectDelay;
|
|
22
|
+
private pingInterval;
|
|
23
|
+
private pingTimeout;
|
|
24
|
+
private isBrowser;
|
|
25
|
+
/**
|
|
26
|
+
* Create a new NordWebSocketClient
|
|
27
|
+
* @param url WebSocket server URL
|
|
28
|
+
*/
|
|
29
|
+
constructor(url: string);
|
|
30
|
+
/**
|
|
31
|
+
* Validate stream format
|
|
32
|
+
* @param stream Stream identifier to validate
|
|
33
|
+
* @throws Error if stream format is invalid
|
|
34
|
+
*/
|
|
35
|
+
private validateStream;
|
|
36
|
+
/**
|
|
37
|
+
* Setup WebSocket ping/pong heartbeat
|
|
38
|
+
*/
|
|
39
|
+
private setupHeartbeat;
|
|
40
|
+
/**
|
|
41
|
+
* Get the appropriate WebSocket class based on environment
|
|
42
|
+
*/
|
|
43
|
+
private getWebSocketClass;
|
|
44
|
+
/**
|
|
45
|
+
* Connect to the Nord WebSocket server
|
|
46
|
+
*/
|
|
47
|
+
connect(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to one or more streams
|
|
50
|
+
* @param streams Array of streams to subscribe to (e.g. ["trades@BTCUSDC", "deltas@BTCUSDC"])
|
|
51
|
+
*/
|
|
52
|
+
subscribe(streams: string[]): void;
|
|
53
|
+
/**
|
|
54
|
+
* Unsubscribe from one or more streams
|
|
55
|
+
* @param streams Array of streams to unsubscribe from
|
|
56
|
+
*/
|
|
57
|
+
unsubscribe(streams: string[]): void;
|
|
58
|
+
/**
|
|
59
|
+
* Close the WebSocket connection
|
|
60
|
+
*/
|
|
61
|
+
close(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Handle incoming WebSocket messages
|
|
64
|
+
* @param message WebSocket message
|
|
65
|
+
*/
|
|
66
|
+
private handleMessage;
|
|
67
|
+
/**
|
|
68
|
+
* Attempt to reconnect to the WebSocket server
|
|
69
|
+
*/
|
|
70
|
+
private reconnect;
|
|
71
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.NordWebSocketClient = void 0;
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
8
|
+
const events_1 = require("events");
|
|
9
|
+
const types_1 = require("../types");
|
|
10
|
+
const VALID_STREAM_TYPES = ['trades', 'deltas', 'user'];
|
|
11
|
+
// Constants for WebSocket readyState
|
|
12
|
+
const WS_OPEN = 1;
|
|
13
|
+
/**
|
|
14
|
+
* WebSocket client for Nord exchange
|
|
15
|
+
*
|
|
16
|
+
* This client connects to one of the specific Nord WebSocket endpoints:
|
|
17
|
+
* - /ws/trades - For trade updates
|
|
18
|
+
* - /ws/deltas - For orderbook delta updates
|
|
19
|
+
* - /ws/user - For user-specific updates
|
|
20
|
+
*
|
|
21
|
+
* Each endpoint handles a specific type of data and subscriptions must match
|
|
22
|
+
* the endpoint type (e.g., only 'trades@BTCUSDC' subscriptions are valid on
|
|
23
|
+
* the /ws/trades endpoint).
|
|
24
|
+
*/
|
|
25
|
+
class NordWebSocketClient extends events_1.EventEmitter {
|
|
26
|
+
/**
|
|
27
|
+
* Create a new NordWebSocketClient
|
|
28
|
+
* @param url WebSocket server URL
|
|
29
|
+
*/
|
|
30
|
+
constructor(url) {
|
|
31
|
+
super();
|
|
32
|
+
this.ws = null;
|
|
33
|
+
this.subscriptions = new Set();
|
|
34
|
+
this.reconnectAttempts = 0;
|
|
35
|
+
this.maxReconnectAttempts = 5;
|
|
36
|
+
this.reconnectDelay = 1000;
|
|
37
|
+
this.pingInterval = null;
|
|
38
|
+
this.pingTimeout = null;
|
|
39
|
+
this.url = url;
|
|
40
|
+
// Check if we're in a browser environment
|
|
41
|
+
// The most reliable way is to check for Node.js process
|
|
42
|
+
this.isBrowser =
|
|
43
|
+
typeof process === 'undefined' ||
|
|
44
|
+
!process.versions ||
|
|
45
|
+
!process.versions.node;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Validate stream format
|
|
49
|
+
* @param stream Stream identifier to validate
|
|
50
|
+
* @throws Error if stream format is invalid
|
|
51
|
+
*/
|
|
52
|
+
validateStream(stream) {
|
|
53
|
+
const [type, params] = stream.split('@');
|
|
54
|
+
if (!type || !params) {
|
|
55
|
+
throw new Error(`Invalid stream format: ${stream}. Expected format: <type>@<params>`);
|
|
56
|
+
}
|
|
57
|
+
// Extract the endpoint from the URL
|
|
58
|
+
const urlPath = new URL(this.url).pathname;
|
|
59
|
+
const endpoint = urlPath.split('/').pop();
|
|
60
|
+
// Ensure the stream type matches the endpoint we're connected to
|
|
61
|
+
if (endpoint && type !== endpoint) {
|
|
62
|
+
throw new Error(`Stream type '${type}' doesn't match the connected endpoint '${endpoint}'`);
|
|
63
|
+
}
|
|
64
|
+
if (!VALID_STREAM_TYPES.includes(type)) {
|
|
65
|
+
throw new Error(`Invalid stream type: ${type}. Valid types are: ${VALID_STREAM_TYPES.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
if (type === 'user' && !/^\d+$/.test(params)) {
|
|
68
|
+
throw new Error(`Invalid user ID in stream: ${params}. Expected numeric ID`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Setup WebSocket ping/pong heartbeat
|
|
73
|
+
*/
|
|
74
|
+
setupHeartbeat() {
|
|
75
|
+
if (this.pingInterval) {
|
|
76
|
+
clearInterval(this.pingInterval);
|
|
77
|
+
}
|
|
78
|
+
if (this.pingTimeout) {
|
|
79
|
+
clearTimeout(this.pingTimeout);
|
|
80
|
+
}
|
|
81
|
+
// In browser, we rely on the browser's WebSocket implementation to handle ping/pong
|
|
82
|
+
if (this.isBrowser) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.pingInterval = setInterval(() => {
|
|
86
|
+
if (this.ws && !this.isBrowser) {
|
|
87
|
+
// Only use ping() method in Node.js environment
|
|
88
|
+
this.ws.ping();
|
|
89
|
+
// Set timeout for pong response
|
|
90
|
+
this.pingTimeout = setTimeout(() => {
|
|
91
|
+
this.emit('error', new Error('WebSocket ping timeout'));
|
|
92
|
+
this.close();
|
|
93
|
+
this.reconnect();
|
|
94
|
+
}, 5000); // 5 second timeout
|
|
95
|
+
}
|
|
96
|
+
}, 30000); // Send ping every 30 seconds
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get the appropriate WebSocket class based on environment
|
|
100
|
+
*/
|
|
101
|
+
getWebSocketClass() {
|
|
102
|
+
if (this.isBrowser) {
|
|
103
|
+
// In browser environments
|
|
104
|
+
// @ts-ignore - Check for WebSocket in globalThis
|
|
105
|
+
if (typeof globalThis !== 'undefined' && globalThis.WebSocket) {
|
|
106
|
+
// @ts-ignore - Return WebSocket from globalThis
|
|
107
|
+
return globalThis.WebSocket;
|
|
108
|
+
}
|
|
109
|
+
throw new Error('WebSocket is not available in this environment');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// In Node.js
|
|
113
|
+
return ws_1.default;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Connect to the Nord WebSocket server
|
|
118
|
+
*/
|
|
119
|
+
connect() {
|
|
120
|
+
if (this.ws) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const WebSocketClass = this.getWebSocketClass();
|
|
125
|
+
if (this.isBrowser) {
|
|
126
|
+
// Browser WebSocket setup
|
|
127
|
+
this.ws = new WebSocketClass(this.url);
|
|
128
|
+
this.ws.onopen = () => {
|
|
129
|
+
this.emit('connected');
|
|
130
|
+
this.reconnectAttempts = 0;
|
|
131
|
+
this.reconnectDelay = 1000;
|
|
132
|
+
// Resubscribe to previous subscriptions
|
|
133
|
+
if (this.subscriptions.size > 0) {
|
|
134
|
+
this.subscribe([...this.subscriptions]);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
this.ws.onmessage = (event) => {
|
|
138
|
+
try {
|
|
139
|
+
const message = JSON.parse(event.data);
|
|
140
|
+
this.handleMessage(message);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.emit('error', new Error(`Failed to parse message: ${error instanceof Error ? error.message : String(error)}`));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
this.ws.onclose = (event) => {
|
|
147
|
+
const reason = event && event.reason ? ` Reason: ${event.reason}` : '';
|
|
148
|
+
const code = event && event.code ? ` Code: ${event.code}` : '';
|
|
149
|
+
this.emit('disconnected');
|
|
150
|
+
console.log(`WebSocket closed.${code}${reason}`);
|
|
151
|
+
this.reconnect();
|
|
152
|
+
};
|
|
153
|
+
this.ws.onerror = (event) => {
|
|
154
|
+
const errorMsg = `WebSocket error: ${event && event.type ? event.type : 'unknown'}`;
|
|
155
|
+
console.error(errorMsg, event);
|
|
156
|
+
this.emit('error', new Error(errorMsg));
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Node.js WebSocket setup
|
|
161
|
+
const nodeWs = new WebSocketClass(this.url);
|
|
162
|
+
this.ws = nodeWs;
|
|
163
|
+
nodeWs.on('open', () => {
|
|
164
|
+
this.emit('connected');
|
|
165
|
+
this.reconnectAttempts = 0;
|
|
166
|
+
this.reconnectDelay = 1000;
|
|
167
|
+
this.setupHeartbeat();
|
|
168
|
+
// Resubscribe to previous subscriptions
|
|
169
|
+
if (this.subscriptions.size > 0) {
|
|
170
|
+
this.subscribe([...this.subscriptions]);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
nodeWs.on('message', (data) => {
|
|
174
|
+
try {
|
|
175
|
+
const message = JSON.parse(data.toString());
|
|
176
|
+
this.handleMessage(message);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
this.emit('error', new Error(`Failed to parse message: ${error instanceof Error ? error.message : String(error)}`));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
nodeWs.on('close', (code, reason) => {
|
|
183
|
+
this.emit('disconnected');
|
|
184
|
+
console.log(`WebSocket closed. Code: ${code} Reason: ${reason}`);
|
|
185
|
+
if (this.pingInterval) {
|
|
186
|
+
clearInterval(this.pingInterval);
|
|
187
|
+
}
|
|
188
|
+
if (this.pingTimeout) {
|
|
189
|
+
clearTimeout(this.pingTimeout);
|
|
190
|
+
}
|
|
191
|
+
this.reconnect();
|
|
192
|
+
});
|
|
193
|
+
nodeWs.on('error', (error) => {
|
|
194
|
+
console.error('WebSocket error:', error);
|
|
195
|
+
this.emit('error', error);
|
|
196
|
+
});
|
|
197
|
+
nodeWs.on('pong', () => {
|
|
198
|
+
if (this.pingTimeout) {
|
|
199
|
+
clearTimeout(this.pingTimeout);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const errorMsg = `Failed to initialize WebSocket: ${error instanceof Error ? error.message : String(error)}`;
|
|
206
|
+
console.error(errorMsg);
|
|
207
|
+
this.emit('error', new Error(errorMsg));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Subscribe to one or more streams
|
|
212
|
+
* @param streams Array of streams to subscribe to (e.g. ["trades@BTCUSDC", "deltas@BTCUSDC"])
|
|
213
|
+
*/
|
|
214
|
+
subscribe(streams) {
|
|
215
|
+
// Validate all streams first
|
|
216
|
+
try {
|
|
217
|
+
streams.forEach((stream) => this.validateStream(stream));
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!this.ws ||
|
|
224
|
+
(this.isBrowser
|
|
225
|
+
? this.ws.readyState !== WS_OPEN
|
|
226
|
+
: this.ws.readyState !== ws_1.default.OPEN)) {
|
|
227
|
+
streams.forEach((stream) => this.subscriptions.add(stream));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const message = {
|
|
231
|
+
type: types_1.WebSocketMessageType.Subscribe,
|
|
232
|
+
streams,
|
|
233
|
+
};
|
|
234
|
+
try {
|
|
235
|
+
const messageStr = JSON.stringify(message);
|
|
236
|
+
if (this.isBrowser) {
|
|
237
|
+
this.ws.send(messageStr);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this.ws.send(messageStr);
|
|
241
|
+
}
|
|
242
|
+
streams.forEach((stream) => this.subscriptions.add(stream));
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Unsubscribe from one or more streams
|
|
250
|
+
* @param streams Array of streams to unsubscribe from
|
|
251
|
+
*/
|
|
252
|
+
unsubscribe(streams) {
|
|
253
|
+
// Validate all streams first
|
|
254
|
+
try {
|
|
255
|
+
streams.forEach((stream) => this.validateStream(stream));
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!this.ws ||
|
|
262
|
+
(this.isBrowser
|
|
263
|
+
? this.ws.readyState !== WS_OPEN
|
|
264
|
+
: this.ws.readyState !== ws_1.default.OPEN)) {
|
|
265
|
+
streams.forEach((stream) => this.subscriptions.delete(stream));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const message = {
|
|
269
|
+
type: types_1.WebSocketMessageType.Unsubscribe,
|
|
270
|
+
streams,
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
const messageStr = JSON.stringify(message);
|
|
274
|
+
if (this.isBrowser) {
|
|
275
|
+
this.ws.send(messageStr);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
this.ws.send(messageStr);
|
|
279
|
+
}
|
|
280
|
+
streams.forEach((stream) => this.subscriptions.delete(stream));
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Close the WebSocket connection
|
|
288
|
+
*/
|
|
289
|
+
close() {
|
|
290
|
+
if (this.ws) {
|
|
291
|
+
if (this.isBrowser) {
|
|
292
|
+
this.ws.close();
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
this.ws.close();
|
|
296
|
+
}
|
|
297
|
+
this.ws = null;
|
|
298
|
+
}
|
|
299
|
+
if (this.pingInterval) {
|
|
300
|
+
clearInterval(this.pingInterval);
|
|
301
|
+
this.pingInterval = null;
|
|
302
|
+
}
|
|
303
|
+
if (this.pingTimeout) {
|
|
304
|
+
clearTimeout(this.pingTimeout);
|
|
305
|
+
this.pingTimeout = null;
|
|
306
|
+
}
|
|
307
|
+
this.subscriptions.clear();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Handle incoming WebSocket messages
|
|
311
|
+
* @param message WebSocket message
|
|
312
|
+
*/
|
|
313
|
+
handleMessage(message) {
|
|
314
|
+
switch (message.type) {
|
|
315
|
+
case types_1.WebSocketMessageType.TradeUpdate:
|
|
316
|
+
this.emit('trade', message);
|
|
317
|
+
break;
|
|
318
|
+
case types_1.WebSocketMessageType.DeltaUpdate:
|
|
319
|
+
this.emit('delta', message);
|
|
320
|
+
break;
|
|
321
|
+
case types_1.WebSocketMessageType.UserUpdate:
|
|
322
|
+
this.emit('user', message);
|
|
323
|
+
break;
|
|
324
|
+
default:
|
|
325
|
+
this.emit('error', new Error(`Unknown message type: ${message.type}`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Attempt to reconnect to the WebSocket server
|
|
330
|
+
*/
|
|
331
|
+
reconnect() {
|
|
332
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
333
|
+
this.emit('error', new Error('Max reconnection attempts reached'));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
this.reconnectAttempts++;
|
|
338
|
+
this.reconnectDelay *= 2; // Exponential backoff
|
|
339
|
+
this.connect();
|
|
340
|
+
}, this.reconnectDelay);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
exports.NordWebSocketClient = NordWebSocketClient;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client for real-time data
|
|
3
|
+
* @module websocket/client
|
|
4
|
+
*/
|
|
5
|
+
import { OrderbookData, Trade } from '../models';
|
|
6
|
+
/**
|
|
7
|
+
* WebSocket client for real-time data
|
|
8
|
+
*/
|
|
9
|
+
export declare class WebSocketClient {
|
|
10
|
+
/**
|
|
11
|
+
* WebSocket connection
|
|
12
|
+
*/
|
|
13
|
+
private ws;
|
|
14
|
+
/**
|
|
15
|
+
* Base URL for the WebSocket server
|
|
16
|
+
*/
|
|
17
|
+
private readonly baseUrl;
|
|
18
|
+
/**
|
|
19
|
+
* Map of subscription IDs to callbacks
|
|
20
|
+
*/
|
|
21
|
+
private subscriptions;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the WebSocket is connected
|
|
24
|
+
*/
|
|
25
|
+
private connected;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the WebSocket is connecting
|
|
28
|
+
*/
|
|
29
|
+
private connecting;
|
|
30
|
+
/**
|
|
31
|
+
* Queue of messages to send when connected
|
|
32
|
+
*/
|
|
33
|
+
private messageQueue;
|
|
34
|
+
/**
|
|
35
|
+
* Create a new WebSocket client
|
|
36
|
+
*
|
|
37
|
+
* @param baseUrl - Base URL for the WebSocket server
|
|
38
|
+
*/
|
|
39
|
+
constructor(baseUrl: string);
|
|
40
|
+
/**
|
|
41
|
+
* Connect to the WebSocket server
|
|
42
|
+
*
|
|
43
|
+
* @returns Promise that resolves when connected
|
|
44
|
+
*/
|
|
45
|
+
connect(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Disconnect from the WebSocket server
|
|
48
|
+
*/
|
|
49
|
+
disconnect(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Send a message to the WebSocket server
|
|
52
|
+
*
|
|
53
|
+
* @param message - Message to send
|
|
54
|
+
*/
|
|
55
|
+
private sendMessage;
|
|
56
|
+
/**
|
|
57
|
+
* Generate a unique subscription ID
|
|
58
|
+
*
|
|
59
|
+
* @returns Unique subscription ID
|
|
60
|
+
*/
|
|
61
|
+
private generateSubscriptionId;
|
|
62
|
+
/**
|
|
63
|
+
* Subscribe to orderbook updates for a specific market
|
|
64
|
+
*
|
|
65
|
+
* @param market - Market symbol (e.g., "BTCUSDC")
|
|
66
|
+
* @param callback - Callback function to handle updates
|
|
67
|
+
* @returns Subscription ID
|
|
68
|
+
*/
|
|
69
|
+
subscribeToOrderbook(market: string, callback: (data: OrderbookData) => void): string;
|
|
70
|
+
/**
|
|
71
|
+
* Subscribe to trade updates for a specific market
|
|
72
|
+
*
|
|
73
|
+
* @param market - Market symbol (e.g., "BTCUSDC")
|
|
74
|
+
* @param callback - Callback function to handle updates
|
|
75
|
+
* @returns Subscription ID
|
|
76
|
+
*/
|
|
77
|
+
subscribeToTrades(market: string, callback: (data: Trade[]) => void): string;
|
|
78
|
+
/**
|
|
79
|
+
* Subscribe to user updates for a specific account
|
|
80
|
+
*
|
|
81
|
+
* @param accountId - Account ID
|
|
82
|
+
* @param callback - Callback function to handle updates
|
|
83
|
+
* @returns Subscription ID
|
|
84
|
+
*/
|
|
85
|
+
subscribeToUserUpdates(accountId: number, callback: (data: any) => void): string;
|
|
86
|
+
/**
|
|
87
|
+
* Unsubscribe from a subscription
|
|
88
|
+
*
|
|
89
|
+
* @param subscriptionId - Subscription ID to unsubscribe from
|
|
90
|
+
* @returns Whether the unsubscription was successful
|
|
91
|
+
*/
|
|
92
|
+
unsubscribe(subscriptionId: string): boolean;
|
|
93
|
+
}
|