@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/dist/index.js CHANGED
@@ -2103,6 +2103,7 @@ var calculateMatchingOrders = (orderbook, isBuying, isYes, quantity, price, slip
2103
2103
 
2104
2104
  // src/constants.ts
2105
2105
  var DEFAULT_API_BASE_URL = "https://platform.alphaarcade.com/api";
2106
+ var DEFAULT_WSS_BASE_URL = "wss://wss.platform.alphaarcade.com";
2106
2107
  var DEFAULT_MARKET_CREATOR_ADDRESS = "5P5Y6HTWUNG2E3VXBQDZN3ENZD3JPAIR5PKT3LOYJAPAUKOLFD6KANYTRY";
2107
2108
 
2108
2109
  // src/modules/orderbook.ts
@@ -2492,6 +2493,89 @@ var proposeMatch = async (config, params) => {
2492
2493
  confirmedRound: Number(result.confirmedRound)
2493
2494
  };
2494
2495
  };
2496
+ var processMatch = async (config, params) => {
2497
+ const { algodClient, signer, activeAddress, usdcAssetId } = config;
2498
+ const { marketAppId, makerEscrowAppId, takerEscrowAppId } = params;
2499
+ const [marketState, makerAppInfo, takerAppInfo] = await Promise.all([
2500
+ getMarketGlobalState(algodClient, marketAppId),
2501
+ algodClient.getApplicationByID(makerEscrowAppId).do(),
2502
+ algodClient.getApplicationByID(takerEscrowAppId).do()
2503
+ ]);
2504
+ const marketFeeAddress = marketState.fee_address;
2505
+ const yesAssetId = marketState.yes_asset_id;
2506
+ const noAssetId = marketState.no_asset_id;
2507
+ const makerState = decodeGlobalState(
2508
+ makerAppInfo.params?.globalState ?? makerAppInfo.params?.["global-state"] ?? []
2509
+ );
2510
+ const takerState = decodeGlobalState(
2511
+ takerAppInfo.params?.globalState ?? takerAppInfo.params?.["global-state"] ?? []
2512
+ );
2513
+ const makerOwner = makerState.owner ?? "";
2514
+ const takerOwner = takerState.owner ?? "";
2515
+ const makerRemaining = (makerState.quantity ?? 0) - (makerState.quantity_filled ?? 0);
2516
+ const takerRemaining = (takerState.quantity ?? 0) - (takerState.quantity_filled ?? 0);
2517
+ const matchedQuantity = Math.min(makerRemaining, takerRemaining);
2518
+ if (matchedQuantity <= 0) {
2519
+ throw new Error("processMatch: no quantity left to match on maker or taker escrow.");
2520
+ }
2521
+ const signerAccount = { signer, addr: activeAddress };
2522
+ const makerEscrowClient = new EscrowAppClient(
2523
+ { resolveBy: "id", id: makerEscrowAppId, sender: signerAccount },
2524
+ algodClient
2525
+ );
2526
+ const takerEscrowClient = new EscrowAppClient(
2527
+ { resolveBy: "id", id: takerEscrowAppId, sender: signerAccount },
2528
+ algodClient
2529
+ );
2530
+ const marketClient = new MarketAppClient(
2531
+ { resolveBy: "id", id: marketAppId, sender: signerAccount },
2532
+ algodClient
2533
+ );
2534
+ const atc = new AtomicTransactionComposer();
2535
+ const assets = [usdcAssetId, yesAssetId, noAssetId];
2536
+ const sendParamsWithFee = { skipSending: true, fee: algokit4.microAlgos(3e3) };
2537
+ const sendParamsNoop = { skipSending: true, fee: algokit4.microAlgos(1e3) };
2538
+ const matchMakerTxn = await makerEscrowClient.matchMaker(
2539
+ { taker: takerEscrowAppId, matchQuantity: matchedQuantity },
2540
+ {
2541
+ assets,
2542
+ apps: [marketAppId],
2543
+ accounts: [takerOwner, marketFeeAddress],
2544
+ sendParams: sendParamsWithFee
2545
+ }
2546
+ );
2547
+ atc.addTransaction({ txn: matchMakerTxn.transaction, signer });
2548
+ const matchTakerTxn = await takerEscrowClient.matchTaker(
2549
+ { maker: makerEscrowAppId },
2550
+ {
2551
+ assets,
2552
+ apps: [marketAppId],
2553
+ accounts: [makerOwner, marketFeeAddress],
2554
+ sendParams: sendParamsWithFee
2555
+ }
2556
+ );
2557
+ atc.addTransaction({ txn: matchTakerTxn.transaction, signer });
2558
+ const processMatchTxn = await marketClient.processPotentialMatch(
2559
+ { maker: makerEscrowAppId, taker: takerEscrowAppId },
2560
+ {
2561
+ assets,
2562
+ accounts: [makerOwner, takerOwner, marketFeeAddress],
2563
+ sendParams: sendParamsNoop
2564
+ }
2565
+ );
2566
+ atc.addTransaction({ txn: processMatchTxn.transaction, signer });
2567
+ const doNoopTxn = await marketClient.doNoop(
2568
+ { callNumber: 1 },
2569
+ { sendParams: sendParamsNoop }
2570
+ );
2571
+ atc.addTransaction({ txn: doNoopTxn.transaction, signer });
2572
+ const result = await atc.execute(algodClient, 4);
2573
+ return {
2574
+ success: true,
2575
+ txIds: result.txIDs,
2576
+ confirmedRound: Number(result.confirmedRound)
2577
+ };
2578
+ };
2495
2579
  var amendOrder = async (config, params) => {
2496
2580
  const { algodClient, signer, activeAddress, usdcAssetId } = config;
2497
2581
  const { marketAppId, escrowAppId, price, quantity, slippage = 0 } = params;
@@ -2548,10 +2632,47 @@ var amendOrder = async (config, params) => {
2548
2632
  );
2549
2633
  atc.addTransaction({ txn: amendCall.transaction, signer });
2550
2634
  const result = await atc.execute(algodClient, 4);
2635
+ const allTxIds = [...result.txIDs];
2636
+ let lastConfirmedRound = Number(result.confirmedRound);
2637
+ const orderbook = await getOrderbook(config, marketAppId);
2638
+ let matchingOrders = calculateMatchingOrders(orderbook, isBuy, position === 1, quantity, price, slippage);
2639
+ matchingOrders = matchingOrders.filter((m) => m.escrowAppId !== escrowAppId);
2640
+ if (matchingOrders.length > 0) {
2641
+ let quantityLeft = quantity;
2642
+ const matchedQuantities = [];
2643
+ const matchedPrices = [];
2644
+ for (const m of matchingOrders) {
2645
+ if (quantityLeft <= 0) break;
2646
+ try {
2647
+ const matchResult = await processMatch(config, {
2648
+ marketAppId,
2649
+ makerEscrowAppId: m.escrowAppId,
2650
+ takerEscrowAppId: escrowAppId
2651
+ });
2652
+ allTxIds.push(...matchResult.txIds);
2653
+ lastConfirmedRound = matchResult.confirmedRound;
2654
+ const q = Math.min(m.quantity, quantityLeft);
2655
+ matchedQuantities.push(q);
2656
+ matchedPrices.push((m.price ?? price) * q);
2657
+ quantityLeft -= q;
2658
+ } catch (err) {
2659
+ console.log(`Error matching order: ${JSON.stringify(err)}`);
2660
+ break;
2661
+ }
2662
+ }
2663
+ const totalMatchedQuantity = matchedQuantities.reduce((a, b) => a + b, 0);
2664
+ const matchedPrice = totalMatchedQuantity > 0 ? Math.round(matchedPrices.reduce((a, b) => a + b, 0) / totalMatchedQuantity) : void 0;
2665
+ return {
2666
+ success: true,
2667
+ txIds: allTxIds,
2668
+ confirmedRound: lastConfirmedRound,
2669
+ ...totalMatchedQuantity > 0 && { matchedQuantity: totalMatchedQuantity, matchedPrice }
2670
+ };
2671
+ }
2551
2672
  return {
2552
2673
  success: true,
2553
- txIds: result.txIDs,
2554
- confirmedRound: Number(result.confirmedRound)
2674
+ txIds: allTxIds,
2675
+ confirmedRound: lastConfirmedRound
2555
2676
  };
2556
2677
  };
