blink-trade-sdk 0.1.0

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/ws.d.ts ADDED
@@ -0,0 +1,328 @@
1
+ /**
2
+ * WebSocket protocol v1 (`/v1/ws`): frames, per-channel continuity, the
3
+ * client, and local state appliers. See `docs/v1/websocket.md`.
4
+ *
5
+ * The client subscribes by channel string; every server push is an `update`
6
+ * frame whose data fields are merged into the frame; stateful channels carry
7
+ * `previous_sequence` / `sequence`. `WsSession` is the transport-free state
8
+ * machine that tracks continuity per channel and turns a sequence gap or a
9
+ * server-closed subscription into a "resubscribe required" signal;
10
+ * `WsClient` drives it over a socket.
11
+ */
12
+ import type { AccountCatalog, AccountCatalogEntry, AccountSnapshot, Balance, Bbo, BookLevels, Candle, CollectionOperation, Delegation, Fill, FundingPayment, FundingRate, Leverage, MarkCandle, Market, MarketStats, MarketType, Order, Position, PriceLevel, Resolution, Subaccount, WsTrade } from './types.js';
13
+ export { WS_PROTOCOL_VERSION } from './ws-transport.js';
14
+ export interface HelloFrame {
15
+ type: 'hello';
16
+ protocol_version: number;
17
+ /** The server closes a connection that sends nothing for this long. */
18
+ keepalive_timeout_ms: number;
19
+ }
20
+ export interface PongFrame {
21
+ type: 'pong';
22
+ timestamp_ns: string;
23
+ }
24
+ export interface UnsubscribedFrame {
25
+ type: 'unsubscribed';
26
+ channel: string;
27
+ timestamp_ns: string;
28
+ }
29
+ /** `400` invalid command or channel, `404` not found, `409` already subscribed, `503` stream unavailable. */
30
+ export type WsErrorCode = 400 | 404 | 409 | 503;
31
+ export interface ErrorFrame {
32
+ type: 'error';
33
+ /** Set when the error concerns one subscription (a rejected subscribe, or a stream the server closed). */
34
+ channel?: string;
35
+ code: WsErrorCode | number;
36
+ message: string;
37
+ timestamp_ns: string;
38
+ }
39
+ /**
40
+ * An `update` frame as received. Data fields are merged into the frame;
41
+ * `classifyUpdate` narrows them by channel. `previous_sequence` is `null` on
42
+ * the first frame after subscribing (a snapshot) and absent on channels
43
+ * without sequences (`bbo`, candles), whose every frame is complete.
44
+ */
45
+ export interface UpdateFrame {
46
+ type: 'update';
47
+ channel: string;
48
+ timestamp_ns: string;
49
+ previous_sequence?: string | null;
50
+ sequence?: string | null;
51
+ [field: string]: unknown;
52
+ }
53
+ export type WsMessage = HelloFrame | PongFrame | UpdateFrame | UnsubscribedFrame | ErrorFrame;
54
+ interface Envelope {
55
+ type: 'update';
56
+ channel: string;
57
+ timestamp_ns: string;
58
+ }
59
+ interface SequencedEnvelope extends Envelope {
60
+ previous_sequence: string | null;
61
+ sequence: string | null;
62
+ }
63
+ export interface MarketsUpdateFrame extends SequencedEnvelope {
64
+ markets: Market[];
65
+ }
66
+ export interface MarketStatsUpdateFrame extends SequencedEnvelope {
67
+ market_stats: MarketStats[];
68
+ }
69
+ export interface BboUpdateFrame extends Envelope {
70
+ bbo: Bbo;
71
+ }
72
+ export interface OrderbookUpdateFrame extends SequencedEnvelope {
73
+ orderbook: BookLevels;
74
+ }
75
+ export interface TradesUpdateFrame extends SequencedEnvelope {
76
+ trades: WsTrade[];
77
+ }
78
+ export interface CandlesUpdateFrame extends Envelope {
79
+ candles: Candle[];
80
+ }
81
+ export interface MarkCandlesUpdateFrame extends Envelope {
82
+ candles: MarkCandle[];
83
+ }
84
+ export interface FundingRatesUpdateFrame extends SequencedEnvelope {
85
+ funding_rates: FundingRate[];
86
+ }
87
+ export interface AccountsSnapshotFrame extends SequencedEnvelope, AccountCatalog {
88
+ previous_sequence: null;
89
+ }
90
+ export interface AccountsDeltaFrame extends SequencedEnvelope {
91
+ operations: CollectionOperation[];
92
+ }
93
+ export interface AccountSnapshotFrame extends SequencedEnvelope, AccountSnapshot {
94
+ previous_sequence: null;
95
+ }
96
+ export interface AccountDeltaFrame extends SequencedEnvelope {
97
+ operations: CollectionOperation[];
98
+ }
99
+ /** An update narrowed by its channel. */
100
+ export type ChannelUpdate = {
101
+ kind: 'markets';
102
+ frame: MarketsUpdateFrame;
103
+ } | {
104
+ kind: 'market-stats';
105
+ frame: MarketStatsUpdateFrame;
106
+ } | {
107
+ kind: 'bbo';
108
+ frame: BboUpdateFrame;
109
+ } | {
110
+ kind: 'orderbook';
111
+ frame: OrderbookUpdateFrame;
112
+ } | {
113
+ kind: 'trades';
114
+ frame: TradesUpdateFrame;
115
+ } | {
116
+ kind: 'candles';
117
+ frame: CandlesUpdateFrame;
118
+ } | {
119
+ kind: 'mark-candles';
120
+ frame: MarkCandlesUpdateFrame;
121
+ } | {
122
+ kind: 'funding-rates';
123
+ frame: FundingRatesUpdateFrame;
124
+ } | {
125
+ kind: 'accounts';
126
+ frame: AccountsSnapshotFrame | AccountsDeltaFrame;
127
+ } | {
128
+ kind: 'account';
129
+ frame: AccountSnapshotFrame | AccountDeltaFrame;
130
+ } | {
131
+ kind: 'unknown';
132
+ frame: UpdateFrame;
133
+ };
134
+ /** `true` when the frame replaces local state: the first frame after subscribing, or any frame on a channel without sequences. */
135
+ export declare function isSnapshot(frame: {
136
+ previous_sequence?: string | null;
137
+ }): boolean;
138
+ /** Narrow an account-channel frame: the snapshot carries the collections, later frames carry `operations`. */
139
+ export declare function isAccountSnapshot(frame: AccountSnapshotFrame | AccountDeltaFrame): frame is AccountSnapshotFrame;
140
+ export declare function isAccountsSnapshot(frame: AccountsSnapshotFrame | AccountsDeltaFrame): frame is AccountsSnapshotFrame;
141
+ /** Narrow an `update` frame by its channel name. */
142
+ export declare function classifyUpdate(frame: UpdateFrame): ChannelUpdate;
143
+ export type ParsedChannel = {
144
+ kind: 'markets';
145
+ } | {
146
+ kind: 'market-stats';
147
+ marketType: MarketType;
148
+ marketId?: number;
149
+ } | {
150
+ kind: 'bbo';
151
+ marketType: MarketType;
152
+ marketId: number;
153
+ } | {
154
+ kind: 'orderbook';
155
+ marketType: MarketType;
156
+ marketId: number;
157
+ } | {
158
+ kind: 'trades';
159
+ marketType: MarketType;
160
+ marketId: number;
161
+ } | {
162
+ kind: 'candles';
163
+ marketType: MarketType;
164
+ marketId: number;
165
+ resolution: string;
166
+ } | {
167
+ kind: 'mark-candles';
168
+ marketId: number;
169
+ resolution: string;
170
+ } | {
171
+ kind: 'funding-rates';
172
+ marketId: number;
173
+ } | {
174
+ kind: 'accounts';
175
+ address: string;
176
+ } | {
177
+ kind: 'account';
178
+ address: string;
179
+ subaccountId: number;
180
+ };
181
+ /** Channel-name builders. */
182
+ export declare const channels: {
183
+ markets: () => string;
184
+ marketStats: (marketType: MarketType, marketId?: number) => string;
185
+ bbo: (marketType: MarketType, marketId: number) => string;
186
+ orderbook: (marketType: MarketType, marketId: number) => string;
187
+ trades: (marketType: MarketType, marketId: number) => string;
188
+ candles: (marketType: MarketType, marketId: number, resolution: Resolution) => string;
189
+ markCandles: (marketId: number, resolution: Resolution) => string;
190
+ fundingRates: (marketId: number) => string;
191
+ accounts: (address: string) => string;
192
+ account: (address: string, subaccountId: number) => string;
193
+ };
194
+ /** Parse a channel name; `undefined` for names this SDK does not know. */
195
+ export declare function parseChannel(channel: string): ParsedChannel | undefined;
196
+ export type ResubscribeReason =
197
+ /** An update's `previous_sequence` did not match the last installed `sequence`; the frame was dropped. */
198
+ {
199
+ kind: 'sequence_gap';
200
+ expected: string | null;
201
+ previous_sequence: string | null;
202
+ sequence: string | null;
203
+ }
204
+ /** The server closed the subscription (an `error` frame naming the channel, e.g. `503 stream unavailable`). */
205
+ | {
206
+ kind: 'subscription_closed';
207
+ code: number;
208
+ message: string;
209
+ };
210
+ /** Local state for `channel` is stale: discard it and subscribe again (`WsClient.resubscribe`). */
211
+ export interface ResubscribeRequired {
212
+ channel: string;
213
+ reason: ResubscribeReason;
214
+ }
215
+ export type SessionEvent = {
216
+ type: 'message';
217
+ message: WsMessage;
218
+ } | {
219
+ type: 'resubscribe_required';
220
+ channel: string;
221
+ reason: ResubscribeReason;
222
+ };
223
+ /**
224
+ * Transport-free protocol state: subscribed channels, the last installed
225
+ * sequence per channel, and continuity checks. Feed it every server frame
226
+ * with `ingest`.
227
+ */
228
+ export declare class WsSession {
229
+ private readonly state;
230
+ /** Record a `subscribe` command. */
231
+ subscribe(channel: string): void;
232
+ /** Record an `unsubscribe` command; the channel stays tracked until the server's `unsubscribed` frame. */
233
+ unsubscribe(channel: string): void;
234
+ /** Channels currently subscribed. */
235
+ channels(): string[];
236
+ /** Last installed sequence; `undefined` when nothing is installed, `null` when the channel has no sequence yet. */
237
+ lastSequence(channel: string): string | null | undefined;
238
+ isInstalled(channel: string): boolean;
239
+ /** Decode one server frame and update continuity. */
240
+ ingest(message: WsMessage): SessionEvent;
241
+ }
242
+ export interface WsClientOptions {
243
+ /** Every server frame: `hello`, `pong`, `update`, `unsubscribed`, `error`. Updates that fail the continuity check are not delivered. */
244
+ onMessage: (message: WsMessage) => void;
245
+ /** A channel's local state is stale; call `resubscribe(channel)` and reinstall the snapshot that follows. */
246
+ onResubscribeRequired?: (signal: ResubscribeRequired) => void;
247
+ onClose?: (event: CloseEvent) => void;
248
+ onError?: (error: unknown) => void;
249
+ /**
250
+ * Ping period; defaults to half the server's `keepalive_timeout_ms`.
251
+ * Uses transport pings when available and protocol-v1 JSON pings in browsers.
252
+ */
253
+ keepaliveIntervalMs?: number;
254
+ /** WebSocket implementation; defaults to `globalThis.WebSocket`. */
255
+ WebSocket?: typeof WebSocket;
256
+ }
257
+ export declare class WsClient {
258
+ private readonly transport;
259
+ private readonly session;
260
+ constructor(url: string, options: WsClientOptions);
261
+ /** Resolves with the server's `hello` once the connection is usable. */
262
+ ready(): Promise<HelloFrame>;
263
+ hello(): HelloFrame | undefined;
264
+ /** Channels currently subscribed. */
265
+ channels(): string[];
266
+ /** Last installed sequence for a channel (see `WsSession.lastSequence`). */
267
+ lastSequence(channel: string): string | null | undefined;
268
+ isInstalled(channel: string): boolean;
269
+ /** Subscribe to a channel (`channels.orderbook('perp', 1)`); the first `update` is the snapshot. A duplicate subscribe yields a `409` error frame. */
270
+ subscribe(channel: string): Promise<void>;
271
+ /** Unsubscribe; the server acknowledges with an `unsubscribed` frame. */
272
+ unsubscribe(channel: string): Promise<void>;
273
+ /** Recover from `onResubscribeRequired`: unsubscribe (a no-op if the server already closed the channel) and subscribe again. */
274
+ resubscribe(channel: string): Promise<void>;
275
+ close(): void;
276
+ }
277
+ /** A price-level order book built from `orderbook/...` updates. */
278
+ export declare class LocalBook {
279
+ private readonly bidLevels;
280
+ private readonly askLevels;
281
+ sequence: string | null;
282
+ /** Apply an update: a snapshot replaces the book, a delta sets absolute sizes (size `0` removes the level). */
283
+ apply(frame: OrderbookUpdateFrame): void;
284
+ applyLevels(snapshot: boolean, levels: BookLevels): void;
285
+ /** Bids, best (highest price) first. */
286
+ bids(): PriceLevel[];
287
+ /** Asks, best (lowest price) first. */
288
+ asks(): PriceLevel[];
289
+ bestBid(): PriceLevel | undefined;
290
+ bestAsk(): PriceLevel | undefined;
291
+ isEmpty(): boolean;
292
+ }
293
+ /**
294
+ * Apply one operation to a generic collection. Append-only collections are
295
+ * kept newest first (like the snapshot): an `insert` goes to the front, and a
296
+ * row whose key is already present (a replayed transaction) is dropped.
297
+ */
298
+ export declare function applyOperation<T extends Record<string, unknown>>(rows: T[], operation: CollectionOperation): T[];
299
+ /**
300
+ * Typed state of one subaccount, maintained from the
301
+ * `account/{address}/{subaccount_id}` channel: the snapshot fills every
302
+ * collection, later frames carry `operations` applied in order with each
303
+ * collection's documented key. `fills` and `funding_payments` are kept newest
304
+ * first and grow with every insert; `truncateHistory` bounds them. Operations
305
+ * on unknown collections are ignored.
306
+ */
307
+ export declare class AccountState {
308
+ account?: Subaccount;
309
+ delegations: Delegation[];
310
+ balances: Balance[];
311
+ positions: Position[];
312
+ leverages: Leverage[];
313
+ orders: Order[];
314
+ fills: Fill[];
315
+ funding_payments: FundingPayment[];
316
+ apply(frame: AccountSnapshotFrame | AccountDeltaFrame): void;
317
+ install(snapshot: AccountSnapshot): void;
318
+ applyOperation(operation: CollectionOperation): void;
319
+ /** Keep only the `maxRows` most recent `fills` and `funding_payments`. */
320
+ truncateHistory(maxRows: number): void;
321
+ }
322
+ /** The `accounts/{address}` catalog, maintained from its snapshot and `reset` operations. */
323
+ export declare class AccountCatalogState {
324
+ subaccounts: AccountCatalogEntry[];
325
+ delegations: Delegation[];
326
+ apply(frame: AccountsSnapshotFrame | AccountsDeltaFrame): void;
327
+ applyOperation(operation: CollectionOperation): void;
328
+ }