@lynx-crypto/kraken-api 0.1.2 → 0.2.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/README.md CHANGED
@@ -15,7 +15,7 @@ TypeScript client for **Kraken SPOT**:
15
15
 
16
16
  See [Kraken Official Documentation](https://docs.kraken.com/api/docs/category/guides)
17
17
 
18
- IMPORTANT
18
+ ## Important
19
19
 
20
20
  - This package is currently **SPOT only**. It does **not** implement Kraken Futures.
21
21
  - Unofficial project. Not affiliated with Kraken.
@@ -25,24 +25,15 @@ IMPORTANT
25
25
  ## Install
26
26
 
27
27
  NPM:
28
-
29
- ```
30
- npm i @lynx-crypto/kraken-api
31
- ```
28
+ `npm i @lynx-crypto/kraken-api`
32
29
 
33
30
  Yarn:
34
-
35
- ```
36
- yarn add @lynx-crypto/kraken-api
37
- ```
31
+ `yarn add @lynx-crypto/kraken-api`
38
32
 
39
33
  pnpm:
34
+ `pnpm add @lynx-crypto/kraken-api`
40
35
 
41
- ```
42
- pnpm add @lynx-crypto/kraken-api
43
- ```
44
-
45
- Node support
36
+ ### Node support
46
37
 
47
38
  - Node >= 18 recommended (uses built-in fetch / AbortController)
48
39
 
@@ -52,14 +43,20 @@ Node support
52
43
 
53
44
  ESM:
54
45
 
55
- ```
56
- import { KrakenSpotRestClient, KrakenSpotWebsocketV2Client } from "@lynx-crypto/kraken-api";
46
+ ```ts
47
+ import {
48
+ KrakenSpotRestClient,
49
+ KrakenSpotWebsocketV2Client,
50
+ } from '@lynx-crypto/kraken-api';
57
51
  ```
58
52
 
59
53
  CJS:
60
54
 
61
- ```
62
- const { KrakenSpotRestClient, KrakenSpotWebsocketV2Client } = require("@lynx-crypto/kraken-api");
55
+ ```js
56
+ const {
57
+ KrakenSpotRestClient,
58
+ KrakenSpotWebsocketV2Client,
59
+ } = require('@lynx-crypto/kraken-api');
63
60
  ```
64
61
 
65
62
  ---
@@ -71,9 +68,9 @@ const { KrakenSpotRestClient, KrakenSpotWebsocketV2Client } = require("@lynx-cry
71
68
  ```ts
72
69
  const kraken = new KrakenSpotRestClient({
73
70
  // Optional:
74
- // baseUrl: "https://api.kraken.com",
75
- // timeoutMs: 10_000,
76
- // userAgent: "my-app/1.0.0",
71
+ // baseUrl: "https://api.kraken.com",
72
+ // timeoutMs: 10_000,
73
+ // userAgent: "my-app/1.0.0",
77
74
 
78
75
  // Required for private endpoints:
79
76
  apiKey: process.env.KRAKEN_API_KEY,
@@ -81,26 +78,123 @@ const kraken = new KrakenSpotRestClient({
81
78
 
82
79
  // Optional logger:
83
80
  // logger: console,
81
+
82
+ // Optional rate limiting:
83
+ // rateLimit: { mode: "auto" },
84
84
  });
85
85
  ```
86
86
 
87
87
  ### Public endpoint example
88
88
 
89
- (Your exact public endpoints depend on what you’ve implemented in src/spot/rest.)
90
-
91
- Example shape:
89
+ (Exact public endpoints depend on what you’ve implemented under src/spot/rest.)
92
90
 
93
91
  ```ts
94
92
  const serverTime = await kraken.public.getServerTime();
93
+ console.log(serverTime);
95
94
  ```
96
95
 
97
96
  ### Private endpoint example
98
97
 
99
- (Your exact private endpoints depend on what you’ve implemented in src/spot/rest.)
98
+ (Exact private endpoints depend on what you’ve implemented under src/spot/rest.)
100
99
 
101
- Example shape:
100
+ ```ts
102
101
  const balances = await kraken.accountData.getAccountBalance();
103
- console.log("USD:", balances["ZUSD"]);
102
+ console.log('USD:', balances['ZUSD']);
103
+ ```
104
+
105
+ ---
106
+
107
+ ## REST rate limiting & retries
108
+
109
+ This library supports Kraken-style rate limiting with optional automatic retries:
110
+
111
+ - Lightweight in-memory token bucket limiter by default
112
+ - Automatic retries are configurable
113
+ - Handles:
114
+ - EAPI:Rate limit exceeded
115
+ - EService: Throttled: <unix timestamp>
116
+ - HTTP 429 Too Many Requests
117
+
118
+ Example:
119
+
120
+ ```ts
121
+ const kraken = new KrakenSpotRestClient({
122
+ apiKey: process.env.KRAKEN_API_KEY,
123
+ apiSecret: process.env.KRAKEN_API_SECRET,
124
+ rateLimit: {
125
+ mode: 'auto',
126
+ tier: 'starter',
127
+ retryOnRateLimit: true,
128
+ maxRetries: 5,
129
+ // restCostFn: (path) => (path.includes("Ledgers") ? 2 : 1),
130
+ },
131
+ });
132
+ ```
133
+
134
+ Disable built-in throttling:
135
+
136
+ ```ts
137
+ rateLimit: {
138
+ mode: 'off';
139
+ }
140
+ ```
141
+
142
+ ### Redis rate limiting (multi-process / multi-container)
143
+
144
+ If you run multiple Node processes, Docker containers, or workers, they all share the same Kraken IP-level limits. In-memory rate limiting only protects a single process.
145
+
146
+ For cross-process coordination, you can use the Redis-backed token bucket limiter.
147
+
148
+ Example (you provide the Redis client + EVAL wrapper):
149
+
150
+ ```ts
151
+ import { KrakenSpotRestClient } from '@lynx-crypto/kraken-api';
152
+ import { RedisTokenBucketLimiter } from '@lynx-crypto/kraken-api/base/redisRateLimit';
153
+
154
+ // Your Redis EVAL wrapper should return a number:
155
+ // - 0 means "proceed now"
156
+ // - >0 means "wait this many ms then retry"
157
+ const evalRedis = async (
158
+ key: string,
159
+ maxCounter: number,
160
+ decayPerSec: number,
161
+ cost: number,
162
+ ttlSeconds: number,
163
+ minWaitMs: number,
164
+ ): Promise<number> => {
165
+ // Example shape (pseudo-code):
166
+ // return await redis.eval(luaScript, { keys: [key], arguments: [maxCounter, decayPerSec, cost, ttlSeconds, minWaitMs] });
167
+ return 0;
168
+ };
169
+
170
+ const kraken = new KrakenSpotRestClient({
171
+ apiKey: process.env.KRAKEN_API_KEY,
172
+ apiSecret: process.env.KRAKEN_API_SECRET,
173
+ rateLimit: {
174
+ mode: 'auto',
175
+ tier: 'starter',
176
+ retryOnRateLimit: true,
177
+ maxRetries: 5,
178
+
179
+ // Cross-process limiter (Redis):
180
+ redis: {
181
+ limiter: new RedisTokenBucketLimiter({
182
+ key: 'kraken:rest:global',
183
+ maxCounter: 15,
184
+ decayPerSec: 0.33,
185
+ ttlSeconds: 30,
186
+ minWaitMs: 50,
187
+ evalRedis,
188
+ }),
189
+ },
190
+ },
191
+ });
192
+ ```
193
+
194
+ Notes:
195
+
196
+ - Redis is optional. Only use it when you need cross-process coordination.
197
+ - If Redis is down / eval fails, the request fails (no silent bypass).
104
198
 
105
199
  ---
106
200
 
@@ -108,67 +202,51 @@ console.log("USD:", balances["ZUSD"]);
108
202
 
109
203
  This package provides a top-level v2 WS client that creates:
110
204
 
111
- - a **public connection** (market data + admin)
112
- - a **private/auth connection** (user-data + user-trading)
205
+ - a public connection (market data + admin)
206
+ - a private/auth connection (user-data + user-trading)
113
207
 
114
208
  ### Create a WS v2 client
115
209
 
116
210
  ```ts
117
211
  const ws = new KrakenSpotWebsocketV2Client({
118
- // Optional override URLs:
119
212
  // publicUrl: "wss://ws.kraken.com/v2",
120
213
  // privateUrl: "wss://ws-auth.kraken.com/v2",
121
214
 
122
- // IMPORTANT: private WS requires a session token
123
215
  authToken: process.env.KRAKEN_WS_AUTH_TOKEN,
124
216
 
125
- // Optional connection tuning:
126
217
  // autoReconnect: true,
127
218
  // reconnectDelayMs: 1_000,
128
219
  // requestTimeoutMs: 10_000,
129
220
 
130
- // Optional logger:
131
221
  // logger: console,
132
-
133
- // Optional WS implementation:
134
- // - In Node, ws is used by default.
135
- // - In browsers, pass the browser WebSocket if needed.
136
222
  // WebSocketImpl: WebSocket,
137
223
  });
138
224
  ```
139
225
 
140
226
  Available sub-APIs:
141
227
 
142
- - `ws.admin` (public connection)
143
- - `ws.marketData` (public connection)
144
- - `ws.userData` (private connection)
145
- - `ws.userTrading` (private connection)
228
+ - `ws.admin`
229
+ - `ws.marketData`
230
+ - `ws.userData`
231
+ - `ws.userTrading`
146
232
 
147
233
  ### Connect
148
234
 
149
- You can connect explicitly:
150
-
151
235
  ```ts
152
236
  await ws.publicConnection.connect();
153
237
  await ws.privateConnection.connect();
154
238
  ```
155
239
 
156
- Or let calls auto-connect (methods like `request()/sendRaw()` will connect if needed).
157
-
158
240
  ---
159
241
 
160
242
  ## WS routing: receiving streaming messages
161
243
 
162
- The underlying `KrakenWebsocketBase` supports message fan-out:
163
-
164
244
  ```ts
165
245
  const unsubscribe = ws.publicConnection.addMessageHandler((msg) => {
166
- // msg is already JSON-parsed when possible
167
- // route based on msg.channel / msg.type, etc.
168
- // console.log(msg);
246
+ // route by msg.channel / msg.type
169
247
  });
170
248
 
171
- // later:
249
+ // later
172
250
  unsubscribe();
173
251
  ```
174
252
 
@@ -176,10 +254,6 @@ unsubscribe();
176
254
 
177
255
  ## WS v2: Admin
178
256
 
179
- Admin utilities exist on the public connection (ex: ping/status/heartbeat).
180
-
181
- Example:
182
-
183
257
  ```ts
184
258
  const pong = await ws.admin.ping({ reqId: 123 });
185
259
  if (!pong.success) console.error('ping failed:', pong.error);
@@ -187,150 +261,82 @@ if (!pong.success) console.error('ping failed:', pong.error);
187
261
 
188
262
  ---
189
263
 
190
- ## WS v2: Market Data (public)
191
-
192
- Market data subscriptions live on ws.marketData (public connection).
193
- (Exact channel helpers depend on your implemented market-data modules.)
194
-
195
- Typical pattern:
196
-
197
- 1. call subscribe helper (await ack)
198
- 2. listen via `addMessageHandler` and route messages by channel/type
199
-
200
- ---
201
-
202
264
  ## WS v2: User Data (authenticated)
203
265
 
204
- User-data streams live on `ws.userData` (private connection).
205
-
206
266
  Implemented channels:
207
267
 
208
- - executions (order lifecycle + fills)
209
- - balances (balance snapshots + ledger-derived updates)
268
+ - `executions`
269
+ - `balances`
210
270
 
211
- Example (executions):
271
+ ### Executions example
212
272
 
213
- ```
273
+ ```ts
214
274
  const ack = await ws.userData.subscribeExecutions({
215
- snap_trades: true,
216
- snap_orders: true,
217
- order_status: true,
275
+ snap_trades: true,
276
+ snap_orders: true,
277
+ order_status: true,
218
278
  });
219
-
220
- if (!ack.success) console.error("executions subscribe error:", ack.error);
221
279
  ```
222
280
 
223
- Then route messages:
224
-
225
- ```
226
- ws.privateConnection.addMessageHandler((msg: any) => {
227
- if (msg?.channel === "executions" && (msg.type === "snapshot" || msg.type === "update")) {
228
- for (const report of msg.data ?? []) {
229
- console.log("[exec]", report.exec_type, report.order_id, report.order_status);
230
- }
281
+ ```ts
282
+ ws.privateConnection.addMessageHandler((msg) => {
283
+ if (msg?.channel === 'executions') {
284
+ for (const report of msg.data ?? []) {
285
+ console.log(report.order_id, report.order_status);
231
286
  }
287
+ }
232
288
  });
233
289
  ```
234
290
 
235
- Example (balances):
291
+ ### Balances example
236
292
 
237
- ````ts
293
+ ```ts
238
294
  const ack2 = await ws.userData.subscribeBalances({ snapshot: true });
239
- if (!ack2.success) console.error("balances subscribe error:", ack2.error);
295
+ ```
240
296
 
241
- ws.privateConnection.addMessageHandler((msg: any) => {
242
- if (msg?.channel === "balances" && msg.type === "snapshot") {
243
- for (const asset of msg.data ?? []) {
244
- console.log("[balances snapshot]", asset.asset, "total:", asset.balance);
245
- }
246
- }
247
- if (msg?.channel === "balances" && msg.type === "update") {
248
- for (const tx of msg.data ?? []) {
249
- console.log("[balances update]", tx.asset, tx.type, "delta:", tx.amount, "new:", tx.balance);
250
- }
251
- }
297
+ ```ts
298
+ ws.privateConnection.addMessageHandler((msg) => {
299
+ if (msg?.channel === 'balances') {
300
+ console.log(msg.data);
301
+ }
252
302
  });
303
+ ```
253
304
 
254
305
  ---
255
306
 
256
307
  ## WS v2: User Trading (authenticated RPC)
257
308
 
258
- User-trading methods live on `ws.userTrading` (private connection).
259
-
260
309
  Implemented RPCs:
261
310
 
262
311
  - `add_order`
263
312
  - `amend_order`
264
- - `edit_order` (legacy)
265
313
  - `cancel_order`
266
314
  - `cancel_all`
267
- - `cancel_all_orders_after` (Dead Man’s Switch)
315
+ - `cancel_all_orders_after`
268
316
  - `batch_add`
269
317
  - `batch_cancel`
270
318
 
271
319
  Add order:
320
+
272
321
  ```ts
273
322
  const res = await ws.userTrading.addOrder({
274
- order_type: "limit",
275
- side: "buy",
276
- symbol: "BTC/USD",
277
- order_qty: 0.01,
278
- limit_price: 30000,
279
- time_in_force: "gtc",
280
- cl_ord_id: "demo-0001",
323
+ order_type: 'limit',
324
+ side: 'buy',
325
+ symbol: 'BTC/USD',
326
+ order_qty: 0.01,
327
+ limit_price: 30000,
328
+ time_in_force: 'gtc',
281
329
  });
282
-
283
-
284
- if (res.success) console.log("order_id:", res.result?.order_id);
285
- else console.error("add_order error:", res.error);
286
- ````
330
+ ```
287
331
 
288
332
  Dead Man’s Switch:
289
333
 
290
- ```
291
- // recommended: refresh every 15–30s with timeout=60
334
+ ```ts
292
335
  await ws.userTrading.cancelAllOrdersAfter({ timeout: 60 });
293
336
  ```
294
337
 
295
338
  ---
296
339
 
297
- ## Options reference
298
-
299
- ### `KrakenSpotRestClient` options
300
-
301
- - `baseUrl?: string`
302
- Default: https://api.kraken.com
303
- - `timeoutMs?: number`
304
- Default: 10_000
305
- - `userAgent?: string`
306
- - `apiKey?: string`
307
- Required for private endpoints
308
- - `apiSecret?: string` (base64)
309
- Required for private endpoints
310
- - `logger?: KrakenLogger`
311
- debug/info/warn/error(msg, meta?)
312
-
313
- ### KrakenSpotWebsocketV2Client options
314
-
315
- - `publicUrl?: string`
316
- Default: wss://ws.kraken.com/v2
317
- - `privateUrl?: string`
318
- Default: wss://ws-auth.kraken.com/v2
319
- - `authToken?: string`
320
- Required for authenticated/private connection features
321
- - `WebSocketImpl?: constructor`
322
- Optional override (browser / custom WS)
323
- - `autoReconnect?: boolean`
324
- Default: true
325
- - `reconnectDelayMs?: number`
326
- Default: 1000
327
- - `requestTimeoutMs?: number`
328
- Default: 10_000
329
- - `logger?: KrakenWebsocketLogger`
330
- debug/info/warn/error(msg, meta?)
331
-
332
- ---
333
-
334
340
  ## Development
335
341
 
336
342
  Install:
@@ -349,11 +355,11 @@ Build:
349
355
 
350
356
  ## Security notes
351
357
 
352
- - Keep API keys/secrets out of source control.
353
- - Use least-privilege API key permissions.
358
+ - Keep API keys and secrets out of source control
359
+ - Use least-privilege API key permissions
354
360
 
355
361
  ---
356
362
 
357
363
  ## License
358
364
 
359
- MIT (see LICENSE)
365
+ MIT (see LICENSE.md)