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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +130 -0
- package/dist/managed-ws.d.ts +135 -0
- package/dist/managed-ws.js +475 -0
- package/dist/types.d.ts +288 -0
- package/dist/types.js +2 -0
- package/dist/ws-transport.d.ts +50 -0
- package/dist/ws-transport.js +118 -0
- package/dist/ws.d.ts +328 -0
- package/dist/ws.js +482 -0
- package/package.json +35 -0
package/dist/ws.js
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
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 { WsTransport } from './ws-transport.js';
|
|
13
|
+
export { WS_PROTOCOL_VERSION } from './ws-transport.js';
|
|
14
|
+
/** `true` when the frame replaces local state: the first frame after subscribing, or any frame on a channel without sequences. */
|
|
15
|
+
export function isSnapshot(frame) {
|
|
16
|
+
return frame.previous_sequence == null;
|
|
17
|
+
}
|
|
18
|
+
/** Narrow an account-channel frame: the snapshot carries the collections, later frames carry `operations`. */
|
|
19
|
+
export function isAccountSnapshot(frame) {
|
|
20
|
+
return !('operations' in frame);
|
|
21
|
+
}
|
|
22
|
+
export function isAccountsSnapshot(frame) {
|
|
23
|
+
return !('operations' in frame);
|
|
24
|
+
}
|
|
25
|
+
/** Narrow an `update` frame by its channel name. */
|
|
26
|
+
export function classifyUpdate(frame) {
|
|
27
|
+
const channel = parseChannel(frame.channel);
|
|
28
|
+
switch (channel?.kind) {
|
|
29
|
+
case 'markets':
|
|
30
|
+
return { kind: 'markets', frame: frame };
|
|
31
|
+
case 'market-stats':
|
|
32
|
+
return { kind: 'market-stats', frame: frame };
|
|
33
|
+
case 'bbo':
|
|
34
|
+
return { kind: 'bbo', frame: frame };
|
|
35
|
+
case 'orderbook':
|
|
36
|
+
return { kind: 'orderbook', frame: frame };
|
|
37
|
+
case 'trades':
|
|
38
|
+
return { kind: 'trades', frame: frame };
|
|
39
|
+
case 'candles':
|
|
40
|
+
return { kind: 'candles', frame: frame };
|
|
41
|
+
case 'mark-candles':
|
|
42
|
+
return { kind: 'mark-candles', frame: frame };
|
|
43
|
+
case 'funding-rates':
|
|
44
|
+
return { kind: 'funding-rates', frame: frame };
|
|
45
|
+
case 'accounts':
|
|
46
|
+
return { kind: 'accounts', frame: frame };
|
|
47
|
+
case 'account':
|
|
48
|
+
return { kind: 'account', frame: frame };
|
|
49
|
+
default:
|
|
50
|
+
return { kind: 'unknown', frame };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Channel-name builders. */
|
|
54
|
+
export const channels = {
|
|
55
|
+
markets: () => 'markets',
|
|
56
|
+
marketStats: (marketType, marketId) => marketId === undefined ? `market-stats/${marketType}` : `market-stats/${marketType}/${marketId}`,
|
|
57
|
+
bbo: (marketType, marketId) => `bbo/${marketType}/${marketId}`,
|
|
58
|
+
orderbook: (marketType, marketId) => `orderbook/${marketType}/${marketId}`,
|
|
59
|
+
trades: (marketType, marketId) => `trades/${marketType}/${marketId}`,
|
|
60
|
+
candles: (marketType, marketId, resolution) => `candles/${marketType}/${marketId}/${resolution}`,
|
|
61
|
+
markCandles: (marketId, resolution) => `mark-candles/${marketId}/${resolution}`,
|
|
62
|
+
fundingRates: (marketId) => `funding-rates/${marketId}`,
|
|
63
|
+
accounts: (address) => `accounts/${address}`,
|
|
64
|
+
account: (address, subaccountId) => `account/${address}/${subaccountId}`,
|
|
65
|
+
};
|
|
66
|
+
/** Parse a channel name; `undefined` for names this SDK does not know. */
|
|
67
|
+
export function parseChannel(channel) {
|
|
68
|
+
const parts = channel.split('/');
|
|
69
|
+
const marketType = (part) => part === 'perp' || part === 'spot' ? part : undefined;
|
|
70
|
+
const id = (part) => part !== undefined && /^[0-9]+$/.test(part) ? Number(part) : undefined;
|
|
71
|
+
const [head, a, b, c] = parts;
|
|
72
|
+
switch (head) {
|
|
73
|
+
case 'markets':
|
|
74
|
+
return parts.length === 1 ? { kind: 'markets' } : undefined;
|
|
75
|
+
case 'market-stats': {
|
|
76
|
+
const kind = marketType(a);
|
|
77
|
+
if (!kind)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (parts.length === 2)
|
|
80
|
+
return { kind: 'market-stats', marketType: kind };
|
|
81
|
+
const marketId = id(b);
|
|
82
|
+
return parts.length === 3 && marketId !== undefined ? { kind: 'market-stats', marketType: kind, marketId } : undefined;
|
|
83
|
+
}
|
|
84
|
+
case 'bbo':
|
|
85
|
+
case 'orderbook':
|
|
86
|
+
case 'trades': {
|
|
87
|
+
const kind = marketType(a);
|
|
88
|
+
const marketId = id(b);
|
|
89
|
+
return parts.length === 3 && kind && marketId !== undefined ? { kind: head, marketType: kind, marketId } : undefined;
|
|
90
|
+
}
|
|
91
|
+
case 'candles': {
|
|
92
|
+
const kind = marketType(a);
|
|
93
|
+
const marketId = id(b);
|
|
94
|
+
return parts.length === 4 && kind && marketId !== undefined && c
|
|
95
|
+
? { kind: 'candles', marketType: kind, marketId, resolution: c }
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
case 'mark-candles': {
|
|
99
|
+
const marketId = id(a);
|
|
100
|
+
return parts.length === 3 && marketId !== undefined && b ? { kind: 'mark-candles', marketId, resolution: b } : undefined;
|
|
101
|
+
}
|
|
102
|
+
case 'funding-rates': {
|
|
103
|
+
const marketId = id(a);
|
|
104
|
+
return parts.length === 2 && marketId !== undefined ? { kind: 'funding-rates', marketId } : undefined;
|
|
105
|
+
}
|
|
106
|
+
case 'accounts':
|
|
107
|
+
return parts.length === 2 && a ? { kind: 'accounts', address: a } : undefined;
|
|
108
|
+
case 'account': {
|
|
109
|
+
const subaccountId = id(b);
|
|
110
|
+
return parts.length === 3 && a && subaccountId !== undefined ? { kind: 'account', address: a, subaccountId } : undefined;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Transport-free protocol state: subscribed channels, the last installed
|
|
118
|
+
* sequence per channel, and continuity checks. Feed it every server frame
|
|
119
|
+
* with `ingest`.
|
|
120
|
+
*/
|
|
121
|
+
export class WsSession {
|
|
122
|
+
state = new Map();
|
|
123
|
+
/** Record a `subscribe` command. */
|
|
124
|
+
subscribe(channel) {
|
|
125
|
+
const state = this.state.get(channel);
|
|
126
|
+
if (state)
|
|
127
|
+
state.subscribed = true;
|
|
128
|
+
else
|
|
129
|
+
this.state.set(channel, { subscribed: true, pendingUnsubscribes: 0, installed: false, lastSequence: null });
|
|
130
|
+
}
|
|
131
|
+
/** Record an `unsubscribe` command; the channel stays tracked until the server's `unsubscribed` frame. */
|
|
132
|
+
unsubscribe(channel) {
|
|
133
|
+
const state = this.state.get(channel);
|
|
134
|
+
if (!state)
|
|
135
|
+
return;
|
|
136
|
+
state.subscribed = false;
|
|
137
|
+
state.pendingUnsubscribes += 1;
|
|
138
|
+
state.installed = false;
|
|
139
|
+
state.lastSequence = null;
|
|
140
|
+
}
|
|
141
|
+
/** Channels currently subscribed. */
|
|
142
|
+
channels() {
|
|
143
|
+
return [...this.state.entries()].filter(([, state]) => state.subscribed).map(([channel]) => channel);
|
|
144
|
+
}
|
|
145
|
+
/** Last installed sequence; `undefined` when nothing is installed, `null` when the channel has no sequence yet. */
|
|
146
|
+
lastSequence(channel) {
|
|
147
|
+
const state = this.state.get(channel);
|
|
148
|
+
return state?.installed ? state.lastSequence : undefined;
|
|
149
|
+
}
|
|
150
|
+
isInstalled(channel) {
|
|
151
|
+
return this.state.get(channel)?.installed ?? false;
|
|
152
|
+
}
|
|
153
|
+
/** Decode one server frame and update continuity. */
|
|
154
|
+
ingest(message) {
|
|
155
|
+
switch (message.type) {
|
|
156
|
+
case 'update': {
|
|
157
|
+
const state = this.state.get(message.channel);
|
|
158
|
+
const stateful = 'previous_sequence' in message;
|
|
159
|
+
if (!state)
|
|
160
|
+
return { type: 'message', message };
|
|
161
|
+
if (!stateful) {
|
|
162
|
+
state.installed = true;
|
|
163
|
+
return { type: 'message', message };
|
|
164
|
+
}
|
|
165
|
+
const previous = message.previous_sequence ?? null;
|
|
166
|
+
const sequence = message.sequence ?? null;
|
|
167
|
+
if (!state.installed && previous === null) {
|
|
168
|
+
state.installed = true;
|
|
169
|
+
state.lastSequence = sequence;
|
|
170
|
+
return { type: 'message', message };
|
|
171
|
+
}
|
|
172
|
+
if (state.installed && state.lastSequence === previous) {
|
|
173
|
+
state.lastSequence = sequence;
|
|
174
|
+
return { type: 'message', message };
|
|
175
|
+
}
|
|
176
|
+
const expected = state.installed ? state.lastSequence : null;
|
|
177
|
+
state.installed = false;
|
|
178
|
+
state.lastSequence = null;
|
|
179
|
+
return {
|
|
180
|
+
type: 'resubscribe_required',
|
|
181
|
+
channel: message.channel,
|
|
182
|
+
reason: { kind: 'sequence_gap', expected, previous_sequence: previous, sequence },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
case 'unsubscribed': {
|
|
186
|
+
const state = this.state.get(message.channel);
|
|
187
|
+
if (state) {
|
|
188
|
+
state.pendingUnsubscribes = Math.max(0, state.pendingUnsubscribes - 1);
|
|
189
|
+
if (!state.subscribed)
|
|
190
|
+
this.state.delete(message.channel);
|
|
191
|
+
}
|
|
192
|
+
return { type: 'message', message };
|
|
193
|
+
}
|
|
194
|
+
case 'error': {
|
|
195
|
+
// 409 means the subscription already exists and stays live; every other channel error ends it.
|
|
196
|
+
if (message.channel !== undefined && message.code !== 409) {
|
|
197
|
+
const state = this.state.get(message.channel);
|
|
198
|
+
if (state) {
|
|
199
|
+
this.state.delete(message.channel);
|
|
200
|
+
if (state.installed) {
|
|
201
|
+
return {
|
|
202
|
+
type: 'resubscribe_required',
|
|
203
|
+
channel: message.channel,
|
|
204
|
+
reason: { kind: 'subscription_closed', code: message.code, message: message.message },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { type: 'message', message };
|
|
210
|
+
}
|
|
211
|
+
default:
|
|
212
|
+
return { type: 'message', message };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export class WsClient {
|
|
217
|
+
transport;
|
|
218
|
+
session = new WsSession();
|
|
219
|
+
constructor(url, options) {
|
|
220
|
+
const Socket = options.WebSocket;
|
|
221
|
+
this.transport = new WsTransport(url, {
|
|
222
|
+
socketFactory: Socket ? url => new Socket(url) : undefined,
|
|
223
|
+
keepaliveIntervalMs: options.keepaliveIntervalMs,
|
|
224
|
+
onMessage: message => {
|
|
225
|
+
const event = this.session.ingest(message);
|
|
226
|
+
if (event.type === 'message')
|
|
227
|
+
options.onMessage(event.message);
|
|
228
|
+
else
|
|
229
|
+
options.onResubscribeRequired?.({ channel: event.channel, reason: event.reason });
|
|
230
|
+
},
|
|
231
|
+
onClose: event => options.onClose?.(event),
|
|
232
|
+
onError: options.onError,
|
|
233
|
+
onProtocolError: options.onError,
|
|
234
|
+
onDiagnostic: (_code, message, detail) => options.onError?.(detail instanceof Error ? detail : new Error(message)),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/** Resolves with the server's `hello` once the connection is usable. */
|
|
238
|
+
ready() {
|
|
239
|
+
return this.transport.ready();
|
|
240
|
+
}
|
|
241
|
+
hello() {
|
|
242
|
+
return this.transport.hello();
|
|
243
|
+
}
|
|
244
|
+
/** Channels currently subscribed. */
|
|
245
|
+
channels() {
|
|
246
|
+
return this.session.channels();
|
|
247
|
+
}
|
|
248
|
+
/** Last installed sequence for a channel (see `WsSession.lastSequence`). */
|
|
249
|
+
lastSequence(channel) {
|
|
250
|
+
return this.session.lastSequence(channel);
|
|
251
|
+
}
|
|
252
|
+
isInstalled(channel) {
|
|
253
|
+
return this.session.isInstalled(channel);
|
|
254
|
+
}
|
|
255
|
+
/** Subscribe to a channel (`channels.orderbook('perp', 1)`); the first `update` is the snapshot. A duplicate subscribe yields a `409` error frame. */
|
|
256
|
+
async subscribe(channel) {
|
|
257
|
+
await this.transport.ready();
|
|
258
|
+
this.transport.send({ type: 'subscribe', channel });
|
|
259
|
+
this.session.subscribe(channel);
|
|
260
|
+
}
|
|
261
|
+
/** Unsubscribe; the server acknowledges with an `unsubscribed` frame. */
|
|
262
|
+
async unsubscribe(channel) {
|
|
263
|
+
await this.transport.ready();
|
|
264
|
+
this.transport.send({ type: 'unsubscribe', channel });
|
|
265
|
+
this.session.unsubscribe(channel);
|
|
266
|
+
}
|
|
267
|
+
/** Recover from `onResubscribeRequired`: unsubscribe (a no-op if the server already closed the channel) and subscribe again. */
|
|
268
|
+
async resubscribe(channel) {
|
|
269
|
+
await this.unsubscribe(channel);
|
|
270
|
+
await this.subscribe(channel);
|
|
271
|
+
}
|
|
272
|
+
close() {
|
|
273
|
+
this.transport.close();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Local state
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
/** A price-level order book built from `orderbook/...` updates. */
|
|
280
|
+
export class LocalBook {
|
|
281
|
+
bidLevels = new Map();
|
|
282
|
+
askLevels = new Map();
|
|
283
|
+
sequence = null;
|
|
284
|
+
/** Apply an update: a snapshot replaces the book, a delta sets absolute sizes (size `0` removes the level). */
|
|
285
|
+
apply(frame) {
|
|
286
|
+
this.applyLevels(isSnapshot(frame), frame.orderbook);
|
|
287
|
+
this.sequence = frame.sequence ?? null;
|
|
288
|
+
}
|
|
289
|
+
applyLevels(snapshot, levels) {
|
|
290
|
+
if (snapshot) {
|
|
291
|
+
this.bidLevels.clear();
|
|
292
|
+
this.askLevels.clear();
|
|
293
|
+
}
|
|
294
|
+
for (const [side, updates] of [
|
|
295
|
+
[this.bidLevels, levels.bids],
|
|
296
|
+
[this.askLevels, levels.asks],
|
|
297
|
+
]) {
|
|
298
|
+
for (const level of updates) {
|
|
299
|
+
if (BigInt(level.size_base_lots) === 0n)
|
|
300
|
+
side.delete(level.price_ticks);
|
|
301
|
+
else
|
|
302
|
+
side.set(level.price_ticks, level.size_base_lots);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Bids, best (highest price) first. */
|
|
307
|
+
bids() {
|
|
308
|
+
return sortedLevels(this.bidLevels, (a, b) => (a > b ? -1 : a < b ? 1 : 0));
|
|
309
|
+
}
|
|
310
|
+
/** Asks, best (lowest price) first. */
|
|
311
|
+
asks() {
|
|
312
|
+
return sortedLevels(this.askLevels, (a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
313
|
+
}
|
|
314
|
+
bestBid() {
|
|
315
|
+
return this.bids()[0];
|
|
316
|
+
}
|
|
317
|
+
bestAsk() {
|
|
318
|
+
return this.asks()[0];
|
|
319
|
+
}
|
|
320
|
+
isEmpty() {
|
|
321
|
+
return this.bidLevels.size === 0 && this.askLevels.size === 0;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function sortedLevels(levels, compare) {
|
|
325
|
+
return [...levels.entries()]
|
|
326
|
+
.map(([price_ticks, size_base_lots]) => ({ price_ticks, size_base_lots, price: BigInt(price_ticks) }))
|
|
327
|
+
.sort((a, b) => compare(a.price, b.price))
|
|
328
|
+
.map(({ price_ticks, size_base_lots }) => ({ price_ticks, size_base_lots }));
|
|
329
|
+
}
|
|
330
|
+
/** Documented per-collection keys for `upsert` / `delete` matching. */
|
|
331
|
+
const COLLECTION_KEYS = {
|
|
332
|
+
account: ['subaccount_id'],
|
|
333
|
+
balances: ['token_id'],
|
|
334
|
+
positions: ['market_id'],
|
|
335
|
+
leverages: ['market_id'],
|
|
336
|
+
orders: ['market_type', 'client_order_id'],
|
|
337
|
+
delegations: ['subaccount_id', 'delegate'],
|
|
338
|
+
subaccounts: ['subaccount_id'],
|
|
339
|
+
fills: ['id', 'role'],
|
|
340
|
+
funding_payments: ['id'],
|
|
341
|
+
};
|
|
342
|
+
const APPEND_ONLY = new Set(['fills', 'funding_payments']);
|
|
343
|
+
function rowMatches(collection, row, key) {
|
|
344
|
+
const fields = (COLLECTION_KEYS[collection] ?? []).filter((field) => key[field] !== undefined);
|
|
345
|
+
return fields.length > 0 && fields.every((field) => String(row[field]) === String(key[field]));
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Apply one operation to a generic collection. Append-only collections are
|
|
349
|
+
* kept newest first (like the snapshot): an `insert` goes to the front, and a
|
|
350
|
+
* row whose key is already present (a replayed transaction) is dropped.
|
|
351
|
+
*/
|
|
352
|
+
export function applyOperation(rows, operation) {
|
|
353
|
+
const { collection, op } = operation;
|
|
354
|
+
switch (op) {
|
|
355
|
+
case 'reset':
|
|
356
|
+
return (operation.rows ?? []);
|
|
357
|
+
case 'insert':
|
|
358
|
+
case 'upsert': {
|
|
359
|
+
const row = requireRow(operation);
|
|
360
|
+
if (APPEND_ONLY.has(collection)) {
|
|
361
|
+
if (rows.some((existing) => rowMatches(collection, existing, row)))
|
|
362
|
+
return rows;
|
|
363
|
+
return [row, ...rows];
|
|
364
|
+
}
|
|
365
|
+
const index = rows.findIndex((existing) => rowMatches(collection, existing, row));
|
|
366
|
+
if (index === -1)
|
|
367
|
+
return [...rows, row];
|
|
368
|
+
const next = rows.slice();
|
|
369
|
+
next[index] = row;
|
|
370
|
+
return next;
|
|
371
|
+
}
|
|
372
|
+
case 'delete': {
|
|
373
|
+
const key = requireRow(operation);
|
|
374
|
+
return rows.filter((existing) => !rowMatches(collection, existing, key));
|
|
375
|
+
}
|
|
376
|
+
default:
|
|
377
|
+
return rows;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function requireRow(operation) {
|
|
381
|
+
if (!operation.row)
|
|
382
|
+
throw new Error(`${operation.collection} ${operation.op} without row`);
|
|
383
|
+
return operation.row;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Typed state of one subaccount, maintained from the
|
|
387
|
+
* `account/{address}/{subaccount_id}` channel: the snapshot fills every
|
|
388
|
+
* collection, later frames carry `operations` applied in order with each
|
|
389
|
+
* collection's documented key. `fills` and `funding_payments` are kept newest
|
|
390
|
+
* first and grow with every insert; `truncateHistory` bounds them. Operations
|
|
391
|
+
* on unknown collections are ignored.
|
|
392
|
+
*/
|
|
393
|
+
export class AccountState {
|
|
394
|
+
account;
|
|
395
|
+
delegations = [];
|
|
396
|
+
balances = [];
|
|
397
|
+
positions = [];
|
|
398
|
+
leverages = [];
|
|
399
|
+
orders = [];
|
|
400
|
+
fills = [];
|
|
401
|
+
funding_payments = [];
|
|
402
|
+
apply(frame) {
|
|
403
|
+
if (isAccountSnapshot(frame))
|
|
404
|
+
this.install(frame);
|
|
405
|
+
else
|
|
406
|
+
for (const operation of frame.operations)
|
|
407
|
+
this.applyOperation(operation);
|
|
408
|
+
}
|
|
409
|
+
install(snapshot) {
|
|
410
|
+
this.account = snapshot.account;
|
|
411
|
+
this.delegations = snapshot.delegations ?? [];
|
|
412
|
+
this.balances = snapshot.balances ?? [];
|
|
413
|
+
this.positions = snapshot.positions ?? [];
|
|
414
|
+
this.leverages = snapshot.leverages ?? [];
|
|
415
|
+
this.orders = snapshot.orders ?? [];
|
|
416
|
+
this.fills = snapshot.fills ?? [];
|
|
417
|
+
this.funding_payments = snapshot.funding_payments ?? [];
|
|
418
|
+
}
|
|
419
|
+
applyOperation(operation) {
|
|
420
|
+
switch (operation.collection) {
|
|
421
|
+
case 'account':
|
|
422
|
+
if (operation.op === 'reset')
|
|
423
|
+
this.account = operation.rows?.[0] ?? undefined;
|
|
424
|
+
else if (operation.op === 'delete')
|
|
425
|
+
this.account = undefined;
|
|
426
|
+
else
|
|
427
|
+
this.account = requireRow(operation);
|
|
428
|
+
break;
|
|
429
|
+
case 'delegations':
|
|
430
|
+
this.delegations = applyOperation(this.delegations, operation);
|
|
431
|
+
break;
|
|
432
|
+
case 'balances':
|
|
433
|
+
this.balances = applyOperation(this.balances, operation);
|
|
434
|
+
break;
|
|
435
|
+
case 'positions':
|
|
436
|
+
this.positions = applyOperation(this.positions, operation);
|
|
437
|
+
break;
|
|
438
|
+
case 'leverages':
|
|
439
|
+
this.leverages = applyOperation(this.leverages, operation);
|
|
440
|
+
break;
|
|
441
|
+
case 'orders':
|
|
442
|
+
this.orders = applyOperation(this.orders, operation);
|
|
443
|
+
break;
|
|
444
|
+
case 'fills':
|
|
445
|
+
this.fills = applyOperation(this.fills, operation);
|
|
446
|
+
break;
|
|
447
|
+
case 'funding_payments':
|
|
448
|
+
this.funding_payments = applyOperation(this.funding_payments, operation);
|
|
449
|
+
break;
|
|
450
|
+
default:
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** Keep only the `maxRows` most recent `fills` and `funding_payments`. */
|
|
455
|
+
truncateHistory(maxRows) {
|
|
456
|
+
this.fills = this.fills.slice(0, maxRows);
|
|
457
|
+
this.funding_payments = this.funding_payments.slice(0, maxRows);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/** The `accounts/{address}` catalog, maintained from its snapshot and `reset` operations. */
|
|
461
|
+
export class AccountCatalogState {
|
|
462
|
+
subaccounts = [];
|
|
463
|
+
delegations = [];
|
|
464
|
+
apply(frame) {
|
|
465
|
+
if (isAccountsSnapshot(frame)) {
|
|
466
|
+
this.subaccounts = frame.subaccounts ?? [];
|
|
467
|
+
this.delegations = frame.delegations ?? [];
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
for (const operation of frame.operations)
|
|
471
|
+
this.applyOperation(operation);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
applyOperation(operation) {
|
|
475
|
+
if (operation.collection === 'subaccounts') {
|
|
476
|
+
this.subaccounts = applyOperation(this.subaccounts, operation);
|
|
477
|
+
}
|
|
478
|
+
else if (operation.collection === 'delegations') {
|
|
479
|
+
this.delegations = applyOperation(this.delegations, operation);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blink-trade-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Blink exchange SDK: typed REST + WebSocket client for api.blink.trade",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/alterity-systems/blink-sdk-ts.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"test": "bun test test/unit.test.ts test/managed-ws.test.ts test/ws-transport.test.ts",
|
|
18
|
+
"test:live": "bun test test/live.test.ts",
|
|
19
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
20
|
+
"prepare": "npm run build",
|
|
21
|
+
"test:local": "bun test test/local.test.ts",
|
|
22
|
+
"test:package": "npm run build && node scripts/check-package.mjs",
|
|
23
|
+
"prepublishOnly": "npm run typecheck && bun run test && npm run test:package"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.6.0",
|
|
27
|
+
"@types/bun": "^1.1.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"packageManager": "bun@1.3.14",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org/"
|
|
34
|
+
}
|
|
35
|
+
}
|