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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alterity Systems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # blink-trade-sdk
2
+
3
+ Typed client for Blink API v1 and the protocol-v1 WebSocket stream.
4
+
5
+ ```sh
6
+ npm install blink-trade-sdk
7
+ ```
8
+
9
+ ```ts
10
+ import { RestClient, WsClient, LocalBook, channels, classifyUpdate } from 'blink-trade-sdk';
11
+
12
+ const api = new RestClient('https://api.blink.trade');
13
+ const markets = await api.markets();
14
+ const book = await api.orderbook(markets[0].market_id, markets[0].market_type, 10);
15
+
16
+ const localBook = new LocalBook();
17
+ const ws = new WsClient('wss://api.blink.trade/v1/ws', {
18
+ onMessage: (message) => {
19
+ if (message.type !== 'update') return;
20
+ const update = classifyUpdate(message);
21
+ if (update.kind === 'orderbook') localBook.apply(update.frame); // snapshot first, then deltas
22
+ if (update.kind === 'trades') console.log(update.frame.trades);
23
+ },
24
+ onResubscribeRequired: ({ channel, reason }) => {
25
+ console.log(channel, reason); // discard local state; a fresh snapshot follows
26
+ void ws.resubscribe(channel);
27
+ },
28
+ });
29
+ await ws.ready();
30
+ await ws.subscribe(channels.orderbook('perp', 1));
31
+ await ws.subscribe('trades/perp/1');
32
+ ```
33
+
34
+ `WsClient` waits for the protocol-v1 `hello` (`protocol_version: 1`,
35
+ `keepalive_timeout_ms`) and rejects any other server. Subscriptions are
36
+ channel strings (`markets`, `market-stats/perp/1`, `bbo/perp/1`,
37
+ `orderbook/perp/1`, `trades/perp/1`, `candles/perp/1/1m`,
38
+ `mark-candles/1/1m`, `funding-rates/1`, `accounts/<address>`,
39
+ `account/<address>/<subaccount_id>`); `channels` builds them and
40
+ `parseChannel` reads them. The first `update` after subscribing is the
41
+ snapshot (`previous_sequence: null`). The client checks `previous_sequence`
42
+ against the last installed `sequence` per channel; a gap, or a subscription
43
+ the server closed (an `error` frame naming the channel), is reported through
44
+ `onResubscribeRequired` and the frame is dropped — call `resubscribe` and
45
+ reinstall the snapshot. Every other frame (`hello`, `update`, `unsubscribed`,
46
+ `error`) reaches `onMessage`.
47
+
48
+ `classifyUpdate` narrows an update by channel. `LocalBook` maintains a
49
+ price-level book from `orderbook/...` frames; `AccountState` applies the
50
+ `account/<address>/<subaccount_id>` snapshot and its `operations` deltas
51
+ (`reset` / `insert` / `upsert` / `delete`); `AccountCatalogState` does the same
52
+ for `accounts/<address>`.
53
+
54
+ Keepalive uses transport pings where the runtime exposes `WebSocket.ping`
55
+ (Bun, `ws`) and protocol-v1 JSON `ping`/`pong` in browsers. Both run at half
56
+ `keepalive_timeout_ms`, keeping idle subscriptions connected.
57
+
58
+ Prices are ticks, sizes are base lots, balances are token atoms, and values
59
+ that may exceed JavaScript's safe integer range are decimal strings.
60
+
61
+ See [docs.blink.trade](https://docs.blink.trade) for the REST, WebSocket, and
62
+ transaction-signing references.
63
+
64
+ Run `bun run test` and `bun run build` before publishing; `bun run test:live`
65
+ streams the market-data channels from the production gateway.
66
+
67
+ ## Managed browser subscriptions
68
+
69
+ `BlinkSocketManager` adds reconnect/backoff, reference-counted subscriptions,
70
+ late-consumer snapshots, snapshot timeouts, browser keepalive and diagnostics.
71
+ Both clients use the same physical transport (connection setup, hello validation,
72
+ JSON parsing and heartbeat) and `WsSession` sequence validation. Use one
73
+ manager per application/tab and dispose it when the application shuts down.
74
+
75
+ ```ts
76
+ import { BlinkSocketManager, LocalBook, channels, gatewayWebSocketUrl } from 'blink-trade-sdk';
77
+ const manager = new BlinkSocketManager(gatewayWebSocketUrl('http://localhost:3001'));
78
+ const book = new LocalBook();
79
+ const stop = manager.subscribe(channels.orderbook('perp', 1), frame => {
80
+ if (frame.type === 'update') {
81
+ // frame.snapshot is explicit: null sequence does not imply a new snapshot.
82
+ book.applyLevels(frame.snapshot, frame.data.orderbook as import('blink-trade-sdk').BookLevels);
83
+ }
84
+ });
85
+ // stop() releases this consumer; manager.dispose() closes everything.
86
+ ```
87
+
88
+ The frontend uses this manager, channel builders, `LocalBook`, candle types and
89
+ `RestClient`. Sovereign transaction signing/submission is outside this SDK's
90
+ REST/WebSocket data API scope. List methods `orders`, `fills`, and `trades`
91
+ return the first page and default to the gateway's maximum of 100 rows; pass a
92
+ smaller limit as the final argument when needed.
93
+
94
+ ## Local end-to-end tests
95
+
96
+ Start the rollup repository's `local_sdk_stack` fixture as described in
97
+ `crates/integration-tests/README.md`. With its public endpoint manifest:
98
+
99
+ ```sh
100
+ BLINK_API=http://127.0.0.1:<gateway-port> bun run test:live
101
+ LOCAL_SDK_STACK_MANIFEST=/tmp/blink-local-stack.json bun run test:local
102
+ ```
103
+
104
+ The local suite covers account/balance/position/order/fill REST projections,
105
+ transaction lookup, spot books, mark candles, BLP, all channel families,
106
+ account-state deltas, subscription sharing, and forced reconnect snapshots.
107
+ It only accepts loopback endpoints and skips when no manifest is provided.
108
+ `bun test` also discovers `test/live.test.ts`, which defaults to the public API;
109
+ use `bun run test` for the offline suite.
110
+
111
+ Git dependencies run `prepare` to generate `dist`; consumers do not need a
112
+ local checkout or copied SDK source.
113
+
114
+ ## Package release checks
115
+
116
+ The committed lockfile and `packageManager` pin make SDK builds reproducible.
117
+ `bun run test:package` packs the SDK, installs the tarball in a temporary consumer
118
+ without lifecycle scripts, and exercises the published entry point. CI runs this
119
+ along with the offline suite. `prepublishOnly` repeats these gates for a release.
120
+
121
+ The package is distributed publicly as `blink-trade-sdk` on
122
+ `https://registry.npmjs.org/`. The source repository can remain private.
123
+
124
+ For the first release, authenticate the npm account that will own the package
125
+ with `npm login`, then run `npm publish` from a clean, validated checkout.
126
+ `publishConfig` fixes the public registry and access level. The publish hook
127
+ runs the offline tests, type checks and isolated package installation check.
128
+
129
+ For subsequent releases, configure an npm [trusted publisher](https://docs.npmjs.com/trusted-publishers/)
130
+ with owner `alterity-systems`, repository `blink-sdk-ts`, workflow `publish.yml`,
131
+ and permission to run `npm publish`. Merge the workflow into the default branch,
132
+ then dispatch it at the intended release ref with the exact package version and
133
+ distribution tag (`latest` for stable releases, `next` for prereleases). It
134
+ rejects a mismatched version before publishing. No npm token secret is needed.
135
+ Npm provenance is only available when the source repository is public.
136
+
137
+ Frontend consumers should pin an exact published version, e.g.
138
+ `bun add --exact blink-trade-sdk@0.1.0`, then run their normal checks. Remove the
139
+ vendored artifact, provenance file and `scripts/update-blink-sdk.mjs` after the
140
+ registry installation succeeds.
@@ -0,0 +1,32 @@
1
+ /** Typed client for the Blink v1 REST API and protocol-v1 WebSocket API. */
2
+ import type { Balance, BlpInfo, Candle, Fill, MarkCandle, Market, MarketStats, MarketType, Order, Orderbook, Position, Resolution, Status, Subaccount, Trade, Transaction } from './types.js';
3
+ export * from './types.js';
4
+ export * from './ws.js';
5
+ export declare class BlinkApiError extends Error {
6
+ readonly status: number;
7
+ readonly code: number;
8
+ constructor(status: number, code: number, message: string);
9
+ }
10
+ export declare class RestClient {
11
+ private readonly base;
12
+ constructor(base: string);
13
+ get<T>(path: string): Promise<T>;
14
+ private query;
15
+ status(): Promise<Status>;
16
+ markets(marketType?: MarketType | 'all', marketId?: number): Promise<Market[]>;
17
+ market(marketId: number, marketType: MarketType): Promise<Market>;
18
+ marketStats(marketId: number, marketType: MarketType): Promise<MarketStats>;
19
+ orderbook(marketId: number, marketType: MarketType, depth?: number): Promise<Orderbook>;
20
+ trades(marketId: number, marketType: MarketType, limit?: number): Promise<Trade[]>;
21
+ candles(marketId: number, marketType: MarketType, resolution: Resolution, startTimestamp: number, endTimestamp: number, countBack: number): Promise<Candle[]>;
22
+ markPriceCandles(marketId: number, resolution: Resolution, startTimestamp: number, endTimestamp: number, countBack: number): Promise<MarkCandle[]>;
23
+ subaccount(address: string, subaccountId: number): Promise<Subaccount>;
24
+ balances(address: string, subaccountId: number): Promise<Balance[]>;
25
+ positions(address: string, subaccountId: number): Promise<Position[]>;
26
+ orders(address: string, subaccountId: number, status: 'active' | 'history' | 'all', limit?: number): Promise<Order[]>;
27
+ fills(address: string, subaccountId: number, limit?: number): Promise<Fill[]>;
28
+ transaction(hash: string): Promise<Transaction>;
29
+ blp(): Promise<BlpInfo>;
30
+ }
31
+ export { BlinkSocketManager, BLINK_WS_PROTOCOL_VERSION, gatewayWebSocketUrl } from './managed-ws.js';
32
+ export type { BlinkSocketManagerOptions, ConnectionStatus, SocketDiagnostic, ChannelFrame, ChannelUpdate as ManagedChannelUpdate, ChannelReset, ChannelError, FrameHandler } from './managed-ws.js';
package/dist/index.js ADDED
@@ -0,0 +1,130 @@
1
+ /** Typed client for the Blink v1 REST API and protocol-v1 WebSocket API. */
2
+ export * from './types.js';
3
+ export * from './ws.js';
4
+ export class BlinkApiError extends Error {
5
+ status;
6
+ code;
7
+ constructor(status, code, message) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.name = 'BlinkApiError';
12
+ }
13
+ }
14
+ export class RestClient {
15
+ base;
16
+ constructor(base) {
17
+ this.base = base.replace(/\/+$/, '');
18
+ }
19
+ async get(path) {
20
+ const response = await fetch(this.base + path);
21
+ if (!response.ok) {
22
+ let message = response.statusText;
23
+ let code = response.status;
24
+ try {
25
+ const body = (await response.json());
26
+ code = body.code ?? code;
27
+ message = body.message ?? message;
28
+ }
29
+ catch {
30
+ // Preserve the HTTP fallback for non-JSON failures.
31
+ }
32
+ throw new BlinkApiError(response.status, code, message);
33
+ }
34
+ return (await response.json());
35
+ }
36
+ query(path, values) {
37
+ const query = new URLSearchParams();
38
+ for (const [key, value] of Object.entries(values)) {
39
+ if (value !== undefined)
40
+ query.set(key, String(value));
41
+ }
42
+ const encoded = query.toString();
43
+ return encoded ? `${path}?${encoded}` : path;
44
+ }
45
+ async status() {
46
+ const body = await this.get('/v1/status');
47
+ return { status: body.status, timestamp_ns: body.timestamp_ns };
48
+ }
49
+ async markets(marketType = 'all', marketId) {
50
+ const body = await this.get(this.query('/v1/markets', { market_type: marketType, market_id: marketId }));
51
+ return body.markets;
52
+ }
53
+ async market(marketId, marketType) {
54
+ const market = (await this.markets(marketType, marketId))[0];
55
+ if (!market)
56
+ throw new BlinkApiError(404, 404, 'market not found');
57
+ return market;
58
+ }
59
+ async marketStats(marketId, marketType) {
60
+ const body = await this.get(this.query('/v1/marketStats', { market_type: marketType, market_id: marketId }));
61
+ const stats = body.market_stats[0];
62
+ if (!stats)
63
+ throw new BlinkApiError(404, 404, 'market stats not found');
64
+ return stats;
65
+ }
66
+ async orderbook(marketId, marketType, depth = 100) {
67
+ const body = await this.get(this.query('/v1/orderBook', { market_type: marketType, market_id: marketId, depth }));
68
+ return { timestamp_ns: body.timestamp_ns, sequence: body.sequence, ...body.orderbook };
69
+ }
70
+ async trades(marketId, marketType, limit = 100) {
71
+ const body = await this.get(this.query('/v1/trades', { market_type: marketType, market_id: marketId, limit }));
72
+ return body.trades;
73
+ }
74
+ async candles(marketId, marketType, resolution, startTimestamp, endTimestamp, countBack) {
75
+ const body = await this.get(this.query('/v1/candles', {
76
+ market_type: marketType,
77
+ market_id: marketId,
78
+ resolution,
79
+ start_timestamp: startTimestamp,
80
+ end_timestamp: endTimestamp,
81
+ count_back: countBack,
82
+ }));
83
+ return body.candles;
84
+ }
85
+ async markPriceCandles(marketId, resolution, startTimestamp, endTimestamp, countBack) {
86
+ const body = await this.get(this.query('/v1/markCandles', {
87
+ market_id: marketId,
88
+ resolution,
89
+ start_timestamp: startTimestamp,
90
+ end_timestamp: endTimestamp,
91
+ count_back: countBack,
92
+ }));
93
+ return body.candles;
94
+ }
95
+ async subaccount(address, subaccountId) {
96
+ const body = await this.get(this.query('/v1/account', { address, subaccount_id: subaccountId }));
97
+ const subaccount = body.subaccounts.find((row) => row.subaccount_id === subaccountId);
98
+ if (!subaccount)
99
+ throw new BlinkApiError(404, 404, 'subaccount not found');
100
+ return subaccount;
101
+ }
102
+ async balances(address, subaccountId) {
103
+ const body = await this.get(this.query('/v1/balances', { address, subaccount_id: subaccountId }));
104
+ return body.balances;
105
+ }
106
+ async positions(address, subaccountId) {
107
+ const body = await this.get(this.query('/v1/positions', { address, subaccount_id: subaccountId }));
108
+ return body.positions;
109
+ }
110
+ async orders(address, subaccountId, status, limit = 100) {
111
+ const body = await this.get(this.query('/v1/orders', { address, subaccount_id: subaccountId, status, limit }));
112
+ return body.orders;
113
+ }
114
+ async fills(address, subaccountId, limit = 100) {
115
+ const body = await this.get(this.query('/v1/fills', { address, subaccount_id: subaccountId, role: 'all', limit }));
116
+ return body.fills;
117
+ }
118
+ async transaction(hash) {
119
+ const body = await this.get(this.query('/v1/transactions', { hash }));
120
+ const transaction = body.transactions[0];
121
+ if (!transaction)
122
+ throw new BlinkApiError(404, 404, 'transaction not found');
123
+ return transaction;
124
+ }
125
+ async blp() {
126
+ const body = await this.get('/v1/blp');
127
+ return body.blp;
128
+ }
129
+ }
130
+ export { BlinkSocketManager, BLINK_WS_PROTOCOL_VERSION, gatewayWebSocketUrl } from './managed-ws.js';
@@ -0,0 +1,135 @@
1
+ import { type WebSocketLike } from './ws-transport.js';
2
+ /**
3
+ * Gateway websocket protocol v1 (`/v1/ws`, docs/v1/websocket.md).
4
+ *
5
+ * One physical socket per tab. Channel strings carry their own parameters and
6
+ * are both the server subscription identity and the client routing key, so a
7
+ * channel is subscribed at most once no matter how many consumers share it.
8
+ */
9
+ export declare const BLINK_WS_PROTOCOL_VERSION = 1;
10
+ export type ConnectionStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'incompatible';
11
+ export type SocketDiagnostic = {
12
+ at: number;
13
+ level: 'info' | 'warning' | 'error';
14
+ code: string;
15
+ message: string;
16
+ detail?: unknown;
17
+ };
18
+ /**
19
+ * One continuity-checked `update` frame. `snapshot` is true for the frame that
20
+ * (re)installs the channel: the first update after subscribing. A null sequence alone does not
21
+ * identify a snapshot once the channel is installed. `data` holds every wire field except
22
+ * the envelope (`type`, `channel`, `timestamp_ns`, `sequence`,
23
+ * `previous_sequence`), e.g. `orderbook`, `trades`, `candles`, `operations`.
24
+ */
25
+ export type ChannelUpdate = {
26
+ type: 'update';
27
+ channel: string;
28
+ snapshot: boolean;
29
+ sequence: string | null;
30
+ previous_sequence: string | null;
31
+ timestamp_ns: string | null;
32
+ data: Record<string, unknown>;
33
+ };
34
+ /**
35
+ * The channel's installed state is no longer continuous (socket closed,
36
+ * server closed the stream, sequence gap, or a consumer forced a replacement).
37
+ * The next frame for the channel is a fresh snapshot.
38
+ */
39
+ export type ChannelReset = {
40
+ type: 'reset';
41
+ channel: string;
42
+ reason: string;
43
+ };
44
+ /** A server error scoped to the channel that is not retried automatically. */
45
+ export type ChannelError = {
46
+ type: 'error';
47
+ channel: string;
48
+ code: number | null;
49
+ message: string;
50
+ };
51
+ export type ChannelFrame = ChannelUpdate | ChannelReset | ChannelError;
52
+ export type FrameHandler = (frame: ChannelFrame) => void;
53
+ type StatusHandler = (status: ConnectionStatus) => void;
54
+ type DiagnosticHandler = (diagnostic: SocketDiagnostic) => void;
55
+ export type BlinkSocketManagerOptions = {
56
+ socketFactory?: (url: string) => WebSocketLike;
57
+ random?: () => number;
58
+ schedule?: typeof setTimeout;
59
+ cancelSchedule?: typeof clearTimeout;
60
+ repeat?: typeof setInterval;
61
+ cancelRepeat?: typeof clearInterval;
62
+ now?: () => number;
63
+ };
64
+ export declare class BlinkSocketManager {
65
+ private readonly url;
66
+ private readonly options;
67
+ private socket;
68
+ private session;
69
+ private channels;
70
+ private statusHandlers;
71
+ private diagnosticHandlers;
72
+ private diagnostics;
73
+ private status;
74
+ private connectScheduled;
75
+ private reconnectTimer;
76
+ private watchdogTimer;
77
+ private reconnectAttempt;
78
+ private generation;
79
+ private helloAccepted;
80
+ private protocolRejected;
81
+ private keepaliveTimeoutMs;
82
+ constructor(url: string, options?: BlinkSocketManagerOptions);
83
+ /**
84
+ * Attach a consumer to a channel. The first consumer subscribes the channel
85
+ * on the server; further consumers share it. A consumer that attaches after
86
+ * the channel's snapshot was installed cannot rebuild that snapshot from
87
+ * deltas, so the server subscription is replaced and every consumer receives
88
+ * a fresh lifecycle (`reset`, then a snapshot `update`).
89
+ */
90
+ subscribe(channel: string, handler: FrameHandler): () => void;
91
+ /** Replace one channel's server subscription so it re-installs a snapshot. */
92
+ resubscribe(channel: string, reason: string): void;
93
+ onStatus(handler: StatusHandler): () => void;
94
+ onDiagnostic(handler: DiagnosticHandler): () => void;
95
+ getDiagnostics(): readonly SocketDiagnostic[];
96
+ getStatus(): ConnectionStatus;
97
+ /** Exposed for diagnostics and focused unit tests. */
98
+ debugState(): {
99
+ channels: number;
100
+ awaitingSnapshot: number;
101
+ keepaliveTimeoutMs: number;
102
+ status: ConnectionStatus;
103
+ };
104
+ dispose(): void;
105
+ private ensureConnection;
106
+ private connect;
107
+ private handleClose;
108
+ private handleMessage;
109
+ private handleUpdate;
110
+ private handleError;
111
+ private rejectProtocol;
112
+ private sendSubscribe;
113
+ private replaceSubscription;
114
+ private removeChannel;
115
+ private clearRetry;
116
+ /** Replace subscriptions whose snapshot never arrived. */
117
+ private startWatchdog;
118
+ private stopWatchdog;
119
+ private clearTimers;
120
+ private scheduleReconnect;
121
+ private canSend;
122
+ private send;
123
+ private dispatch;
124
+ private now;
125
+ private setStatus;
126
+ private addDiagnostic;
127
+ }
128
+ export declare function gatewayWebSocketUrl(apiUrl: string): string;
129
+ export declare const __testing: {
130
+ SOCKET_CONNECTING: number;
131
+ SOCKET_OPEN: number;
132
+ SNAPSHOT_TIMEOUT_MS: number;
133
+ STREAM_RETRY_MS: number;
134
+ };
135
+ export {};