@alpha-arcade/sdk 0.3.1 → 0.3.6
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 +167 -4
- package/dist/index.cjs +339 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +189 -2
- package/dist/index.d.ts +189 -2
- package/dist/index.js +338 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript SDK for trading on **Alpha Market** — Algorand prediction markets.
|
|
4
4
|
|
|
5
|
-
Place orders, manage positions, read orderbooks, and build automated trading bots
|
|
5
|
+
Place orders, manage positions, read orderbooks from the API or chain, and build automated trading bots.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -246,7 +246,7 @@ for (const pos of positions) {
|
|
|
246
246
|
|
|
247
247
|
#### `getOrderbook(marketAppId)`
|
|
248
248
|
|
|
249
|
-
Fetches the full on-chain orderbook.
|
|
249
|
+
Fetches the full on-chain orderbook for a single market app.
|
|
250
250
|
|
|
251
251
|
```typescript
|
|
252
252
|
const book = await client.getOrderbook(123456789);
|
|
@@ -263,6 +263,24 @@ if (book.yes.bids.length > 0) {
|
|
|
263
263
|
}
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
+
#### `getFullOrderbookFromApi(marketId)`
|
|
267
|
+
|
|
268
|
+
Fetches the full processed orderbook snapshot for a market from the Alpha REST API. Requires `apiKey`.
|
|
269
|
+
|
|
270
|
+
This returns the same shape as websocket `orderbook_changed.orderbook`: a record keyed by `marketAppId`, where each value includes:
|
|
271
|
+
- top-level aggregated `bids`, `asks`, and `spread`
|
|
272
|
+
- detailed `yes` and `no` bid/ask orders with `escrowAppId` and `owner`
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const snapshot = await client.getFullOrderbookFromApi('market-uuid-here');
|
|
276
|
+
|
|
277
|
+
for (const [appId, book] of Object.entries(snapshot)) {
|
|
278
|
+
console.log(`App ${appId}: spread=${book.spread}`);
|
|
279
|
+
console.log('Top-level bids:', book.bids);
|
|
280
|
+
console.log('Detailed YES bids:', book.yes.bids);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
266
284
|
#### `getOpenOrders(marketAppId, walletAddress?)`
|
|
267
285
|
|
|
268
286
|
Gets open orders for a wallet on a specific market (from on-chain data).
|
|
@@ -326,15 +344,160 @@ const market = await client.getMarketFromApi('uuid-here');
|
|
|
326
344
|
|
|
327
345
|
#### `getRewardMarkets()`
|
|
328
346
|
|
|
329
|
-
Fetches markets that have liquidity rewards from the Alpha REST API. Requires `apiKey`. Returns the same `Market[]` shape with reward fields populated: `totalRewards`, `rewardsPaidOut`, `rewardsSpreadDistance`, `rewardsMinContracts`, `lastRewardAmount`, `lastRewardTs`.
|
|
347
|
+
Fetches markets that have liquidity rewards from the Alpha REST API. Requires `apiKey`. Returns the same `Market[]` shape with reward fields populated: `totalRewards`, `totalPregameRewards`, `rewardsPaidOut`, `rewardsSpreadDistance`, `rewardsMinContracts`, `lastRewardAmount`, `lastRewardTs`. For sports markets, pregame liquidity rewards may be exposed via `totalPregameRewards`.
|
|
330
348
|
|
|
331
349
|
```typescript
|
|
332
350
|
const rewardMarkets = await client.getRewardMarkets();
|
|
333
351
|
for (const m of rewardMarkets) {
|
|
334
|
-
|
|
352
|
+
const rewardTotal = m.totalRewards ?? m.totalPregameRewards ?? 0;
|
|
353
|
+
console.log(`${m.title}: $${rewardTotal / 1e6} total rewards`);
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### WebSocket Streams
|
|
360
|
+
|
|
361
|
+
Real-time data streams via WebSocket. No API key or auth required. Replaces polling with push-based updates.
|
|
362
|
+
|
|
363
|
+
The SDK connects to the public platform websocket at `wss://wss.platform.alphaarcade.com`. The first
|
|
364
|
+
subscription is sent in the connection query string, and any later subscribe or unsubscribe calls use the
|
|
365
|
+
server's control-message envelope:
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{
|
|
369
|
+
"id": "request-id",
|
|
370
|
+
"method": "SUBSCRIBE",
|
|
371
|
+
"params": [
|
|
372
|
+
{ "stream": "get-orderbook", "slug": "will-btc-hit-100k" }
|
|
373
|
+
]
|
|
335
374
|
}
|
|
336
375
|
```
|
|
337
376
|
|
|
377
|
+
Supported public streams:
|
|
378
|
+
|
|
379
|
+
- `get-live-markets`
|
|
380
|
+
- `get-market` with `slug`
|
|
381
|
+
- `get-orderbook` with `slug`
|
|
382
|
+
- `get-wallet-orders` with `wallet`
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { AlphaWebSocket } from '@alpha-arcade/sdk';
|
|
386
|
+
|
|
387
|
+
// Node.js 22+ and browsers — native WebSocket, nothing extra needed
|
|
388
|
+
const ws = new AlphaWebSocket();
|
|
389
|
+
|
|
390
|
+
// Node.js < 22 — install `ws` and pass it in:
|
|
391
|
+
// npm install ws
|
|
392
|
+
import WebSocket from 'ws';
|
|
393
|
+
const ws = new AlphaWebSocket({ WebSocket });
|
|
394
|
+
|
|
395
|
+
// Subscribe to orderbook updates (~5s snapshots)
|
|
396
|
+
const unsub = ws.subscribeOrderbook('will-btc-hit-100k', (event) => {
|
|
397
|
+
console.log('Orderbook:', event.orderbook);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Unsubscribe when done
|
|
401
|
+
unsub();
|
|
402
|
+
|
|
403
|
+
// Close the connection
|
|
404
|
+
ws.close();
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### `subscribeLiveMarkets(callback)`
|
|
408
|
+
|
|
409
|
+
Receive incremental diffs whenever market probabilities change.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
ws.subscribeLiveMarkets((event) => {
|
|
413
|
+
console.log('Markets changed at', event.ts, event);
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### `subscribeMarket(slug, callback)`
|
|
418
|
+
|
|
419
|
+
Receive change events for a single market. Uses the market **slug** (not `marketAppId`) — see note on `subscribeOrderbook` below.
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
ws.subscribeMarket('will-btc-hit-100k', (event) => {
|
|
423
|
+
console.log('Market update:', event);
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### `subscribeOrderbook(slug, callback)`
|
|
428
|
+
|
|
429
|
+
Receive full orderbook snapshots on every change (~5s interval). The payload matches `getFullOrderbookFromApi(marketId)`.
|
|
430
|
+
|
|
431
|
+
**Note:** The WebSocket API uses market **slugs** (URL-friendly names like `"will-btc-hit-100k"`), not `marketAppId` numbers. You can get a market's slug from the `slug` field on `Market` objects returned by `getLiveMarkets()` or `getMarket()`.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
ws.subscribeOrderbook('will-btc-hit-100k', (event) => {
|
|
435
|
+
// Top-level bids/asks use decimal prices (cents)
|
|
436
|
+
// Nested yes/no use raw microunit prices with escrowAppId and owner
|
|
437
|
+
for (const [appId, book] of Object.entries(event.orderbook)) {
|
|
438
|
+
console.log(`App ${appId}: spread=${book.spread}`);
|
|
439
|
+
console.log(' Bids:', book.bids);
|
|
440
|
+
console.log(' Yes bids:', book.yes.bids);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
#### `subscribeWalletOrders(wallet, callback)`
|
|
446
|
+
|
|
447
|
+
Receive updates when orders for a wallet are created or modified.
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
ws.subscribeWalletOrders('MMU6X...', (event) => {
|
|
451
|
+
console.log('Wallet orders changed:', event);
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### Unsubscribing
|
|
456
|
+
|
|
457
|
+
Each `subscribe*` method returns an unsubscribe function. Call it to stop receiving events for that stream:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
const unsub = ws.subscribeOrderbook('my-market', (event) => { /* ... */ });
|
|
461
|
+
|
|
462
|
+
// Later, stop listening
|
|
463
|
+
unsub();
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
#### Control Methods
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// List active subscriptions on this connection
|
|
470
|
+
const subs = await ws.listSubscriptions();
|
|
471
|
+
|
|
472
|
+
// Query server properties (`heartbeat` or `limits`)
|
|
473
|
+
const props = await ws.getProperty('heartbeat');
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### Configuration
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
import WebSocket from 'ws'; // Only needed on Node.js < 22
|
|
480
|
+
|
|
481
|
+
const ws = new AlphaWebSocket({
|
|
482
|
+
WebSocket, // Pass `ws` on Node.js < 22 (not needed in browsers or Node 22+)
|
|
483
|
+
url: 'wss://custom-endpoint.example.com', // Override default URL
|
|
484
|
+
reconnect: true, // Auto-reconnect (default: true)
|
|
485
|
+
maxReconnectAttempts: 10, // Give up after 10 retries (default: Infinity)
|
|
486
|
+
heartbeatIntervalMs: 60_000, // Ping interval in ms (default: 60000)
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
#### Connection Details
|
|
491
|
+
|
|
492
|
+
| Setting | Value |
|
|
493
|
+
|---------|-------|
|
|
494
|
+
| Heartbeat | 60s (auto-handled) |
|
|
495
|
+
| Idle timeout | 180s |
|
|
496
|
+
| Rate limit | 5 messages/sec/connection |
|
|
497
|
+
| Reconnect | Exponential backoff (1s → 30s max) |
|
|
498
|
+
|
|
499
|
+
The client automatically responds to server pings, sends keepalive pings, and reconnects with exponential backoff on unexpected disconnects. All active subscriptions are restored after reconnect.
|
|
500
|
+
|
|
338
501
|
---
|
|
339
502
|
|
|
340
503
|
### Utility Functions
|
package/dist/index.cjs
CHANGED
|
@@ -2125,6 +2125,7 @@ var calculateMatchingOrders = (orderbook, isBuying, isYes, quantity, price, slip
|
|
|
2125
2125
|
|
|
2126
2126
|
// src/constants.ts
|
|
2127
2127
|
var DEFAULT_API_BASE_URL = "https://platform.alphaarcade.com/api";
|
|
2128
|
+
var DEFAULT_WSS_BASE_URL = "wss://wss.platform.alphaarcade.com";
|
|
2128
2129
|
var DEFAULT_MARKET_CREATOR_ADDRESS = "5P5Y6HTWUNG2E3VXBQDZN3ENZD3JPAIR5PKT3LOYJAPAUKOLFD6KANYTRY";
|
|
2129
2130
|
|
|
2130
2131
|
// src/modules/orderbook.ts
|
|
@@ -2265,6 +2266,18 @@ var getWalletOrdersFromApi = async (config, walletAddress) => {
|
|
|
2265
2266
|
}
|
|
2266
2267
|
return allOrders;
|
|
2267
2268
|
};
|
|
2269
|
+
var getFullOrderbookFromApi = async (config, marketId) => {
|
|
2270
|
+
if (!config.apiKey) {
|
|
2271
|
+
throw new Error("apiKey is required for API-based orderbook fetching. Retrieve an API key from the Alpha Arcade platform via the Account page and pass it to the client.");
|
|
2272
|
+
}
|
|
2273
|
+
const baseUrl = config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
|
|
2274
|
+
const url = `${baseUrl}/get-full-orderbook?marketId=${encodeURIComponent(marketId)}`;
|
|
2275
|
+
const response = await fetch(url, { headers: { "x-api-key": config.apiKey } });
|
|
2276
|
+
if (!response.ok) {
|
|
2277
|
+
throw new Error(`Alpha API error: ${response.status} ${response.statusText}`);
|
|
2278
|
+
}
|
|
2279
|
+
return response.json();
|
|
2280
|
+
};
|
|
2268
2281
|
|
|
2269
2282
|
// src/modules/trading.ts
|
|
2270
2283
|
var extractEscrowAppId = async (algodClient, indexerClient, targetTxId) => {
|
|
@@ -3089,12 +3102,18 @@ var getMarketFromApi = async (config, marketId) => {
|
|
|
3089
3102
|
}
|
|
3090
3103
|
return market;
|
|
3091
3104
|
};
|
|
3105
|
+
var hasRewardLiquidity = (market) => {
|
|
3106
|
+
if ((market.totalRewards ?? 0) > 0 || (market.totalPregameRewards ?? 0) > 0) {
|
|
3107
|
+
return true;
|
|
3108
|
+
}
|
|
3109
|
+
return (market.options ?? []).some((option) => (option.totalRewards ?? 0) > 0 || (option.totalPregameRewards ?? 0) > 0);
|
|
3110
|
+
};
|
|
3092
3111
|
var getRewardMarkets = async (config) => {
|
|
3093
3112
|
if (!config.apiKey) {
|
|
3094
3113
|
throw new Error("apiKey is required for API-based market fetching. Retrieve an API key from the Alpha Arcade platform via the Account page and pass it to the client.");
|
|
3095
3114
|
}
|
|
3096
3115
|
const markets = await getLiveMarketsFromApi(config);
|
|
3097
|
-
return markets.filter((
|
|
3116
|
+
return markets.filter((market) => hasRewardLiquidity(market));
|
|
3098
3117
|
};
|
|
3099
3118
|
var getLiveMarkets = async (config) => {
|
|
3100
3119
|
if (config.apiKey) {
|
|
@@ -3271,6 +3290,19 @@ var AlphaClient = class {
|
|
|
3271
3290
|
async getOrderbook(marketAppId) {
|
|
3272
3291
|
return getOrderbook(this.config, marketAppId);
|
|
3273
3292
|
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Fetches the full processed orderbook snapshot for a market from the Alpha REST API.
|
|
3295
|
+
*
|
|
3296
|
+
* Returns the same shape as websocket `orderbook_changed.orderbook`: a record keyed by
|
|
3297
|
+
* `marketAppId`, where each value includes aggregated bids/asks plus detailed yes/no orders.
|
|
3298
|
+
* Requires `apiKey`.
|
|
3299
|
+
*
|
|
3300
|
+
* @param marketId - The Alpha market UUID
|
|
3301
|
+
* @returns Full processed market orderbook keyed by marketAppId
|
|
3302
|
+
*/
|
|
3303
|
+
async getFullOrderbookFromApi(marketId) {
|
|
3304
|
+
return getFullOrderbookFromApi(this.config, marketId);
|
|
3305
|
+
}
|
|
3274
3306
|
/**
|
|
3275
3307
|
* Gets open orders for a specific wallet on a market.
|
|
3276
3308
|
*
|
|
@@ -3366,9 +3398,315 @@ var AlphaClient = class {
|
|
|
3366
3398
|
}
|
|
3367
3399
|
};
|
|
3368
3400
|
|
|
3401
|
+
// src/websocket.ts
|
|
3402
|
+
var WS_OPEN = 1;
|
|
3403
|
+
var resolveWebSocket = (provided) => {
|
|
3404
|
+
if (provided) return provided;
|
|
3405
|
+
if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
3406
|
+
return globalThis.WebSocket;
|
|
3407
|
+
}
|
|
3408
|
+
throw new Error(
|
|
3409
|
+
'No WebSocket implementation found. On Node.js < 22, install the "ws" package and pass it: new AlphaWebSocket({ WebSocket: require("ws") })'
|
|
3410
|
+
);
|
|
3411
|
+
};
|
|
3412
|
+
var AlphaWebSocket = class {
|
|
3413
|
+
url;
|
|
3414
|
+
reconnectEnabled;
|
|
3415
|
+
maxReconnectAttempts;
|
|
3416
|
+
heartbeatIntervalMs;
|
|
3417
|
+
WebSocketImpl;
|
|
3418
|
+
ws = null;
|
|
3419
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
3420
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
3421
|
+
lastOrderbookVersionBySubscription = /* @__PURE__ */ new Map();
|
|
3422
|
+
heartbeatTimer = null;
|
|
3423
|
+
reconnectTimer = null;
|
|
3424
|
+
reconnectAttempts = 0;
|
|
3425
|
+
intentionallyClosed = false;
|
|
3426
|
+
connectPromise = null;
|
|
3427
|
+
constructor(config) {
|
|
3428
|
+
this.url = config?.url ?? DEFAULT_WSS_BASE_URL;
|
|
3429
|
+
this.reconnectEnabled = config?.reconnect ?? true;
|
|
3430
|
+
this.maxReconnectAttempts = config?.maxReconnectAttempts ?? Infinity;
|
|
3431
|
+
this.heartbeatIntervalMs = config?.heartbeatIntervalMs ?? 6e4;
|
|
3432
|
+
this.WebSocketImpl = resolveWebSocket(config?.WebSocket);
|
|
3433
|
+
}
|
|
3434
|
+
/** Whether the WebSocket is currently open and connected */
|
|
3435
|
+
get connected() {
|
|
3436
|
+
return this.ws?.readyState === WS_OPEN;
|
|
3437
|
+
}
|
|
3438
|
+
// ============================================
|
|
3439
|
+
// Subscribe Methods
|
|
3440
|
+
// ============================================
|
|
3441
|
+
/**
|
|
3442
|
+
* Subscribe to live market probability updates (incremental diffs).
|
|
3443
|
+
* @returns An unsubscribe function
|
|
3444
|
+
*/
|
|
3445
|
+
subscribeLiveMarkets(callback) {
|
|
3446
|
+
return this.subscribe("get-live-markets", {}, "markets_changed", callback);
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Subscribe to change events for a single market.
|
|
3450
|
+
* @param slug - The market slug
|
|
3451
|
+
* @returns An unsubscribe function
|
|
3452
|
+
*/
|
|
3453
|
+
subscribeMarket(slug, callback) {
|
|
3454
|
+
return this.subscribe("get-market", { slug }, "market_changed", callback);
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Subscribe to full orderbook snapshots (~5s interval on changes).
|
|
3458
|
+
* @param slug - The market slug
|
|
3459
|
+
* @returns An unsubscribe function
|
|
3460
|
+
*/
|
|
3461
|
+
subscribeOrderbook(slug, callback) {
|
|
3462
|
+
return this.subscribe("get-orderbook", { slug }, "orderbook_changed", callback);
|
|
3463
|
+
}
|
|
3464
|
+
/**
|
|
3465
|
+
* Subscribe to wallet order updates.
|
|
3466
|
+
* @param wallet - The wallet address
|
|
3467
|
+
* @returns An unsubscribe function
|
|
3468
|
+
*/
|
|
3469
|
+
subscribeWalletOrders(wallet, callback) {
|
|
3470
|
+
return this.subscribe("get-wallet-orders", { wallet }, "wallet_orders_changed", callback);
|
|
3471
|
+
}
|
|
3472
|
+
// ============================================
|
|
3473
|
+
// Control Methods
|
|
3474
|
+
// ============================================
|
|
3475
|
+
/** Query the server for the list of active subscriptions on this connection */
|
|
3476
|
+
listSubscriptions() {
|
|
3477
|
+
return this.sendRequest({ method: "LIST_SUBSCRIPTIONS" });
|
|
3478
|
+
}
|
|
3479
|
+
/** Query a server property (e.g. "heartbeat", "limits") */
|
|
3480
|
+
getProperty(property) {
|
|
3481
|
+
return this.sendRequest({ method: "GET_PROPERTY", params: [property] });
|
|
3482
|
+
}
|
|
3483
|
+
// ============================================
|
|
3484
|
+
// Lifecycle
|
|
3485
|
+
// ============================================
|
|
3486
|
+
/** Open the WebSocket connection. Called automatically on first subscribe. */
|
|
3487
|
+
connect() {
|
|
3488
|
+
if (this.connectPromise) return this.connectPromise;
|
|
3489
|
+
this.connectPromise = this.doConnect();
|
|
3490
|
+
return this.connectPromise;
|
|
3491
|
+
}
|
|
3492
|
+
/** Close the connection and clean up all resources */
|
|
3493
|
+
close() {
|
|
3494
|
+
this.intentionallyClosed = true;
|
|
3495
|
+
this.clearTimers();
|
|
3496
|
+
this.subscriptions.clear();
|
|
3497
|
+
for (const [, req] of this.pendingRequests) {
|
|
3498
|
+
clearTimeout(req.timer);
|
|
3499
|
+
req.reject(new Error("WebSocket closed"));
|
|
3500
|
+
}
|
|
3501
|
+
this.pendingRequests.clear();
|
|
3502
|
+
if (this.ws) {
|
|
3503
|
+
this.ws.close();
|
|
3504
|
+
this.ws = null;
|
|
3505
|
+
}
|
|
3506
|
+
this.connectPromise = null;
|
|
3507
|
+
}
|
|
3508
|
+
// ============================================
|
|
3509
|
+
// Internal
|
|
3510
|
+
// ============================================
|
|
3511
|
+
buildStreamKey(stream, params) {
|
|
3512
|
+
const parts = [stream, ...Object.entries(params).sort().map(([k, v]) => `${k}=${v}`)];
|
|
3513
|
+
return parts.join("&");
|
|
3514
|
+
}
|
|
3515
|
+
buildQueryString() {
|
|
3516
|
+
const subs = [...this.subscriptions.values()];
|
|
3517
|
+
if (subs.length === 0) return "";
|
|
3518
|
+
const first = subs[0];
|
|
3519
|
+
const params = new URLSearchParams({ stream: first.stream, ...first.params });
|
|
3520
|
+
return "?" + params.toString();
|
|
3521
|
+
}
|
|
3522
|
+
subscribe(stream, params, eventType, callback) {
|
|
3523
|
+
const key = this.buildStreamKey(stream, params);
|
|
3524
|
+
this.subscriptions.set(key, { stream, params, callback, eventType });
|
|
3525
|
+
if (this.connected) {
|
|
3526
|
+
this.sendSubscribe(stream, params);
|
|
3527
|
+
} else {
|
|
3528
|
+
this.connect();
|
|
3529
|
+
}
|
|
3530
|
+
return () => {
|
|
3531
|
+
this.subscriptions.delete(key);
|
|
3532
|
+
this.lastOrderbookVersionBySubscription.delete(key);
|
|
3533
|
+
if (this.connected) {
|
|
3534
|
+
this.sendUnsubscribe(stream, params);
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
async doConnect() {
|
|
3539
|
+
this.intentionallyClosed = false;
|
|
3540
|
+
return new Promise((resolve, reject) => {
|
|
3541
|
+
const qs = this.buildQueryString();
|
|
3542
|
+
const ws = new this.WebSocketImpl(this.url + qs);
|
|
3543
|
+
ws.onopen = () => {
|
|
3544
|
+
this.ws = ws;
|
|
3545
|
+
this.reconnectAttempts = 0;
|
|
3546
|
+
this.startHeartbeat();
|
|
3547
|
+
const subs = [...this.subscriptions.values()];
|
|
3548
|
+
for (const sub of subs) {
|
|
3549
|
+
this.sendSubscribe(sub.stream, sub.params);
|
|
3550
|
+
}
|
|
3551
|
+
resolve();
|
|
3552
|
+
};
|
|
3553
|
+
ws.onmessage = (event) => {
|
|
3554
|
+
this.handleMessage(event.data);
|
|
3555
|
+
};
|
|
3556
|
+
ws.onclose = () => {
|
|
3557
|
+
this.ws = null;
|
|
3558
|
+
this.connectPromise = null;
|
|
3559
|
+
this.stopHeartbeat();
|
|
3560
|
+
if (!this.intentionallyClosed) {
|
|
3561
|
+
this.scheduleReconnect();
|
|
3562
|
+
}
|
|
3563
|
+
};
|
|
3564
|
+
ws.onerror = (err) => {
|
|
3565
|
+
if (!this.ws) {
|
|
3566
|
+
reject(new Error("WebSocket connection failed"));
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
handleMessage(raw) {
|
|
3572
|
+
let msg;
|
|
3573
|
+
try {
|
|
3574
|
+
msg = JSON.parse(raw);
|
|
3575
|
+
} catch {
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
if (msg.type === "ping") {
|
|
3579
|
+
this.send({ method: "PONG" });
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
const responseId = typeof msg.id === "string" ? msg.id : typeof msg.requestId === "string" ? msg.requestId : null;
|
|
3583
|
+
if (responseId && this.pendingRequests.has(responseId)) {
|
|
3584
|
+
const req = this.pendingRequests.get(responseId);
|
|
3585
|
+
this.pendingRequests.delete(responseId);
|
|
3586
|
+
clearTimeout(req.timer);
|
|
3587
|
+
req.resolve(msg);
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
const eventType = msg.type;
|
|
3591
|
+
if (!eventType) return;
|
|
3592
|
+
for (const [key, sub] of this.subscriptions.entries()) {
|
|
3593
|
+
if (!this.matchesSubscriptionMessage(sub, msg)) {
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
if (!this.shouldDispatchSubscriptionMessage(key, sub, msg)) {
|
|
3597
|
+
continue;
|
|
3598
|
+
}
|
|
3599
|
+
try {
|
|
3600
|
+
sub.callback(msg);
|
|
3601
|
+
} catch {
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
matchesSubscriptionMessage(sub, msg) {
|
|
3606
|
+
if (sub.eventType !== msg.type) return false;
|
|
3607
|
+
if (msg.type === "orderbook_changed") {
|
|
3608
|
+
const messageMarketId = typeof msg.marketId === "string" ? msg.marketId : "";
|
|
3609
|
+
const messageSlug = typeof msg.slug === "string" ? msg.slug : "";
|
|
3610
|
+
const subscriptionMarketId = typeof sub.params.marketId === "string" ? sub.params.marketId : "";
|
|
3611
|
+
const subscriptionSlug = typeof sub.params.slug === "string" ? sub.params.slug : "";
|
|
3612
|
+
if (subscriptionMarketId) return subscriptionMarketId === messageMarketId;
|
|
3613
|
+
if (subscriptionSlug && messageSlug) return subscriptionSlug === messageSlug;
|
|
3614
|
+
if (subscriptionSlug || subscriptionMarketId) return false;
|
|
3615
|
+
}
|
|
3616
|
+
if (msg.type === "wallet_orders_changed") {
|
|
3617
|
+
const messageWallet = typeof msg.wallet === "string" ? msg.wallet : "";
|
|
3618
|
+
const subscriptionWallet = typeof sub.params.wallet === "string" ? sub.params.wallet : "";
|
|
3619
|
+
if (subscriptionWallet) return subscriptionWallet === messageWallet;
|
|
3620
|
+
return false;
|
|
3621
|
+
}
|
|
3622
|
+
return true;
|
|
3623
|
+
}
|
|
3624
|
+
shouldDispatchSubscriptionMessage(key, sub, msg) {
|
|
3625
|
+
if (sub.eventType !== "orderbook_changed") {
|
|
3626
|
+
return true;
|
|
3627
|
+
}
|
|
3628
|
+
const version = Number(msg.version ?? 0);
|
|
3629
|
+
if (!Number.isFinite(version)) {
|
|
3630
|
+
return true;
|
|
3631
|
+
}
|
|
3632
|
+
const lastVersion = this.lastOrderbookVersionBySubscription.get(key) ?? 0;
|
|
3633
|
+
if (version < lastVersion) {
|
|
3634
|
+
return false;
|
|
3635
|
+
}
|
|
3636
|
+
this.lastOrderbookVersionBySubscription.set(key, version);
|
|
3637
|
+
return true;
|
|
3638
|
+
}
|
|
3639
|
+
sendSubscribe(stream, params) {
|
|
3640
|
+
this.send({ method: "SUBSCRIBE", params: [{ stream, ...params }] });
|
|
3641
|
+
}
|
|
3642
|
+
sendUnsubscribe(stream, params) {
|
|
3643
|
+
this.send({ method: "UNSUBSCRIBE", params: [{ stream, ...params }] });
|
|
3644
|
+
}
|
|
3645
|
+
sendRequest(payload, timeoutMs = 1e4) {
|
|
3646
|
+
const requestId = crypto.randomUUID();
|
|
3647
|
+
return new Promise((resolve, reject) => {
|
|
3648
|
+
const timer = setTimeout(() => {
|
|
3649
|
+
this.pendingRequests.delete(requestId);
|
|
3650
|
+
reject(new Error("Request timed out"));
|
|
3651
|
+
}, timeoutMs);
|
|
3652
|
+
this.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
3653
|
+
if (!this.connected) {
|
|
3654
|
+
this.connect().then(() => {
|
|
3655
|
+
this.send({ ...payload, id: requestId });
|
|
3656
|
+
}).catch(reject);
|
|
3657
|
+
} else {
|
|
3658
|
+
this.send({ ...payload, id: requestId });
|
|
3659
|
+
}
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
send(data) {
|
|
3663
|
+
if (this.ws?.readyState === WS_OPEN) {
|
|
3664
|
+
this.ws.send(JSON.stringify(data));
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
// ============================================
|
|
3668
|
+
// Heartbeat
|
|
3669
|
+
// ============================================
|
|
3670
|
+
startHeartbeat() {
|
|
3671
|
+
this.stopHeartbeat();
|
|
3672
|
+
this.heartbeatTimer = setInterval(() => {
|
|
3673
|
+
this.send({ method: "PING" });
|
|
3674
|
+
}, this.heartbeatIntervalMs);
|
|
3675
|
+
}
|
|
3676
|
+
stopHeartbeat() {
|
|
3677
|
+
if (this.heartbeatTimer) {
|
|
3678
|
+
clearInterval(this.heartbeatTimer);
|
|
3679
|
+
this.heartbeatTimer = null;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
// ============================================
|
|
3683
|
+
// Reconnect
|
|
3684
|
+
// ============================================
|
|
3685
|
+
scheduleReconnect() {
|
|
3686
|
+
if (!this.reconnectEnabled) return;
|
|
3687
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
|
3688
|
+
const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
|
|
3689
|
+
this.reconnectAttempts++;
|
|
3690
|
+
this.reconnectTimer = setTimeout(() => {
|
|
3691
|
+
this.reconnectTimer = null;
|
|
3692
|
+
this.connect().catch(() => {
|
|
3693
|
+
});
|
|
3694
|
+
}, delay);
|
|
3695
|
+
}
|
|
3696
|
+
clearTimers() {
|
|
3697
|
+
this.stopHeartbeat();
|
|
3698
|
+
if (this.reconnectTimer) {
|
|
3699
|
+
clearTimeout(this.reconnectTimer);
|
|
3700
|
+
this.reconnectTimer = null;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
|
|
3369
3705
|
exports.AlphaClient = AlphaClient;
|
|
3706
|
+
exports.AlphaWebSocket = AlphaWebSocket;
|
|
3370
3707
|
exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
|
|
3371
3708
|
exports.DEFAULT_MARKET_CREATOR_ADDRESS = DEFAULT_MARKET_CREATOR_ADDRESS;
|
|
3709
|
+
exports.DEFAULT_WSS_BASE_URL = DEFAULT_WSS_BASE_URL;
|
|
3372
3710
|
exports.calculateFee = calculateFee;
|
|
3373
3711
|
exports.calculateFeeFromTotal = calculateFeeFromTotal;
|
|
3374
3712
|
exports.calculateMatchingOrders = calculateMatchingOrders;
|