@alpha-arcade/sdk 0.3.0 → 0.4.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 +123 -0
- package/dist/index.cjs +401 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -1
- package/dist/index.d.ts +195 -1
- package/dist/index.js +400 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -337,6 +337,129 @@ for (const m of rewardMarkets) {
|
|
|
337
337
|
|
|
338
338
|
---
|
|
339
339
|
|
|
340
|
+
### WebSocket Streams
|
|
341
|
+
|
|
342
|
+
Real-time data streams via WebSocket. No API key or auth required. Replaces polling with push-based updates.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { AlphaWebSocket } from '@alpha-arcade/sdk';
|
|
346
|
+
|
|
347
|
+
// Node.js 22+ and browsers — native WebSocket, nothing extra needed
|
|
348
|
+
const ws = new AlphaWebSocket();
|
|
349
|
+
|
|
350
|
+
// Node.js < 22 — install `ws` and pass it in:
|
|
351
|
+
// npm install ws
|
|
352
|
+
import WebSocket from 'ws';
|
|
353
|
+
const ws = new AlphaWebSocket({ WebSocket });
|
|
354
|
+
|
|
355
|
+
// Subscribe to orderbook updates (~5s snapshots)
|
|
356
|
+
const unsub = ws.subscribeOrderbook('will-btc-hit-100k', (event) => {
|
|
357
|
+
console.log('Orderbook:', event.orderbook);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Unsubscribe when done
|
|
361
|
+
unsub();
|
|
362
|
+
|
|
363
|
+
// Close the connection
|
|
364
|
+
ws.close();
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### `subscribeLiveMarkets(callback)`
|
|
368
|
+
|
|
369
|
+
Receive incremental diffs whenever market probabilities change.
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
ws.subscribeLiveMarkets((event) => {
|
|
373
|
+
console.log('Markets changed at', event.ts, event);
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### `subscribeMarket(slug, callback)`
|
|
378
|
+
|
|
379
|
+
Receive change events for a single market. Uses the market **slug** (not `marketAppId`) — see note on `subscribeOrderbook` below.
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
ws.subscribeMarket('will-btc-hit-100k', (event) => {
|
|
383
|
+
console.log('Market update:', event);
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### `subscribeOrderbook(slug, callback)`
|
|
388
|
+
|
|
389
|
+
Receive full orderbook snapshots on every change (~5s interval). Replaces on-chain polling.
|
|
390
|
+
|
|
391
|
+
**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()`.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
ws.subscribeOrderbook('will-btc-hit-100k', (event) => {
|
|
395
|
+
// Top-level bids/asks use decimal prices (cents)
|
|
396
|
+
// Nested yes/no use raw microunit prices with escrowAppId and owner
|
|
397
|
+
for (const [appId, book] of Object.entries(event.orderbook)) {
|
|
398
|
+
console.log(`App ${appId}: spread=${book.spread}`);
|
|
399
|
+
console.log(' Bids:', book.bids);
|
|
400
|
+
console.log(' Yes bids:', book.yes.bids);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### `subscribeWalletOrders(wallet, callback)`
|
|
406
|
+
|
|
407
|
+
Receive updates when orders for a wallet are created or modified.
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
ws.subscribeWalletOrders('MMU6X...', (event) => {
|
|
411
|
+
console.log('Wallet orders changed:', event);
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
#### Unsubscribing
|
|
416
|
+
|
|
417
|
+
Each `subscribe*` method returns an unsubscribe function. Call it to stop receiving events for that stream:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
const unsub = ws.subscribeOrderbook('my-market', (event) => { /* ... */ });
|
|
421
|
+
|
|
422
|
+
// Later, stop listening
|
|
423
|
+
unsub();
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### Control Methods
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// List active subscriptions on this connection
|
|
430
|
+
const subs = await ws.listSubscriptions();
|
|
431
|
+
|
|
432
|
+
// Query server properties
|
|
433
|
+
const props = await ws.getProperty('heartbeat');
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
#### Configuration
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
import WebSocket from 'ws'; // Only needed on Node.js < 22
|
|
440
|
+
|
|
441
|
+
const ws = new AlphaWebSocket({
|
|
442
|
+
WebSocket, // Pass `ws` on Node.js < 22 (not needed in browsers or Node 22+)
|
|
443
|
+
url: 'wss://custom-endpoint.example.com', // Override default URL
|
|
444
|
+
reconnect: true, // Auto-reconnect (default: true)
|
|
445
|
+
maxReconnectAttempts: 10, // Give up after 10 retries (default: Infinity)
|
|
446
|
+
heartbeatIntervalMs: 60_000, // Ping interval in ms (default: 60000)
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
#### Connection Details
|
|
451
|
+
|
|
452
|
+
| Setting | Value |
|
|
453
|
+
|---------|-------|
|
|
454
|
+
| Heartbeat | 60s (auto-handled) |
|
|
455
|
+
| Idle timeout | 180s |
|
|
456
|
+
| Rate limit | 5 messages/sec/connection |
|
|
457
|
+
| Reconnect | Exponential backoff (1s → 30s max) |
|
|
458
|
+
|
|
459
|
+
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.
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
340
463
|
### Utility Functions
|
|
341
464
|
|
|
342
465
|
These are exported for advanced users:
|
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
|
|
@@ -2514,6 +2515,89 @@ var proposeMatch = async (config, params) => {
|
|
|
2514
2515
|
confirmedRound: Number(result.confirmedRound)
|
|
2515
2516
|
};
|
|
2516
2517
|
};
|
|
2518
|
+
var processMatch = async (config, params) => {
|
|
2519
|
+
const { algodClient, signer, activeAddress, usdcAssetId } = config;
|
|
2520
|
+
const { marketAppId, makerEscrowAppId, takerEscrowAppId } = params;
|
|
2521
|
+
const [marketState, makerAppInfo, takerAppInfo] = await Promise.all([
|
|
2522
|
+
getMarketGlobalState(algodClient, marketAppId),
|
|
2523
|
+
algodClient.getApplicationByID(makerEscrowAppId).do(),
|
|
2524
|
+
algodClient.getApplicationByID(takerEscrowAppId).do()
|
|
2525
|
+
]);
|
|
2526
|
+
const marketFeeAddress = marketState.fee_address;
|
|
2527
|
+
const yesAssetId = marketState.yes_asset_id;
|
|
2528
|
+
const noAssetId = marketState.no_asset_id;
|
|
2529
|
+
const makerState = decodeGlobalState(
|
|
2530
|
+
makerAppInfo.params?.globalState ?? makerAppInfo.params?.["global-state"] ?? []
|
|
2531
|
+
);
|
|
2532
|
+
const takerState = decodeGlobalState(
|
|
2533
|
+
takerAppInfo.params?.globalState ?? takerAppInfo.params?.["global-state"] ?? []
|
|
2534
|
+
);
|
|
2535
|
+
const makerOwner = makerState.owner ?? "";
|
|
2536
|
+
const takerOwner = takerState.owner ?? "";
|
|
2537
|
+
const makerRemaining = (makerState.quantity ?? 0) - (makerState.quantity_filled ?? 0);
|
|
2538
|
+
const takerRemaining = (takerState.quantity ?? 0) - (takerState.quantity_filled ?? 0);
|
|
2539
|
+
const matchedQuantity = Math.min(makerRemaining, takerRemaining);
|
|
2540
|
+
if (matchedQuantity <= 0) {
|
|
2541
|
+
throw new Error("processMatch: no quantity left to match on maker or taker escrow.");
|
|
2542
|
+
}
|
|
2543
|
+
const signerAccount = { signer, addr: activeAddress };
|
|
2544
|
+
const makerEscrowClient = new EscrowAppClient(
|
|
2545
|
+
{ resolveBy: "id", id: makerEscrowAppId, sender: signerAccount },
|
|
2546
|
+
algodClient
|
|
2547
|
+
);
|
|
2548
|
+
const takerEscrowClient = new EscrowAppClient(
|
|
2549
|
+
{ resolveBy: "id", id: takerEscrowAppId, sender: signerAccount },
|
|
2550
|
+
algodClient
|
|
2551
|
+
);
|
|
2552
|
+
const marketClient = new MarketAppClient(
|
|
2553
|
+
{ resolveBy: "id", id: marketAppId, sender: signerAccount },
|
|
2554
|
+
algodClient
|
|
2555
|
+
);
|
|
2556
|
+
const atc = new algosdk2.AtomicTransactionComposer();
|
|
2557
|
+
const assets = [usdcAssetId, yesAssetId, noAssetId];
|
|
2558
|
+
const sendParamsWithFee = { skipSending: true, fee: algokit4__namespace.microAlgos(3e3) };
|
|
2559
|
+
const sendParamsNoop = { skipSending: true, fee: algokit4__namespace.microAlgos(1e3) };
|
|
2560
|
+
const matchMakerTxn = await makerEscrowClient.matchMaker(
|
|
2561
|
+
{ taker: takerEscrowAppId, matchQuantity: matchedQuantity },
|
|
2562
|
+
{
|
|
2563
|
+
assets,
|
|
2564
|
+
apps: [marketAppId],
|
|
2565
|
+
accounts: [takerOwner, marketFeeAddress],
|
|
2566
|
+
sendParams: sendParamsWithFee
|
|
2567
|
+
}
|
|
2568
|
+
);
|
|
2569
|
+
atc.addTransaction({ txn: matchMakerTxn.transaction, signer });
|
|
2570
|
+
const matchTakerTxn = await takerEscrowClient.matchTaker(
|
|
2571
|
+
{ maker: makerEscrowAppId },
|
|
2572
|
+
{
|
|
2573
|
+
assets,
|
|
2574
|
+
apps: [marketAppId],
|
|
2575
|
+
accounts: [makerOwner, marketFeeAddress],
|
|
2576
|
+
sendParams: sendParamsWithFee
|
|
2577
|
+
}
|
|
2578
|
+
);
|
|
2579
|
+
atc.addTransaction({ txn: matchTakerTxn.transaction, signer });
|
|
2580
|
+
const processMatchTxn = await marketClient.processPotentialMatch(
|
|
2581
|
+
{ maker: makerEscrowAppId, taker: takerEscrowAppId },
|
|
2582
|
+
{
|
|
2583
|
+
assets,
|
|
2584
|
+
accounts: [makerOwner, takerOwner, marketFeeAddress],
|
|
2585
|
+
sendParams: sendParamsNoop
|
|
2586
|
+
}
|
|
2587
|
+
);
|
|
2588
|
+
atc.addTransaction({ txn: processMatchTxn.transaction, signer });
|
|
2589
|
+
const doNoopTxn = await marketClient.doNoop(
|
|
2590
|
+
{ callNumber: 1 },
|
|
2591
|
+
{ sendParams: sendParamsNoop }
|
|
2592
|
+
);
|
|
2593
|
+
atc.addTransaction({ txn: doNoopTxn.transaction, signer });
|
|
2594
|
+
const result = await atc.execute(algodClient, 4);
|
|
2595
|
+
return {
|
|
2596
|
+
success: true,
|
|
2597
|
+
txIds: result.txIDs,
|
|
2598
|
+
confirmedRound: Number(result.confirmedRound)
|
|
2599
|
+
};
|
|
2600
|
+
};
|
|
2517
2601
|
var amendOrder = async (config, params) => {
|
|
2518
2602
|
const { algodClient, signer, activeAddress, usdcAssetId } = config;
|
|
2519
2603
|
const { marketAppId, escrowAppId, price, quantity, slippage = 0 } = params;
|
|
@@ -2570,10 +2654,47 @@ var amendOrder = async (config, params) => {
|
|
|
2570
2654
|
);
|
|
2571
2655
|
atc.addTransaction({ txn: amendCall.transaction, signer });
|
|
2572
2656
|
const result = await atc.execute(algodClient, 4);
|
|
2657
|
+
const allTxIds = [...result.txIDs];
|
|
2658
|
+
let lastConfirmedRound = Number(result.confirmedRound);
|
|
2659
|
+
const orderbook = await getOrderbook(config, marketAppId);
|
|
2660
|
+
let matchingOrders = calculateMatchingOrders(orderbook, isBuy, position === 1, quantity, price, slippage);
|
|
2661
|
+
matchingOrders = matchingOrders.filter((m) => m.escrowAppId !== escrowAppId);
|
|
2662
|
+
if (matchingOrders.length > 0) {
|
|
2663
|
+
let quantityLeft = quantity;
|
|
2664
|
+
const matchedQuantities = [];
|
|
2665
|
+
const matchedPrices = [];
|
|
2666
|
+
for (const m of matchingOrders) {
|
|
2667
|
+
if (quantityLeft <= 0) break;
|
|
2668
|
+
try {
|
|
2669
|
+
const matchResult = await processMatch(config, {
|
|
2670
|
+
marketAppId,
|
|
2671
|
+
makerEscrowAppId: m.escrowAppId,
|
|
2672
|
+
takerEscrowAppId: escrowAppId
|
|
2673
|
+
});
|
|
2674
|
+
allTxIds.push(...matchResult.txIds);
|
|
2675
|
+
lastConfirmedRound = matchResult.confirmedRound;
|
|
2676
|
+
const q = Math.min(m.quantity, quantityLeft);
|
|
2677
|
+
matchedQuantities.push(q);
|
|
2678
|
+
matchedPrices.push((m.price ?? price) * q);
|
|
2679
|
+
quantityLeft -= q;
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
console.log(`Error matching order: ${JSON.stringify(err)}`);
|
|
2682
|
+
break;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
const totalMatchedQuantity = matchedQuantities.reduce((a, b) => a + b, 0);
|
|
2686
|
+
const matchedPrice = totalMatchedQuantity > 0 ? Math.round(matchedPrices.reduce((a, b) => a + b, 0) / totalMatchedQuantity) : void 0;
|
|
2687
|
+
return {
|
|
2688
|
+
success: true,
|
|
2689
|
+
txIds: allTxIds,
|
|
2690
|
+
confirmedRound: lastConfirmedRound,
|
|
2691
|
+
...totalMatchedQuantity > 0 && { matchedQuantity: totalMatchedQuantity, matchedPrice }
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2573
2694
|
return {
|
|
2574
2695
|
success: true,
|
|
2575
|
-
txIds:
|
|
2576
|
-
confirmedRound:
|
|
2696
|
+
txIds: allTxIds,
|
|
2697
|
+
confirmedRound: lastConfirmedRound
|
|
2577
2698
|
};
|
|
2578
2699
|
};
|
|
2579
2700
|
var splitShares = async (config, params) => {
|
|
@@ -3056,6 +3177,19 @@ var AlphaClient = class {
|
|
|
3056
3177
|
async proposeMatch(params) {
|
|
3057
3178
|
return proposeMatch(this.config, params);
|
|
3058
3179
|
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Matches two existing limit orders (no create-escrow in the group).
|
|
3182
|
+
*
|
|
3183
|
+
* Calls the market app's process_potential_match(maker, taker). Use this
|
|
3184
|
+
* after amending an order: the amended order is the taker (pays the fee),
|
|
3185
|
+
* the counterparty is the maker.
|
|
3186
|
+
*
|
|
3187
|
+
* @param params - Process match params (marketAppId, makerEscrowAppId, takerEscrowAppId)
|
|
3188
|
+
* @returns Whether the match succeeded
|
|
3189
|
+
*/
|
|
3190
|
+
async processMatch(params) {
|
|
3191
|
+
return processMatch(this.config, params);
|
|
3192
|
+
}
|
|
3059
3193
|
/**
|
|
3060
3194
|
* Amends (edits) an existing unfilled order in-place.
|
|
3061
3195
|
*
|
|
@@ -3233,9 +3367,274 @@ var AlphaClient = class {
|
|
|
3233
3367
|
}
|
|
3234
3368
|
};
|
|
3235
3369
|
|
|
3370
|
+
// src/websocket.ts
|
|
3371
|
+
var WS_OPEN = 1;
|
|
3372
|
+
var resolveWebSocket = (provided) => {
|
|
3373
|
+
if (provided) return provided;
|
|
3374
|
+
if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
3375
|
+
return globalThis.WebSocket;
|
|
3376
|
+
}
|
|
3377
|
+
throw new Error(
|
|
3378
|
+
'No WebSocket implementation found. On Node.js < 22, install the "ws" package and pass it: new AlphaWebSocket({ WebSocket: require("ws") })'
|
|
3379
|
+
);
|
|
3380
|
+
};
|
|
3381
|
+
var AlphaWebSocket = class {
|
|
3382
|
+
url;
|
|
3383
|
+
reconnectEnabled;
|
|
3384
|
+
maxReconnectAttempts;
|
|
3385
|
+
heartbeatIntervalMs;
|
|
3386
|
+
WebSocketImpl;
|
|
3387
|
+
ws = null;
|
|
3388
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
3389
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
3390
|
+
heartbeatTimer = null;
|
|
3391
|
+
reconnectTimer = null;
|
|
3392
|
+
reconnectAttempts = 0;
|
|
3393
|
+
intentionallyClosed = false;
|
|
3394
|
+
connectPromise = null;
|
|
3395
|
+
constructor(config) {
|
|
3396
|
+
this.url = config?.url ?? DEFAULT_WSS_BASE_URL;
|
|
3397
|
+
this.reconnectEnabled = config?.reconnect ?? true;
|
|
3398
|
+
this.maxReconnectAttempts = config?.maxReconnectAttempts ?? Infinity;
|
|
3399
|
+
this.heartbeatIntervalMs = config?.heartbeatIntervalMs ?? 6e4;
|
|
3400
|
+
this.WebSocketImpl = resolveWebSocket(config?.WebSocket);
|
|
3401
|
+
}
|
|
3402
|
+
/** Whether the WebSocket is currently open and connected */
|
|
3403
|
+
get connected() {
|
|
3404
|
+
return this.ws?.readyState === WS_OPEN;
|
|
3405
|
+
}
|
|
3406
|
+
// ============================================
|
|
3407
|
+
// Subscribe Methods
|
|
3408
|
+
// ============================================
|
|
3409
|
+
/**
|
|
3410
|
+
* Subscribe to live market probability updates (incremental diffs).
|
|
3411
|
+
* @returns An unsubscribe function
|
|
3412
|
+
*/
|
|
3413
|
+
subscribeLiveMarkets(callback) {
|
|
3414
|
+
return this.subscribe("get-live-markets", {}, "markets_changed", callback);
|
|
3415
|
+
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Subscribe to change events for a single market.
|
|
3418
|
+
* @param slug - The market slug
|
|
3419
|
+
* @returns An unsubscribe function
|
|
3420
|
+
*/
|
|
3421
|
+
subscribeMarket(slug, callback) {
|
|
3422
|
+
return this.subscribe("get-market", { slug }, "market_changed", callback);
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Subscribe to full orderbook snapshots (~5s interval on changes).
|
|
3426
|
+
* @param slug - The market slug
|
|
3427
|
+
* @returns An unsubscribe function
|
|
3428
|
+
*/
|
|
3429
|
+
subscribeOrderbook(slug, callback) {
|
|
3430
|
+
return this.subscribe("get-orderbook", { slug }, "orderbook_changed", callback);
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Subscribe to wallet order updates.
|
|
3434
|
+
* @param wallet - The wallet address
|
|
3435
|
+
* @returns An unsubscribe function
|
|
3436
|
+
*/
|
|
3437
|
+
subscribeWalletOrders(wallet, callback) {
|
|
3438
|
+
return this.subscribe("get-wallet-orders", { wallet }, "wallet_orders_changed", callback);
|
|
3439
|
+
}
|
|
3440
|
+
// ============================================
|
|
3441
|
+
// Control Methods
|
|
3442
|
+
// ============================================
|
|
3443
|
+
/** Query the server for the list of active subscriptions on this connection */
|
|
3444
|
+
listSubscriptions() {
|
|
3445
|
+
return this.sendRequest({ method: "LIST_SUBSCRIPTIONS" });
|
|
3446
|
+
}
|
|
3447
|
+
/** Query a server property (e.g. "heartbeat", "limits") */
|
|
3448
|
+
getProperty(property) {
|
|
3449
|
+
return this.sendRequest({ method: "GET_PROPERTY", property });
|
|
3450
|
+
}
|
|
3451
|
+
// ============================================
|
|
3452
|
+
// Lifecycle
|
|
3453
|
+
// ============================================
|
|
3454
|
+
/** Open the WebSocket connection. Called automatically on first subscribe. */
|
|
3455
|
+
connect() {
|
|
3456
|
+
if (this.connectPromise) return this.connectPromise;
|
|
3457
|
+
this.connectPromise = this.doConnect();
|
|
3458
|
+
return this.connectPromise;
|
|
3459
|
+
}
|
|
3460
|
+
/** Close the connection and clean up all resources */
|
|
3461
|
+
close() {
|
|
3462
|
+
this.intentionallyClosed = true;
|
|
3463
|
+
this.clearTimers();
|
|
3464
|
+
this.subscriptions.clear();
|
|
3465
|
+
for (const [, req] of this.pendingRequests) {
|
|
3466
|
+
clearTimeout(req.timer);
|
|
3467
|
+
req.reject(new Error("WebSocket closed"));
|
|
3468
|
+
}
|
|
3469
|
+
this.pendingRequests.clear();
|
|
3470
|
+
if (this.ws) {
|
|
3471
|
+
this.ws.close();
|
|
3472
|
+
this.ws = null;
|
|
3473
|
+
}
|
|
3474
|
+
this.connectPromise = null;
|
|
3475
|
+
}
|
|
3476
|
+
// ============================================
|
|
3477
|
+
// Internal
|
|
3478
|
+
// ============================================
|
|
3479
|
+
buildStreamKey(stream, params) {
|
|
3480
|
+
const parts = [stream, ...Object.entries(params).sort().map(([k, v]) => `${k}=${v}`)];
|
|
3481
|
+
return parts.join("&");
|
|
3482
|
+
}
|
|
3483
|
+
buildQueryString() {
|
|
3484
|
+
const subs = [...this.subscriptions.values()];
|
|
3485
|
+
if (subs.length === 0) return "";
|
|
3486
|
+
const first = subs[0];
|
|
3487
|
+
const params = new URLSearchParams({ stream: first.stream, ...first.params });
|
|
3488
|
+
return "?" + params.toString();
|
|
3489
|
+
}
|
|
3490
|
+
subscribe(stream, params, eventType, callback) {
|
|
3491
|
+
const key = this.buildStreamKey(stream, params);
|
|
3492
|
+
this.subscriptions.set(key, { stream, params, callback, eventType });
|
|
3493
|
+
if (this.connected) {
|
|
3494
|
+
this.sendSubscribe(stream, params);
|
|
3495
|
+
} else {
|
|
3496
|
+
this.connect();
|
|
3497
|
+
}
|
|
3498
|
+
return () => {
|
|
3499
|
+
this.subscriptions.delete(key);
|
|
3500
|
+
if (this.connected) {
|
|
3501
|
+
this.sendUnsubscribe(stream, params);
|
|
3502
|
+
}
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
async doConnect() {
|
|
3506
|
+
this.intentionallyClosed = false;
|
|
3507
|
+
return new Promise((resolve, reject) => {
|
|
3508
|
+
const qs = this.buildQueryString();
|
|
3509
|
+
const ws = new this.WebSocketImpl(this.url + qs);
|
|
3510
|
+
ws.onopen = () => {
|
|
3511
|
+
this.ws = ws;
|
|
3512
|
+
this.reconnectAttempts = 0;
|
|
3513
|
+
this.startHeartbeat();
|
|
3514
|
+
const subs = [...this.subscriptions.values()];
|
|
3515
|
+
for (const sub of subs.slice(1)) {
|
|
3516
|
+
this.sendSubscribe(sub.stream, sub.params);
|
|
3517
|
+
}
|
|
3518
|
+
resolve();
|
|
3519
|
+
};
|
|
3520
|
+
ws.onmessage = (event) => {
|
|
3521
|
+
this.handleMessage(event.data);
|
|
3522
|
+
};
|
|
3523
|
+
ws.onclose = () => {
|
|
3524
|
+
this.ws = null;
|
|
3525
|
+
this.connectPromise = null;
|
|
3526
|
+
this.stopHeartbeat();
|
|
3527
|
+
if (!this.intentionallyClosed) {
|
|
3528
|
+
this.scheduleReconnect();
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
ws.onerror = (err) => {
|
|
3532
|
+
if (!this.ws) {
|
|
3533
|
+
reject(new Error("WebSocket connection failed"));
|
|
3534
|
+
}
|
|
3535
|
+
};
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
handleMessage(raw) {
|
|
3539
|
+
let msg;
|
|
3540
|
+
try {
|
|
3541
|
+
msg = JSON.parse(raw);
|
|
3542
|
+
} catch {
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
if (msg.type === "ping") {
|
|
3546
|
+
this.send({ method: "PONG" });
|
|
3547
|
+
return;
|
|
3548
|
+
}
|
|
3549
|
+
if (msg.requestId && this.pendingRequests.has(msg.requestId)) {
|
|
3550
|
+
const req = this.pendingRequests.get(msg.requestId);
|
|
3551
|
+
this.pendingRequests.delete(msg.requestId);
|
|
3552
|
+
clearTimeout(req.timer);
|
|
3553
|
+
req.resolve(msg);
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
const eventType = msg.type;
|
|
3557
|
+
if (!eventType) return;
|
|
3558
|
+
for (const sub of this.subscriptions.values()) {
|
|
3559
|
+
if (sub.eventType === eventType) {
|
|
3560
|
+
try {
|
|
3561
|
+
sub.callback(msg);
|
|
3562
|
+
} catch {
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
sendSubscribe(stream, params) {
|
|
3568
|
+
this.send({ method: "SUBSCRIBE", stream, ...params });
|
|
3569
|
+
}
|
|
3570
|
+
sendUnsubscribe(stream, params) {
|
|
3571
|
+
this.send({ method: "UNSUBSCRIBE", stream, ...params });
|
|
3572
|
+
}
|
|
3573
|
+
sendRequest(payload, timeoutMs = 1e4) {
|
|
3574
|
+
const requestId = crypto.randomUUID();
|
|
3575
|
+
return new Promise((resolve, reject) => {
|
|
3576
|
+
const timer = setTimeout(() => {
|
|
3577
|
+
this.pendingRequests.delete(requestId);
|
|
3578
|
+
reject(new Error("Request timed out"));
|
|
3579
|
+
}, timeoutMs);
|
|
3580
|
+
this.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
3581
|
+
if (!this.connected) {
|
|
3582
|
+
this.connect().then(() => {
|
|
3583
|
+
this.send({ ...payload, requestId });
|
|
3584
|
+
}).catch(reject);
|
|
3585
|
+
} else {
|
|
3586
|
+
this.send({ ...payload, requestId });
|
|
3587
|
+
}
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
send(data) {
|
|
3591
|
+
if (this.ws?.readyState === WS_OPEN) {
|
|
3592
|
+
this.ws.send(JSON.stringify(data));
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
// ============================================
|
|
3596
|
+
// Heartbeat
|
|
3597
|
+
// ============================================
|
|
3598
|
+
startHeartbeat() {
|
|
3599
|
+
this.stopHeartbeat();
|
|
3600
|
+
this.heartbeatTimer = setInterval(() => {
|
|
3601
|
+
this.send({ type: "ping" });
|
|
3602
|
+
}, this.heartbeatIntervalMs);
|
|
3603
|
+
}
|
|
3604
|
+
stopHeartbeat() {
|
|
3605
|
+
if (this.heartbeatTimer) {
|
|
3606
|
+
clearInterval(this.heartbeatTimer);
|
|
3607
|
+
this.heartbeatTimer = null;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
// ============================================
|
|
3611
|
+
// Reconnect
|
|
3612
|
+
// ============================================
|
|
3613
|
+
scheduleReconnect() {
|
|
3614
|
+
if (!this.reconnectEnabled) return;
|
|
3615
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
|
3616
|
+
const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
|
|
3617
|
+
this.reconnectAttempts++;
|
|
3618
|
+
this.reconnectTimer = setTimeout(() => {
|
|
3619
|
+
this.reconnectTimer = null;
|
|
3620
|
+
this.connect().catch(() => {
|
|
3621
|
+
});
|
|
3622
|
+
}, delay);
|
|
3623
|
+
}
|
|
3624
|
+
clearTimers() {
|
|
3625
|
+
this.stopHeartbeat();
|
|
3626
|
+
if (this.reconnectTimer) {
|
|
3627
|
+
clearTimeout(this.reconnectTimer);
|
|
3628
|
+
this.reconnectTimer = null;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
};
|
|
3632
|
+
|
|
3236
3633
|
exports.AlphaClient = AlphaClient;
|
|
3634
|
+
exports.AlphaWebSocket = AlphaWebSocket;
|
|
3237
3635
|
exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
|
|
3238
3636
|
exports.DEFAULT_MARKET_CREATOR_ADDRESS = DEFAULT_MARKET_CREATOR_ADDRESS;
|
|
3637
|
+
exports.DEFAULT_WSS_BASE_URL = DEFAULT_WSS_BASE_URL;
|
|
3239
3638
|
exports.calculateFee = calculateFee;
|
|
3240
3639
|
exports.calculateFeeFromTotal = calculateFeeFromTotal;
|
|
3241
3640
|
exports.calculateMatchingOrders = calculateMatchingOrders;
|