@n1xyz/nord-ts 0.0.20 → 0.0.22
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/dist/gen/nord_pb.d.ts +3651 -0
- package/dist/gen/nord_pb.js +892 -0
- package/dist/gen/openapi.d.ts +241 -2
- package/dist/nord/api/actions.d.ts +30 -72
- package/dist/nord/api/actions.js +179 -200
- package/dist/nord/api/core.d.ts +1 -1
- package/dist/nord/api/core.js +1 -13
- package/dist/nord/client/Nord.d.ts +3 -3
- package/dist/nord/client/Nord.js +6 -9
- package/dist/nord/client/NordUser.d.ts +26 -13
- package/dist/nord/client/NordUser.js +13 -10
- package/dist/types.d.ts +13 -11
- package/dist/types.js +29 -4
- package/dist/utils.d.ts +6 -20
- package/dist/utils.js +17 -35
- package/dist/websocket/NordWebSocketClient.d.ts +0 -11
- package/dist/websocket/NordWebSocketClient.js +2 -98
- package/package.json +27 -24
- package/src/gen/nord_pb.ts +4172 -0
- package/src/gen/openapi.ts +241 -2
- package/src/nord/api/actions.ts +249 -370
- package/src/nord/api/core.ts +1 -16
- package/src/nord/client/Nord.ts +9 -13
- package/src/nord/client/NordUser.ts +40 -19
- package/src/types.ts +32 -12
- package/src/utils.ts +24 -43
- package/src/websocket/NordWebSocketClient.ts +2 -121
package/src/nord/api/core.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { checkedFetch } from "../../utils";
|
|
1
|
+
import { SubscriptionPattern } from "../../types";
|
|
3
2
|
import { NordWebSocketClient } from "../../websocket/index";
|
|
4
3
|
import { NordError } from "../utils/NordError";
|
|
5
4
|
|
|
@@ -19,12 +18,10 @@ import { NordError } from "../utils/NordError";
|
|
|
19
18
|
export function initWebSocketClient(
|
|
20
19
|
webServerUrl: string,
|
|
21
20
|
subscriptions?: SubscriptionPattern[] | "trades" | "delta" | "account",
|
|
22
|
-
initialSubscriptions?: SubscriptionPattern[],
|
|
23
21
|
): NordWebSocketClient {
|
|
24
22
|
try {
|
|
25
23
|
// Determine URL and subscriptions based on parameters
|
|
26
24
|
let wsUrl = webServerUrl.replace(/^http/, "ws") + `/ws`;
|
|
27
|
-
let wsSubscriptions: SubscriptionPattern[] = [];
|
|
28
25
|
|
|
29
26
|
// Validate subscriptions parameter
|
|
30
27
|
if (typeof subscriptions === "string") {
|
|
@@ -35,12 +32,6 @@ export function initWebSocketClient(
|
|
|
35
32
|
subscriptions === "account"
|
|
36
33
|
) {
|
|
37
34
|
wsUrl += `/${subscriptions}`;
|
|
38
|
-
// If initialSubscriptions provided, use them
|
|
39
|
-
if (initialSubscriptions && initialSubscriptions.length > 0) {
|
|
40
|
-
// Validate initialSubscriptions
|
|
41
|
-
initialSubscriptions.forEach(validateSubscription);
|
|
42
|
-
wsSubscriptions = initialSubscriptions;
|
|
43
|
-
}
|
|
44
35
|
} else {
|
|
45
36
|
throw new NordError(
|
|
46
37
|
`Invalid endpoint: ${subscriptions}. Must be "trades", "deltas", or "account".`,
|
|
@@ -68,12 +59,6 @@ export function initWebSocketClient(
|
|
|
68
59
|
// Add connected handler for debugging
|
|
69
60
|
ws.on("connected", () => {
|
|
70
61
|
console.log("Nord WebSocket connected successfully");
|
|
71
|
-
|
|
72
|
-
// Subscribe to additional subscriptions if provided
|
|
73
|
-
// For new format, these are already part of the URL
|
|
74
|
-
if (wsSubscriptions.length > 0) {
|
|
75
|
-
ws.subscribe(wsSubscriptions);
|
|
76
|
-
}
|
|
77
62
|
});
|
|
78
63
|
|
|
79
64
|
// Connect the WebSocket
|
package/src/nord/client/Nord.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { ProtonClient } from "@n1xyz/proton";
|
|
2
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
1
3
|
import { EventEmitter } from "events";
|
|
2
|
-
import { PublicKey, Connection } from "@solana/web3.js";
|
|
3
4
|
import createClient, { Client, FetchOptions } from "openapi-fetch";
|
|
5
|
+
import * as proto from "../../gen/nord_pb";
|
|
6
|
+
import type { paths } from "../../gen/openapi.ts";
|
|
4
7
|
import {
|
|
5
|
-
Info,
|
|
6
8
|
Account,
|
|
7
9
|
ActionResponse,
|
|
8
10
|
AggregateMetrics,
|
|
11
|
+
Info,
|
|
9
12
|
Market,
|
|
13
|
+
MarketStats,
|
|
10
14
|
NordConfig,
|
|
11
15
|
OrderbookQuery,
|
|
12
16
|
OrderbookResponse,
|
|
@@ -15,18 +19,13 @@ import {
|
|
|
15
19
|
Token,
|
|
16
20
|
TradesResponse,
|
|
17
21
|
User,
|
|
18
|
-
MarketStats,
|
|
19
22
|
} from "../../types";
|
|
20
|
-
import
|
|
21
|
-
import * as proto from "../../gen/nord";
|
|
22
|
-
// import { base64 } from "@scure/base";
|
|
23
|
+
import * as utils from "../../utils";
|
|
23
24
|
import { NordWebSocketClient } from "../../websocket/index";
|
|
24
25
|
import * as core from "../api/core";
|
|
25
26
|
import * as metrics from "../api/metrics";
|
|
26
|
-
import * as utils from "../../utils";
|
|
27
27
|
import { OrderbookSubscription, TradeSubscription } from "../models/Subscriber";
|
|
28
28
|
import { NordError } from "../utils/NordError";
|
|
29
|
-
import type { paths } from "../../gen/openapi.ts";
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
31
|
* User subscription interface
|
|
@@ -306,9 +305,9 @@ export class Nord {
|
|
|
306
305
|
actionId: x.actionId,
|
|
307
306
|
action: utils.decodeLengthDelimited(
|
|
308
307
|
Buffer.from(x.payload, "base64"),
|
|
309
|
-
proto.
|
|
308
|
+
proto.ActionSchema,
|
|
310
309
|
),
|
|
311
|
-
physicalExecTime: new Date(x.physicalTime
|
|
310
|
+
physicalExecTime: new Date(x.physicalTime),
|
|
312
311
|
}));
|
|
313
312
|
}
|
|
314
313
|
|
|
@@ -427,7 +426,6 @@ export class Nord {
|
|
|
427
426
|
wsClient.on("delta", handleDelta);
|
|
428
427
|
|
|
429
428
|
subscription.close = () => {
|
|
430
|
-
wsClient.unsubscribe([`deltas@${symbol}`]);
|
|
431
429
|
wsClient.removeListener("delta", handleDelta);
|
|
432
430
|
subscription.removeAllListeners();
|
|
433
431
|
};
|
|
@@ -471,7 +469,6 @@ export class Nord {
|
|
|
471
469
|
wsClient.on("trades", handleTrade);
|
|
472
470
|
|
|
473
471
|
subscription.close = () => {
|
|
474
|
-
wsClient.unsubscribe([`trades@${symbol}`]);
|
|
475
472
|
wsClient.removeListener("trades", handleTrade);
|
|
476
473
|
subscription.removeAllListeners();
|
|
477
474
|
};
|
|
@@ -507,7 +504,6 @@ export class Nord {
|
|
|
507
504
|
wsClient.on("account", handleAccountUpdate);
|
|
508
505
|
|
|
509
506
|
subscription.close = () => {
|
|
510
|
-
wsClient.unsubscribe([`account@${accountId}`]);
|
|
511
507
|
wsClient.removeListener("account", handleAccountUpdate);
|
|
512
508
|
subscription.removeAllListeners();
|
|
513
509
|
};
|
|
@@ -4,14 +4,19 @@ import {
|
|
|
4
4
|
TOKEN_PROGRAM_ID,
|
|
5
5
|
TOKEN_2022_PROGRAM_ID,
|
|
6
6
|
} from "@solana/spl-token";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Connection,
|
|
9
|
+
PublicKey,
|
|
10
|
+
Transaction,
|
|
11
|
+
SendOptions,
|
|
12
|
+
} from "@solana/web3.js";
|
|
8
13
|
import Decimal from "decimal.js";
|
|
9
14
|
import * as ed from "@noble/ed25519";
|
|
10
15
|
import { sha512 } from "@noble/hashes/sha512";
|
|
11
16
|
ed.etc.sha512Sync = sha512;
|
|
12
17
|
import { floatToScaledBigIntLossy } from "@n1xyz/proton";
|
|
13
|
-
import { FillMode,
|
|
14
|
-
import * as proto from "../../gen/
|
|
18
|
+
import { FillMode, Side, SPLTokenInfo, QuoteSize } from "../../types";
|
|
19
|
+
import * as proto from "../../gen/nord_pb";
|
|
15
20
|
import {
|
|
16
21
|
BigIntValue,
|
|
17
22
|
checkedFetch,
|
|
@@ -87,8 +92,8 @@ export interface PlaceOrderParams {
|
|
|
87
92
|
/** Order price */
|
|
88
93
|
price?: Decimal.Value;
|
|
89
94
|
|
|
90
|
-
/** Quote size (
|
|
91
|
-
quoteSize?:
|
|
95
|
+
/** Quote size object (requires non-zero price and size) */
|
|
96
|
+
quoteSize?: QuoteSize;
|
|
92
97
|
|
|
93
98
|
/** Account ID to place the order from */
|
|
94
99
|
accountId?: number;
|
|
@@ -142,8 +147,8 @@ export interface UserAtomicSubaction {
|
|
|
142
147
|
/** Order price */
|
|
143
148
|
price?: Decimal.Value;
|
|
144
149
|
|
|
145
|
-
/** Quote size (for market
|
|
146
|
-
quoteSize?:
|
|
150
|
+
/** Quote size object (for market-style placement) */
|
|
151
|
+
quoteSize?: QuoteSize;
|
|
147
152
|
|
|
148
153
|
/** The client order ID of the order. */
|
|
149
154
|
clientOrderId?: BigIntValue;
|
|
@@ -471,6 +476,7 @@ export class NordUser {
|
|
|
471
476
|
* @param amount - Amount to deposit
|
|
472
477
|
* @param tokenId - Token ID
|
|
473
478
|
* @param recipient - Recipient address; defaults to the user's address
|
|
479
|
+
* @param sendOptions - Send options for .sendTransaction
|
|
474
480
|
* @returns Transaction signature
|
|
475
481
|
* @throws {NordError} If required parameters are missing or operation fails
|
|
476
482
|
*/
|
|
@@ -478,10 +484,12 @@ export class NordUser {
|
|
|
478
484
|
amount,
|
|
479
485
|
tokenId,
|
|
480
486
|
recipient,
|
|
487
|
+
sendOptions,
|
|
481
488
|
}: Readonly<{
|
|
482
489
|
amount: number;
|
|
483
490
|
tokenId: number;
|
|
484
491
|
recipient?: PublicKey;
|
|
492
|
+
sendOptions?: SendOptions;
|
|
485
493
|
}>): Promise<string> {
|
|
486
494
|
try {
|
|
487
495
|
// Find the token info
|
|
@@ -502,8 +510,7 @@ export class NordUser {
|
|
|
502
510
|
sourceTokenAccount: fromAccount,
|
|
503
511
|
});
|
|
504
512
|
|
|
505
|
-
const { blockhash } =
|
|
506
|
-
await this.connection.getLatestBlockhash("confirmed");
|
|
513
|
+
const { blockhash } = await this.connection.getLatestBlockhash();
|
|
507
514
|
const tx = new Transaction();
|
|
508
515
|
|
|
509
516
|
tx.add(ix);
|
|
@@ -515,6 +522,7 @@ export class NordUser {
|
|
|
515
522
|
|
|
516
523
|
const signature = await this.connection.sendRawTransaction(
|
|
517
524
|
signedTx.serialize(),
|
|
525
|
+
sendOptions,
|
|
518
526
|
);
|
|
519
527
|
|
|
520
528
|
return signature;
|
|
@@ -657,7 +665,7 @@ export class NordUser {
|
|
|
657
665
|
* @throws {NordError} If the operation fails
|
|
658
666
|
*/
|
|
659
667
|
async refreshSession(): Promise<void> {
|
|
660
|
-
|
|
668
|
+
const result = await createSession(
|
|
661
669
|
this.nord.webServerUrl,
|
|
662
670
|
this.walletSignFn,
|
|
663
671
|
await this.nord.getTimestamp(),
|
|
@@ -667,6 +675,7 @@ export class NordUser {
|
|
|
667
675
|
sessionPubkey: this.sessionPubKey,
|
|
668
676
|
},
|
|
669
677
|
);
|
|
678
|
+
this.sessionId = result.sessionId;
|
|
670
679
|
}
|
|
671
680
|
/**
|
|
672
681
|
* Revoke a session
|
|
@@ -746,10 +755,14 @@ export class NordUser {
|
|
|
746
755
|
* Place an order on the exchange
|
|
747
756
|
*
|
|
748
757
|
* @param params - Order parameters
|
|
749
|
-
* @returns
|
|
758
|
+
* @returns Object containing actionId, orderId (if posted), fills, and clientOrderId
|
|
750
759
|
* @throws {NordError} If the operation fails
|
|
751
760
|
*/
|
|
752
|
-
async placeOrder(params: PlaceOrderParams): Promise<
|
|
761
|
+
async placeOrder(params: PlaceOrderParams): Promise<{
|
|
762
|
+
actionId: bigint;
|
|
763
|
+
orderId?: bigint;
|
|
764
|
+
fills: proto.Receipt_Trade[];
|
|
765
|
+
}> {
|
|
753
766
|
try {
|
|
754
767
|
this.checkSessionValidity();
|
|
755
768
|
const market = findMarket(this.nord.markets, params.marketId);
|
|
@@ -757,7 +770,7 @@ export class NordUser {
|
|
|
757
770
|
throw new NordError(`Market with ID ${params.marketId} not found`);
|
|
758
771
|
}
|
|
759
772
|
|
|
760
|
-
|
|
773
|
+
const result = await placeOrder(
|
|
761
774
|
this.nord.webServerUrl,
|
|
762
775
|
this.sessionSignFn,
|
|
763
776
|
await this.nord.getTimestamp(),
|
|
@@ -776,6 +789,7 @@ export class NordUser {
|
|
|
776
789
|
quoteSize: params.quoteSize,
|
|
777
790
|
},
|
|
778
791
|
);
|
|
792
|
+
return result;
|
|
779
793
|
} catch (error) {
|
|
780
794
|
throw new NordError("Failed to place order", { cause: error });
|
|
781
795
|
}
|
|
@@ -786,18 +800,22 @@ export class NordUser {
|
|
|
786
800
|
*
|
|
787
801
|
* @param orderId - Order ID to cancel
|
|
788
802
|
* @param providedAccountId - Account ID that placed the order
|
|
789
|
-
* @returns
|
|
803
|
+
* @returns Object containing actionId, cancelled orderId, and accountId
|
|
790
804
|
* @throws {NordError} If the operation fails
|
|
791
805
|
*/
|
|
792
806
|
async cancelOrder(
|
|
793
807
|
orderId: BigIntValue,
|
|
794
808
|
providedAccountId?: number,
|
|
795
|
-
): Promise<
|
|
809
|
+
): Promise<{
|
|
810
|
+
actionId: bigint;
|
|
811
|
+
orderId: bigint;
|
|
812
|
+
accountId: number;
|
|
813
|
+
}> {
|
|
796
814
|
const accountId =
|
|
797
815
|
providedAccountId != null ? providedAccountId : this.accountIds?.[0];
|
|
798
816
|
try {
|
|
799
817
|
this.checkSessionValidity();
|
|
800
|
-
|
|
818
|
+
const result = await cancelOrder(
|
|
801
819
|
this.nord.webServerUrl,
|
|
802
820
|
this.sessionSignFn,
|
|
803
821
|
await this.nord.getTimestamp(),
|
|
@@ -808,6 +826,7 @@ export class NordUser {
|
|
|
808
826
|
orderId,
|
|
809
827
|
},
|
|
810
828
|
);
|
|
829
|
+
return result;
|
|
811
830
|
} catch (error) {
|
|
812
831
|
throw new NordError(`Failed to cancel order ${orderId}`, {
|
|
813
832
|
cause: error,
|
|
@@ -860,7 +879,10 @@ export class NordUser {
|
|
|
860
879
|
async atomic(
|
|
861
880
|
userActions: UserAtomicSubaction[],
|
|
862
881
|
providedAccountId?: number,
|
|
863
|
-
): Promise<
|
|
882
|
+
): Promise<{
|
|
883
|
+
actionId: bigint;
|
|
884
|
+
results: proto.Receipt_AtomicSubactionResultKind[];
|
|
885
|
+
}> {
|
|
864
886
|
try {
|
|
865
887
|
this.checkSessionValidity();
|
|
866
888
|
|
|
@@ -889,8 +911,7 @@ export class NordUser {
|
|
|
889
911
|
priceDecimals: market.priceDecimals,
|
|
890
912
|
size: act.size,
|
|
891
913
|
price: act.price,
|
|
892
|
-
|
|
893
|
-
quoteSizePrice: undefined,
|
|
914
|
+
quoteSize: act.quoteSize,
|
|
894
915
|
clientOrderId: act.clientOrderId,
|
|
895
916
|
} as ApiAtomicSubaction;
|
|
896
917
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import * as proto from "./gen/
|
|
1
|
+
import * as proto from "./gen/nord_pb";
|
|
2
2
|
import type { components } from "./gen/openapi.ts";
|
|
3
|
+
import Decimal from "decimal.js";
|
|
4
|
+
import { toScaledU64 } from "./utils";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* The peak TPS rate is queried over the specified period.
|
|
@@ -251,21 +253,11 @@ export interface ActionNonceResponse {
|
|
|
251
253
|
* WebSocket message types
|
|
252
254
|
*/
|
|
253
255
|
export enum WebSocketMessageType {
|
|
254
|
-
Subscribe = "subscribe",
|
|
255
|
-
Unsubscribe = "unsubscribe",
|
|
256
256
|
TradeUpdate = "trades",
|
|
257
257
|
DeltaUpdate = "delta",
|
|
258
258
|
AccountUpdate = "account",
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
/**
|
|
262
|
-
* WebSocket subscription request
|
|
263
|
-
*/
|
|
264
|
-
export interface WebSocketSubscription {
|
|
265
|
-
e: WebSocketMessageType;
|
|
266
|
-
streams: string[]; // Array of streams to subscribe/unsubscribe (e.g. ["trades@BTCUSDC", "deltas@BTCUSDC"])
|
|
267
|
-
}
|
|
268
|
-
|
|
269
261
|
/**
|
|
270
262
|
* WebSocket trade update message
|
|
271
263
|
*/
|
|
@@ -300,7 +292,6 @@ export interface WebSocketAccountUpdate {
|
|
|
300
292
|
}
|
|
301
293
|
|
|
302
294
|
export type WebSocketMessage =
|
|
303
|
-
| WebSocketSubscription
|
|
304
295
|
| WebSocketTradeUpdate
|
|
305
296
|
| WebSocketDeltaUpdate
|
|
306
297
|
| WebSocketAccountUpdate;
|
|
@@ -311,3 +302,32 @@ export interface SPLTokenInfo {
|
|
|
311
302
|
tokenId: number;
|
|
312
303
|
name: string;
|
|
313
304
|
}
|
|
305
|
+
|
|
306
|
+
// Positive decimal price and size.
|
|
307
|
+
export class QuoteSize {
|
|
308
|
+
price: Decimal;
|
|
309
|
+
size: Decimal;
|
|
310
|
+
constructor(quotePrice: Decimal.Value, quoteSize: Decimal.Value) {
|
|
311
|
+
const p = new Decimal(quotePrice);
|
|
312
|
+
const s = new Decimal(quoteSize);
|
|
313
|
+
if (p.isZero() || s.isZero()) {
|
|
314
|
+
throw new Error("quotePrice and quoteSize must be non-zero");
|
|
315
|
+
}
|
|
316
|
+
this.price = p;
|
|
317
|
+
this.size = s;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
value(): Decimal {
|
|
321
|
+
return this.price.mul(this.size);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
toScaledU64(
|
|
325
|
+
marketPriceDecimals: number,
|
|
326
|
+
marketSizeDecimals: number,
|
|
327
|
+
): { price: bigint; size: bigint } {
|
|
328
|
+
return {
|
|
329
|
+
price: toScaledU64(this.price, marketPriceDecimals),
|
|
330
|
+
size: toScaledU64(this.size, marketSizeDecimals),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { bls12_381 as bls } from "@noble/curves/bls12-381";
|
|
|
4
4
|
import { secp256k1 as secp } from "@noble/curves/secp256k1";
|
|
5
5
|
import { sha256 } from "@noble/hashes/sha256";
|
|
6
6
|
import { KeyType, type Market, type Token } from "./types";
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
7
|
+
import { sizeDelimitedPeek } from "@bufbuild/protobuf/wire";
|
|
8
|
+
import { fromBinary, type Message } from "@bufbuild/protobuf";
|
|
9
|
+
import type { GenMessage } from "@bufbuild/protobuf/codegenv2";
|
|
9
10
|
import { ethers } from "ethers";
|
|
10
11
|
import fetch from "node-fetch";
|
|
11
12
|
import { RequestInfo, RequestInit, Response } from "node-fetch";
|
|
@@ -101,6 +102,7 @@ export function makeWalletSignFn(
|
|
|
101
102
|
signingKey.sign(ethers.hashMessage(message)).serialized;
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
// Returned numbers do fit into specified bits range, or error is thrown.
|
|
104
106
|
function makeToScaledBigUint(params: {
|
|
105
107
|
precision: number;
|
|
106
108
|
exponent: number;
|
|
@@ -176,63 +178,42 @@ export const toScaledU128 = makeToScaledBigUint({
|
|
|
176
178
|
exponent: 56,
|
|
177
179
|
});
|
|
178
180
|
|
|
179
|
-
/**
|
|
180
|
-
* Encodes any protobuf message into a length-delimited format,
|
|
181
|
-
* i.e. prefixed with its length encoded as varint
|
|
182
|
-
* @param message message object
|
|
183
|
-
* @param coder associated coder object which implements `MessageFns` interface
|
|
184
|
-
* @returns Encoded message as Uint8Array, prefixed with its length
|
|
185
|
-
*/
|
|
186
|
-
export function encodeLengthDelimited<T, M extends proto.MessageFns<T>>(
|
|
187
|
-
message: T,
|
|
188
|
-
coder: M,
|
|
189
|
-
): Uint8Array {
|
|
190
|
-
const encoded = coder.encode(message).finish();
|
|
191
|
-
if (encoded.byteLength > MAX_PAYLOAD_SIZE) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`Encoded message size (${encoded.byteLength} bytes) is greater than max payload size (${MAX_PAYLOAD_SIZE} bytes).`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
const encodedLength = new BinaryWriter().uint32(encoded.byteLength).finish();
|
|
197
|
-
return new Uint8Array([...encodedLength, ...encoded]);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
181
|
/**
|
|
201
182
|
* Decodes any protobuf message from a length-delimited format,
|
|
202
183
|
* i.e. prefixed with its length encoded as varint
|
|
203
184
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* ```
|
|
208
|
-
* const foo: proto.Bar = decodeLengthDelimited(bytes, proto.Bar);
|
|
209
|
-
* ```
|
|
210
|
-
*
|
|
211
|
-
* @param bytes Byte array with encoded message
|
|
212
|
-
* @param coder associated coder object which implements `MessageFns` interface
|
|
213
|
-
* @returns Decoded Action as Uint8Array.
|
|
185
|
+
* @param bytes Byte array with encoded message
|
|
186
|
+
* @param schema Message schema for decoding
|
|
187
|
+
* @returns Decoded message
|
|
214
188
|
*/
|
|
215
|
-
export function decodeLengthDelimited<T
|
|
189
|
+
export function decodeLengthDelimited<T extends Message>(
|
|
216
190
|
bytes: Uint8Array,
|
|
217
|
-
|
|
191
|
+
schema: GenMessage<T>,
|
|
218
192
|
): T {
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
193
|
+
// use sizeDelimitedPeek to extract the message length and offset
|
|
194
|
+
const peekResult = sizeDelimitedPeek(bytes);
|
|
195
|
+
|
|
196
|
+
if (peekResult.size === null || peekResult.offset === null) {
|
|
197
|
+
throw new Error("Failed to parse size-delimited message");
|
|
198
|
+
}
|
|
222
199
|
|
|
223
|
-
if (
|
|
200
|
+
if (peekResult.size > MAX_PAYLOAD_SIZE) {
|
|
224
201
|
throw new Error(
|
|
225
|
-
`Encoded message size (${
|
|
202
|
+
`Encoded message size (${peekResult.size} bytes) is greater than max payload size (${MAX_PAYLOAD_SIZE} bytes).`,
|
|
226
203
|
);
|
|
227
204
|
}
|
|
228
205
|
|
|
229
|
-
if (
|
|
206
|
+
if (peekResult.offset + peekResult.size > bytes.length) {
|
|
230
207
|
throw new Error(
|
|
231
|
-
`Encoded message size (${
|
|
208
|
+
`Encoded message size (${peekResult.size} bytes) is greater than remaining buffer size (${bytes.length - peekResult.offset} bytes).`,
|
|
232
209
|
);
|
|
233
210
|
}
|
|
234
211
|
|
|
235
|
-
|
|
212
|
+
// decode the message using the offset and size from peek
|
|
213
|
+
return fromBinary(
|
|
214
|
+
schema,
|
|
215
|
+
bytes.slice(peekResult.offset, peekResult.offset + peekResult.size),
|
|
216
|
+
);
|
|
236
217
|
}
|
|
237
218
|
|
|
238
219
|
export function checkPubKeyLength(keyType: KeyType, len: number): void {
|
|
@@ -4,8 +4,6 @@ import {
|
|
|
4
4
|
WebSocketAccountUpdate,
|
|
5
5
|
WebSocketDeltaUpdate,
|
|
6
6
|
WebSocketMessage,
|
|
7
|
-
WebSocketMessageType,
|
|
8
|
-
WebSocketSubscription,
|
|
9
7
|
WebSocketTradeUpdate,
|
|
10
8
|
} from "../types";
|
|
11
9
|
import { NordWebSocketClientEvents } from "./events";
|
|
@@ -29,9 +27,6 @@ type WebSocketInstance = WebSocket | BrowserWebSocket;
|
|
|
29
27
|
|
|
30
28
|
const VALID_STREAM_TYPES = ["trades", "delta", "account"];
|
|
31
29
|
|
|
32
|
-
// Constants for WebSocket readyState
|
|
33
|
-
const WS_OPEN = 1;
|
|
34
|
-
|
|
35
30
|
/**
|
|
36
31
|
* WebSocket client for Nord exchange
|
|
37
32
|
*
|
|
@@ -47,7 +42,6 @@ export class NordWebSocketClient
|
|
|
47
42
|
{
|
|
48
43
|
private ws: WebSocketInstance | null = null;
|
|
49
44
|
private url: string;
|
|
50
|
-
private subscriptions: Set<string> = new Set();
|
|
51
45
|
private reconnectAttempts: number = 0;
|
|
52
46
|
private maxReconnectAttempts: number = 5;
|
|
53
47
|
private reconnectDelay: number = 1000;
|
|
@@ -174,11 +168,6 @@ export class NordWebSocketClient
|
|
|
174
168
|
this.emit("connected");
|
|
175
169
|
this.reconnectAttempts = 0;
|
|
176
170
|
this.reconnectDelay = 1000;
|
|
177
|
-
|
|
178
|
-
// Resubscribe to previous subscriptions
|
|
179
|
-
if (this.subscriptions.size > 0) {
|
|
180
|
-
this.subscribe([...this.subscriptions]);
|
|
181
|
-
}
|
|
182
171
|
};
|
|
183
172
|
|
|
184
173
|
(this.ws as BrowserWebSocket).onmessage = (event: { data: any }) => {
|
|
@@ -197,18 +186,13 @@ export class NordWebSocketClient
|
|
|
197
186
|
}
|
|
198
187
|
};
|
|
199
188
|
|
|
200
|
-
(this.ws as BrowserWebSocket).onclose = (
|
|
201
|
-
const reason =
|
|
202
|
-
event && event.reason ? ` Reason: ${event.reason}` : "";
|
|
203
|
-
const code = event && event.code ? ` Code: ${event.code}` : "";
|
|
189
|
+
(this.ws as BrowserWebSocket).onclose = (_event: any) => {
|
|
204
190
|
this.emit("disconnected");
|
|
205
|
-
console.log(`WebSocket closed.${code}${reason}`);
|
|
206
191
|
this.reconnect();
|
|
207
192
|
};
|
|
208
193
|
|
|
209
194
|
(this.ws as BrowserWebSocket).onerror = (event: any) => {
|
|
210
195
|
const errorMsg = `WebSocket error: ${event && event.type ? event.type : "unknown"}`;
|
|
211
|
-
console.error(errorMsg, event);
|
|
212
196
|
this.emit("error", new Error(errorMsg));
|
|
213
197
|
};
|
|
214
198
|
} else {
|
|
@@ -221,11 +205,6 @@ export class NordWebSocketClient
|
|
|
221
205
|
this.reconnectAttempts = 0;
|
|
222
206
|
this.reconnectDelay = 1000;
|
|
223
207
|
this.setupHeartbeat();
|
|
224
|
-
|
|
225
|
-
// Resubscribe to previous subscriptions
|
|
226
|
-
if (this.subscriptions.size > 0) {
|
|
227
|
-
this.subscribe([...this.subscriptions]);
|
|
228
|
-
}
|
|
229
208
|
});
|
|
230
209
|
|
|
231
210
|
nodeWs.on("message", (data: WebSocket.Data) => {
|
|
@@ -242,9 +221,8 @@ export class NordWebSocketClient
|
|
|
242
221
|
}
|
|
243
222
|
});
|
|
244
223
|
|
|
245
|
-
nodeWs.on("close", (
|
|
224
|
+
nodeWs.on("close", (_code: number, _reason: string) => {
|
|
246
225
|
this.emit("disconnected");
|
|
247
|
-
console.log(`WebSocket closed. Code: ${code} Reason: ${reason}`);
|
|
248
226
|
if (this.pingInterval) {
|
|
249
227
|
clearInterval(this.pingInterval);
|
|
250
228
|
}
|
|
@@ -255,7 +233,6 @@ export class NordWebSocketClient
|
|
|
255
233
|
});
|
|
256
234
|
|
|
257
235
|
nodeWs.on("error", (error: Error) => {
|
|
258
|
-
console.error("WebSocket error:", error);
|
|
259
236
|
this.emit("error", error);
|
|
260
237
|
});
|
|
261
238
|
|
|
@@ -267,105 +244,10 @@ export class NordWebSocketClient
|
|
|
267
244
|
}
|
|
268
245
|
} catch (error) {
|
|
269
246
|
const errorMsg = `Failed to initialize WebSocket: ${error instanceof Error ? error.message : String(error)}`;
|
|
270
|
-
console.error(errorMsg);
|
|
271
247
|
this.emit("error", new Error(errorMsg));
|
|
272
248
|
}
|
|
273
249
|
}
|
|
274
250
|
|
|
275
|
-
/**
|
|
276
|
-
* Subscribe to one or more streams
|
|
277
|
-
* @param streams Array of streams to subscribe to (e.g. ["trades@BTCUSDC", "deltas@BTCUSDC"])
|
|
278
|
-
*/
|
|
279
|
-
public subscribe(streams: string[]): void {
|
|
280
|
-
// Validate all streams first
|
|
281
|
-
try {
|
|
282
|
-
streams.forEach((stream) => this.validateStream(stream));
|
|
283
|
-
} catch (error) {
|
|
284
|
-
this.emit(
|
|
285
|
-
"error",
|
|
286
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
287
|
-
);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
!this.ws ||
|
|
293
|
-
(this.isBrowser
|
|
294
|
-
? (this.ws as BrowserWebSocket).readyState !== WS_OPEN
|
|
295
|
-
: (this.ws as WebSocket).readyState !== WebSocket.OPEN)
|
|
296
|
-
) {
|
|
297
|
-
streams.forEach((stream) => this.subscriptions.add(stream));
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const message: WebSocketSubscription = {
|
|
302
|
-
e: WebSocketMessageType.Subscribe,
|
|
303
|
-
streams,
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const messageStr = JSON.stringify(message);
|
|
308
|
-
if (this.isBrowser) {
|
|
309
|
-
(this.ws as BrowserWebSocket).send(messageStr);
|
|
310
|
-
} else {
|
|
311
|
-
(this.ws as WebSocket).send(messageStr);
|
|
312
|
-
}
|
|
313
|
-
streams.forEach((stream) => this.subscriptions.add(stream));
|
|
314
|
-
} catch (error) {
|
|
315
|
-
this.emit(
|
|
316
|
-
"error",
|
|
317
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Unsubscribe from one or more streams
|
|
324
|
-
* @param streams Array of streams to unsubscribe from
|
|
325
|
-
*/
|
|
326
|
-
public unsubscribe(streams: string[]): void {
|
|
327
|
-
// Validate all streams first
|
|
328
|
-
try {
|
|
329
|
-
streams.forEach((stream) => this.validateStream(stream));
|
|
330
|
-
} catch (error) {
|
|
331
|
-
this.emit(
|
|
332
|
-
"error",
|
|
333
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
334
|
-
);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
!this.ws ||
|
|
340
|
-
(this.isBrowser
|
|
341
|
-
? (this.ws as BrowserWebSocket).readyState !== WS_OPEN
|
|
342
|
-
: (this.ws as WebSocket).readyState !== WebSocket.OPEN)
|
|
343
|
-
) {
|
|
344
|
-
streams.forEach((stream) => this.subscriptions.delete(stream));
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const message: WebSocketSubscription = {
|
|
349
|
-
e: WebSocketMessageType.Unsubscribe,
|
|
350
|
-
streams,
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
const messageStr = JSON.stringify(message);
|
|
355
|
-
if (this.isBrowser) {
|
|
356
|
-
(this.ws as BrowserWebSocket).send(messageStr);
|
|
357
|
-
} else {
|
|
358
|
-
(this.ws as WebSocket).send(messageStr);
|
|
359
|
-
}
|
|
360
|
-
streams.forEach((stream) => this.subscriptions.delete(stream));
|
|
361
|
-
} catch (error) {
|
|
362
|
-
this.emit(
|
|
363
|
-
"error",
|
|
364
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
251
|
/**
|
|
370
252
|
* Close the WebSocket connection
|
|
371
253
|
*/
|
|
@@ -386,7 +268,6 @@ export class NordWebSocketClient
|
|
|
386
268
|
clearTimeout(this.pingTimeout);
|
|
387
269
|
this.pingTimeout = null;
|
|
388
270
|
}
|
|
389
|
-
this.subscriptions.clear();
|
|
390
271
|
}
|
|
391
272
|
|
|
392
273
|
/**
|