2557
2678
  var splitShares = async (config, params) => {
@@ -3034,6 +3155,19 @@ var AlphaClient = class {
3034
3155
  async proposeMatch(params) {
3035
3156
  return proposeMatch(this.config, params);
3036
3157
  }
3158
+ /**
3159
+ * Matches two existing limit orders (no create-escrow in the group).
3160
+ *
3161
+ * Calls the market app's process_potential_match(maker, taker). Use this
3162
+ * after amending an order: the amended order is the taker (pays the fee),
3163
+ * the counterparty is the maker.
3164
+ *
3165
+ * @param params - Process match params (marketAppId, makerEscrowAppId, takerEscrowAppId)
3166
+ * @returns Whether the match succeeded
3167
+ */
3168
+ async processMatch(params) {
3169
+ return processMatch(this.config, params);
3170
+ }
3037
3171
  /**
3038
3172
  * Amends (edits) an existing unfilled order in-place.
3039
3173
  *
@@ -3211,6 +3345,269 @@ var AlphaClient = class {
3211
3345
  }
3212
3346
  };
3213
3347
 
3214
- export { AlphaClient, DEFAULT_API_BASE_URL, DEFAULT_MARKET_CREATOR_ADDRESS, calculateFee, calculateFeeFromTotal, calculateMatchingOrders, checkAssetOptIn, decodeGlobalState, getEscrowGlobalState, getLiveMarketsFromApi, getMarketFromApi, getMarketGlobalState, getMarketOnChain, getMarketsOnChain };
3348
+ // src/websocket.ts
3349
+ var WS_OPEN = 1;
3350
+ var resolveWebSocket = (provided) => {
3351
+ if (provided) return provided;
3352
+ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
3353
+ return globalThis.WebSocket;
3354
+ }
3355
+ throw new Error(
3356
+ 'No WebSocket implementation found. On Node.js < 22, install the "ws" package and pass it: new AlphaWebSocket({ WebSocket: require("ws") })'
3357
+ );
3358
+ };
3359
+ var AlphaWebSocket = class {
3360
+ url;
3361
+ reconnectEnabled;
3362
+ maxReconnectAttempts;
3363
+ heartbeatIntervalMs;
3364
+ WebSocketImpl;
3365
+ ws = null;
3366
+ subscriptions = /* @__PURE__ */ new Map();
3367
+ pendingRequests = /* @__PURE__ */ new Map();
3368
+ heartbeatTimer = null;
3369
+ reconnectTimer = null;
3370
+ reconnectAttempts = 0;
3371
+ intentionallyClosed = false;
3372
+ connectPromise = null;
3373
+ constructor(config) {
3374
+ this.url = config?.url ?? DEFAULT_WSS_BASE_URL;
3375
+ this.reconnectEnabled = config?.reconnect ?? true;
3376
+ this.maxReconnectAttempts = config?.maxReconnectAttempts ?? Infinity;
3377
+ this.heartbeatIntervalMs = config?.heartbeatIntervalMs ?? 6e4;
3378
+ this.WebSocketImpl = resolveWebSocket(config?.WebSocket);
3379
+ }
3380
+ /** Whether the WebSocket is currently open and connected */
3381
+ get connected() {
3382
+ return this.ws?.readyState === WS_OPEN;
3383
+ }
3384
+ // ============================================
3385
+ // Subscribe Methods
3386
+ // ============================================
3387
+ /**
3388
+ * Subscribe to live market probability updates (incremental diffs).
3389
+ * @returns An unsubscribe function
3390
+ */
3391
+ subscribeLiveMarkets(callback) {
3392
+ return this.subscribe("get-live-markets", {}, "markets_changed", callback);
3393
+ }
3394
+ /**
3395
+ * Subscribe to change events for a single market.
3396
+ * @param slug - The market slug
3397
+ * @returns An unsubscribe function
3398
+ */
3399
+ subscribeMarket(slug, callback) {
3400
+ return this.subscribe("get-market", { slug }, "market_changed", callback);
3401
+ }
3402
+ /**
3403
+ * Subscribe to full orderbook snapshots (~5s interval on changes).
3404
+ * @param slug - The market slug
3405
+ * @returns An unsubscribe function
3406
+ */
3407
+ subscribeOrderbook(slug, callback) {
3408
+ return this.subscribe("get-orderbook", { slug }, "orderbook_changed", callback);
3409
+ }
3410
+ /**
3411
+ * Subscribe to wallet order updates.
3412
+ * @param wallet - The wallet address
3413
+ * @returns An unsubscribe function
3414
+ */
3415
+ subscribeWalletOrders(wallet, callback) {
3416
+ return this.subscribe("get-wallet-orders", { wallet }, "wallet_orders_changed", callback);
3417
+ }
3418
+ // ============================================
3419
+ // Control Methods
3420
+ // ============================================
3421
+ /** Query the server for the list of active subscriptions on this connection */
3422
+ listSubscriptions() {
3423
+ return this.sendRequest({ method: "LIST_SUBSCRIPTIONS" });
3424
+ }
3425
+ /** Query a server property (e.g. "heartbeat", "limits") */
3426
+ getProperty(property) {
3427
+ return this.sendRequest({ method: "GET_PROPERTY", property });
3428
+ }
3429
+ // ============================================
3430
+ // Lifecycle
3431
+ // ============================================
3432
+ /** Open the WebSocket connection. Called automatically on first subscribe. */
3433
+ connect() {
3434
+ if (this.connectPromise) return this.connectPromise;
3435
+ this.connectPromise = this.doConnect();
3436
+ return this.connectPromise;
3437
+ }
3438
+ /** Close the connection and clean up all resources */
3439
+ close() {
3440
+ this.intentionallyClosed = true;
3441
+ this.clearTimers();
3442
+ this.subscriptions.clear();
3443
+ for (const [, req] of this.pendingRequests) {
3444
+ clearTimeout(req.timer);
3445
+ req.reject(new Error("WebSocket closed"));
3446
+ }
3447
+ this.pendingRequests.clear();
3448
+ if (this.ws) {
3449
+ this.ws.close();
3450
+ this.ws = null;
3451
+ }
3452
+ this.connectPromise = null;
3453
+ }
3454
+ // ============================================
3455
+ // Internal
3456
+ // ============================================
3457
+ buildStreamKey(stream, params) {
3458
+ const parts = [stream, ...Object.entries(params).sort().map(([k, v]) => `${k}=${v}`)];
3459
+ return parts.join("&");
3460
+ }
3461
+ buildQueryString() {
3462
+ const subs = [...this.subscriptions.values()];
3463
+ if (subs.length === 0) return "";
3464
+ const first = subs[0];
3465
+ const params = new URLSearchParams({ stream: first.stream, ...first.params });
3466
+ return "?" + params.toString();
3467
+ }
3468
+ subscribe(stream, params, eventType, callback) {
3469
+ const key = this.buildStreamKey(stream, params);
3470
+ this.subscriptions.set(key, { stream, params, callback, eventType });
3471
+ if (this.connected) {
3472
+ this.sendSubscribe(stream, params);
3473
+ } else {
3474
+ this.connect();
3475
+ }
3476
+ return () => {
3477
+ this.subscriptions.delete(key);
3478
+ if (this.connected) {
3479
+ this.sendUnsubscribe(stream, params);
3480
+ }
3481
+ };
3482
+ }
3483
+ async doConnect() {
3484
+ this.intentionallyClosed = false;
3485
+ return new Promise((resolve, reject) => {
3486
+ const qs = this.buildQueryString();
3487
+ const ws = new this.WebSocketImpl(this.url + qs);
3488
+ ws.onopen = () => {
3489
+ this.ws = ws;
3490
+ this.reconnectAttempts = 0;
3491
+ this.startHeartbeat();
3492
+ const subs = [...this.subscriptions.values()];
3493
+ for (const sub of subs.slice(1)) {
3494
+ this.sendSubscribe(sub.stream, sub.params);
3495
+ }
3496
+ resolve();
3497
+ };
3498
+ ws.onmessage = (event) => {
3499
+ this.handleMessage(event.data);
3500
+ };
3501
+ ws.onclose = () => {
3502
+ this.ws = null;
3503
+ this.connectPromise = null;
3504
+ this.stopHeartbeat();
3505
+ if (!this.intentionallyClosed) {
3506
+ this.scheduleReconnect();
3507
+ }
3508
+ };
3509
+ ws.onerror = (err) => {
3510
+ if (!this.ws) {
3511
+ reject(new Error("WebSocket connection failed"));
3512
+ }
3513
+ };
3514
+ });
3515
+ }
3516
+ handleMessage(raw) {
3517
+ let msg;
3518
+ try {
3519
+ msg = JSON.parse(raw);
3520
+ } catch {
3521
+ return;
3522
+ }
3523
+ if (msg.type === "ping") {
3524
+ this.send({ method: "PONG" });
3525
+ return;
3526
+ }
3527
+ if (msg.requestId && this.pendingRequests.has(msg.requestId)) {
3528
+ const req = this.pendingRequests.get(msg.requestId);
3529
+ this.pendingRequests.delete(msg.requestId);
3530
+ clearTimeout(req.timer);
3531
+ req.resolve(msg);
3532
+ return;
3533
+ }
3534
+ const eventType = msg.type;
3535
+ if (!eventType) return;
3536
+ for (const sub of this.subscriptions.values()) {
3537
+ if (sub.eventType === eventType) {
3538
+ try {
3539
+ sub.callback(msg);
3540
+ } catch {
3541
+ }
3542
+ }
3543
+ }
3544
+ }
3545
+ sendSubscribe(stream, params) {
3546
+ this.send({ method: "SUBSCRIBE", stream, ...params });
3547
+ }
3548
+ sendUnsubscribe(stream, params) {
3549
+ this.send({ method: "UNSUBSCRIBE", stream, ...params });
3550
+ }
3551
+ sendRequest(payload, timeoutMs = 1e4) {
3552
+ const requestId = crypto.randomUUID();
3553
+ return new Promise((resolve, reject) => {
3554
+ const timer = setTimeout(() => {
3555
+ this.pendingRequests.delete(requestId);
3556
+ reject(new Error("Request timed out"));
3557
+ }, timeoutMs);
3558
+ this.pendingRequests.set(requestId, { resolve, reject, timer });
3559
+ if (!this.connected) {
3560
+ this.connect().then(() => {
3561
+ this.send({ ...payload, requestId });
3562
+ }).catch(reject);
3563
+ } else {
3564
+ this.send({ ...payload, requestId });
3565
+ }
3566
+ });
3567
+ }
3568
+ send(data) {
3569
+ if (this.ws?.readyState === WS_OPEN) {
3570
+ this.ws.send(JSON.stringify(data));
3571
+ }
3572
+ }
3573
+ // ============================================
3574
+ // Heartbeat
3575
+ // ============================================
3576
+ startHeartbeat() {
3577
+ this.stopHeartbeat();
3578
+ this.heartbeatTimer = setInterval(() => {
3579
+ this.send({ type: "ping" });
3580
+ }, this.heartbeatIntervalMs);
3581
+ }
3582
+ stopHeartbeat() {
3583
+ if (this.heartbeatTimer) {
3584
+ clearInterval(this.heartbeatTimer);
3585
+ this.heartbeatTimer = null;
3586
+ }
3587
+ }
3588
+ // ============================================
3589
+ // Reconnect
3590
+ // ============================================
3591
+ scheduleReconnect() {
3592
+ if (!this.reconnectEnabled) return;
3593
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
3594
+ const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
3595
+ this.reconnectAttempts++;
3596
+ this.reconnectTimer = setTimeout(() => {
3597
+ this.reconnectTimer = null;
3598
+ this.connect().catch(() => {
3599
+ });
3600
+ }, delay);
3601
+ }
3602
+ clearTimers() {
3603
+ this.stopHeartbeat();
3604
+ if (this.reconnectTimer) {
3605
+ clearTimeout(this.reconnectTimer);
3606
+ this.reconnectTimer = null;
3607
+ }
3608
+ }
3609
+ };
3610
+
3611
+ export { AlphaClient, AlphaWebSocket, DEFAULT_API_BASE_URL, DEFAULT_MARKET_CREATOR_ADDRESS, DEFAULT_WSS_BASE_URL, calculateFee, calculateFeeFromTotal, calculateMatchingOrders, checkAssetOptIn, decodeGlobalState, getEscrowGlobalState, getLiveMarketsFromApi, getMarketFromApi, getMarketGlobalState, getMarketOnChain, getMarketsOnChain };
3215
3612
  //# sourceMappingURL=index.js.map
3216
3613
  //# sourceMappingURL=index.js.map