@taphubhq/sdk-core 0.15.1 → 0.15.3

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.cjs CHANGED
@@ -431,6 +431,15 @@ function coerceStatus(raw) {
431
431
  console.warn(`Unknown bid status "${raw}", defaulting to "pending"`);
432
432
  return "pending";
433
433
  }
434
+ var SDK_TO_WIRE_STATUS = {
435
+ pending: "pending",
436
+ win: "won",
437
+ lose: "lost",
438
+ cancelled: "cancelled"
439
+ };
440
+ function toWireStatus(status) {
441
+ return SDK_TO_WIRE_STATUS[status];
442
+ }
434
443
  function normaliseBid(node) {
435
444
  if (typeof node.id !== "string" || node.id === "" || typeof node.user_id !== "string" || node.user_id === "" || typeof node.game_id !== "string" || node.game_id === "" || typeof node.status !== "string" || node.status === "") {
436
445
  throw new TaphubServerError("Invalid response from server", {
@@ -469,8 +478,8 @@ var PLACE_BID_MUTATION = `mutation PlaceBid($input: PlaceBidInput!) {
469
478
  id user_id game_id currency amount coefficient time1 time2 price1 price2 status payout slippage created_at
470
479
  }
471
480
  }`;
472
- var MY_BIDS_QUERY = `query MyBids($status: String, $limit: Int, $offset: Int, $gameId: ID) {
473
- myBids(status: $status, limit: $limit, offset: $offset, gameId: $gameId) {
481
+ var MY_BIDS_QUERY = `query MyBids($statuses: [BidStatus!], $limit: Int, $offset: Int, $gameId: ID) {
482
+ myBids(statuses: $statuses, limit: $limit, offset: $offset, gameId: $gameId) {
474
483
  id user_id game_id currency amount coefficient time1 time2 price1 price2 status payout slippage created_at
475
484
  }
476
485
  }`;
@@ -543,8 +552,9 @@ var BidModule = class {
543
552
  }
544
553
  async listBids(opts) {
545
554
  const variables = {};
546
- if (opts?.status !== void 0) {
547
- variables.status = opts.status;
555
+ const statusList = opts?.statuses ?? (opts?.status !== void 0 ? [opts.status] : void 0);
556
+ if (statusList !== void 0 && statusList.length > 0) {
557
+ variables.statuses = statusList.map(toWireStatus);
548
558
  }
549
559
  if (opts?.limit !== void 0) {
550
560
  variables.limit = opts.limit;
@@ -885,6 +895,10 @@ var GAME_SCOPED_SUFFIXES = ["config", "ideal_config"];
885
895
  var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
886
896
  var TOPIC_PREFIX = "game";
887
897
  var MARKET_TOPIC_PREFIX = "market";
898
+ var WALLET_TOPIC_PREFIX = "wallet";
899
+ function walletBalanceTopic(userId) {
900
+ return `${WALLET_TOPIC_PREFIX}/users/${userId}/balance`;
901
+ }
888
902
  function agencyPairStatsTopic(aid, gamePairId) {
889
903
  return `public/agency/${aid}/pair/${gamePairId}/stats`;
890
904
  }
@@ -905,6 +919,7 @@ function createMqttTransport(endpoint, opts = {}) {
905
919
  const subscriptions = /* @__PURE__ */ new Map();
906
920
  const candleSubscriptions = /* @__PURE__ */ new Map();
907
921
  const statsSubscriptions = /* @__PURE__ */ new Map();
922
+ const walletSubscriptions = /* @__PURE__ */ new Map();
908
923
  const { onLifecycle } = opts;
909
924
  let connectStartedAt = 0;
910
925
  function fireLifecycle(event) {
@@ -973,6 +988,18 @@ function createMqttTransport(endpoint, opts = {}) {
973
988
  statsSub.onStats(receivedTopic, payload2);
974
989
  return;
975
990
  }
991
+ const walletSub = [...walletSubscriptions.values()].find((s) => s.topic === receivedTopic);
992
+ if (walletSub) {
993
+ let payload2;
994
+ try {
995
+ payload2 = JSON.parse(message.toString());
996
+ } catch {
997
+ walletSub.onError(new Error(`Invalid JSON on topic ${receivedTopic}`));
998
+ return;
999
+ }
1000
+ walletSub.onMessage(receivedTopic, payload2);
1001
+ return;
1002
+ }
976
1003
  let matched;
977
1004
  for (const sub of subscriptions.values()) {
978
1005
  if (sub.topics.includes(receivedTopic)) {
@@ -1047,6 +1074,19 @@ function createMqttTransport(endpoint, opts = {}) {
1047
1074
  if (client) client.unsubscribe(entry.topic);
1048
1075
  statsSubscriptions.delete(topic);
1049
1076
  },
1077
+ subscribeWallet(userId, onMessage, onError) {
1078
+ if (walletSubscriptions.has(userId)) return;
1079
+ const topic = walletBalanceTopic(userId);
1080
+ const mqttClient = ensureConnected();
1081
+ walletSubscriptions.set(userId, { topic, onMessage, onError });
1082
+ mqttClient.subscribe(topic, { qos: 0 });
1083
+ },
1084
+ unsubscribeWallet(userId) {
1085
+ const entry = walletSubscriptions.get(userId);
1086
+ if (!entry) return;
1087
+ if (client) client.unsubscribe(entry.topic);
1088
+ walletSubscriptions.delete(userId);
1089
+ },
1050
1090
  unsubscribeAll(gameId, userId) {
1051
1091
  const matches = entriesForGame(gameId);
1052
1092
  if (matches.length === 0) return;
@@ -1084,10 +1124,14 @@ function createMqttTransport(endpoint, opts = {}) {
1084
1124
  for (const entry of statsSubscriptions.values()) {
1085
1125
  client.unsubscribe(entry.topic);
1086
1126
  }
1127
+ for (const entry of walletSubscriptions.values()) {
1128
+ client.unsubscribe(entry.topic);
1129
+ }
1087
1130
  }
1088
1131
  subscriptions.clear();
1089
1132
  candleSubscriptions.clear();
1090
1133
  statsSubscriptions.clear();
1134
+ walletSubscriptions.clear();
1091
1135
  if (client) {
1092
1136
  client.end(true);
1093
1137
  client = null;
@@ -1097,7 +1141,7 @@ function createMqttTransport(endpoint, opts = {}) {
1097
1141
  }
1098
1142
 
1099
1143
  // src/modules/realtime/index.ts
1100
- var import_eventemitter32 = __toESM(require("eventemitter3"));
1144
+ var import_eventemitter33 = __toESM(require("eventemitter3"));
1101
1145
 
1102
1146
  // src/modules/realtime/GameChannel.ts
1103
1147
  var import_eventemitter3 = __toESM(require("eventemitter3"));
@@ -1109,6 +1153,16 @@ var GameChannel = class extends import_eventemitter3.default {
1109
1153
  }
1110
1154
  };
1111
1155
 
1156
+ // src/modules/realtime/WalletChannel.ts
1157
+ var import_eventemitter32 = __toESM(require("eventemitter3"));
1158
+ var WalletChannel = class extends import_eventemitter32.default {
1159
+ userId;
1160
+ constructor(userId) {
1161
+ super();
1162
+ this.userId = userId;
1163
+ }
1164
+ };
1165
+
1112
1166
  // src/modules/realtime/index.ts
1113
1167
  var TOPIC_SUFFIX_CANDLE = "candle";
1114
1168
  var TOPIC_SUFFIX_BID_RESULT = "bid_result";
@@ -1168,6 +1222,16 @@ function mapWireBalanceUpdate(raw) {
1168
1222
  balance: p.balance
1169
1223
  };
1170
1224
  }
1225
+ function mapWireWalletBalance(raw) {
1226
+ const p = raw;
1227
+ return {
1228
+ id: p.id,
1229
+ balance: p.balance,
1230
+ reservedAmount: p.reserved_amount,
1231
+ currency: p.currency,
1232
+ reason: p.reason ?? ""
1233
+ };
1234
+ }
1171
1235
  function mapWireConfig(raw) {
1172
1236
  const p = raw;
1173
1237
  return {
@@ -1205,9 +1269,11 @@ function mapWireToEvent(topic, payload) {
1205
1269
  function normaliseUserId2(userId) {
1206
1270
  return userId && userId !== "" ? userId : null;
1207
1271
  }
1208
- var RealtimeModule = class extends import_eventemitter32.default {
1272
+ var RealtimeModule = class extends import_eventemitter33.default {
1209
1273
  #transport;
1210
1274
  #entries = /* @__PURE__ */ new Map();
1275
+ #walletEntries = /* @__PURE__ */ new Map();
1276
+ // keyed by userId
1211
1277
  #agencyId;
1212
1278
  constructor(mqttEndpointOrOptions) {
1213
1279
  super();
@@ -1287,6 +1353,56 @@ var RealtimeModule = class extends import_eventemitter32.default {
1287
1353
  target.channel.removeAllListeners();
1288
1354
  this.#transport.unsubscribeAll(gameId, target.userId);
1289
1355
  }
1356
+ /**
1357
+ * Subscribe to the user-scoped wallet balance stream for `userId`, topic
1358
+ * `wallet/users/{userId}/balance`. Returns a `WalletChannel` that emits
1359
+ * `walletBalanceUpdate` on every balance change (deposit / transfer / refund /
1360
+ * reserve / commit / cancel / bid settlement) — independent of any game.
1361
+ *
1362
+ * Reference-counted by `userId`, mirroring `subscribe`/`unsubscribe`: repeat
1363
+ * calls return the same channel and the topic is torn down only when balanced
1364
+ * `unsubscribeWallet` calls bring the count to zero. The wallet lifecycle is
1365
+ * fully independent of game subscriptions.
1366
+ */
1367
+ subscribeWallet(userId) {
1368
+ const cleanUserId = normaliseUserId2(userId);
1369
+ if (!cleanUserId) {
1370
+ throw new TaphubError("userId is required to subscribe to wallet balance", {
1371
+ code: "UserIdRequired"
1372
+ });
1373
+ }
1374
+ const existing = this.#walletEntries.get(cleanUserId);
1375
+ if (existing) {
1376
+ existing.refcount += 1;
1377
+ return existing.channel;
1378
+ }
1379
+ const channel = new WalletChannel(cleanUserId);
1380
+ const onMessage = (_topic, payload) => {
1381
+ channel.emit("walletBalanceUpdate", mapWireWalletBalance(payload));
1382
+ };
1383
+ const onError = (err) => {
1384
+ channel.emit("error", err);
1385
+ };
1386
+ this.#walletEntries.set(cleanUserId, { userId: cleanUserId, channel, refcount: 1 });
1387
+ this.#transport.subscribeWallet(cleanUserId, onMessage, onError);
1388
+ return channel;
1389
+ }
1390
+ /**
1391
+ * Decrement the wallet subscription refcount for `userId`. Tears down the MQTT
1392
+ * topic and removes the `WalletChannel` only when the count reaches zero.
1393
+ * No-op if there is no active wallet subscription for `userId`.
1394
+ */
1395
+ unsubscribeWallet(userId) {
1396
+ const cleanUserId = normaliseUserId2(userId);
1397
+ if (!cleanUserId) return;
1398
+ const entry = this.#walletEntries.get(cleanUserId);
1399
+ if (!entry) return;
1400
+ entry.refcount -= 1;
1401
+ if (entry.refcount > 0) return;
1402
+ this.#walletEntries.delete(cleanUserId);
1403
+ entry.channel.removeAllListeners();
1404
+ this.#transport.unsubscribeWallet(cleanUserId);
1405
+ }
1290
1406
  /**
1291
1407
  * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
1292
1408
  * Independent of game/agency — candle is market-wide public data.
@@ -1330,6 +1446,10 @@ var RealtimeModule = class extends import_eventemitter32.default {
1330
1446
  entry.channel.removeAllListeners();
1331
1447
  }
1332
1448
  this.#entries.clear();
1449
+ for (const entry of this.#walletEntries.values()) {
1450
+ entry.channel.removeAllListeners();
1451
+ }
1452
+ this.#walletEntries.clear();
1333
1453
  this.#transport.close();
1334
1454
  }
1335
1455
  };
package/dist/index.d.mts CHANGED
@@ -222,7 +222,10 @@ declare class BidModule {
222
222
  signal?: AbortSignal;
223
223
  }): Promise<CancelBidResult>;
224
224
  listBids(opts?: {
225
+ /** @deprecated Use `statuses` instead. Folded into a one-element `statuses` list. */
225
226
  status?: BidStatus;
227
+ /** Include-semantics: return only bids whose status is in this list. */
228
+ statuses?: BidStatus[];
226
229
  limit?: number;
227
230
  offset?: number;
228
231
  gameId?: string;
@@ -482,7 +485,19 @@ interface MqttWireAgencyPairStats {
482
485
  maxCoef: number;
483
486
  ts: number;
484
487
  }
485
- type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig | MqttWireAgencyPairStats;
488
+ /**
489
+ * Wire payload on the user-scoped wallet topic `wallet/users/{userId}/balance`,
490
+ * published by `taphub-user-service` (`streams/wallet.go`). All fields are JSON
491
+ * strings. `reserved_amount` is snake_case on the wire.
492
+ */
493
+ interface MqttWireWalletBalance {
494
+ id: string;
495
+ balance: string;
496
+ reserved_amount: string;
497
+ currency: string;
498
+ reason: string;
499
+ }
500
+ type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig | MqttWireAgencyPairStats | MqttWireWalletBalance;
486
501
  type MqttMessageHandler = (gameId: string, topic: string, payload: MqttWirePayload) => void;
487
502
  type MqttErrorHandler = (err: Error) => void;
488
503
  type MqttLifecycleEvent = {
@@ -503,6 +518,7 @@ type MqttLifecycleEvent = {
503
518
  type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
504
519
  type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
505
520
  type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
521
+ type MqttWalletMessageHandler = (topic: string, payload: MqttWirePayload) => void;
506
522
  interface MqttTransport {
507
523
  /**
508
524
  * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
@@ -536,6 +552,15 @@ interface MqttTransport {
536
552
  subscribeAgencyPairStats(aid: string, gamePairId: string, onStats: MqttAgencyPairStatsHandler): void;
537
553
  /** Tear down the stats subscription for an `(aid, gamePairId)`. */
538
554
  unsubscribeAgencyPairStats(aid: string, gamePairId: string): void;
555
+ /**
556
+ * Subscribe to the user-scoped wallet balance stream, topic
557
+ * `wallet/users/{userId}/balance`. Independent of any game subscription — NOT
558
+ * torn down by `unsubscribeAll`. Idempotent: repeat calls for the same
559
+ * `userId` are deduplicated. Subscribed at QoS 0 to match the publisher.
560
+ */
561
+ subscribeWallet(userId: string, onMessage: MqttWalletMessageHandler, onError: MqttErrorHandler): void;
562
+ /** Tear down the wallet subscription for a `userId`. No-op if not subscribed. */
563
+ unsubscribeWallet(userId: string): void;
539
564
  close(): void;
540
565
  }
541
566
 
@@ -598,6 +623,36 @@ interface MqttBalanceEvent {
598
623
  userId: string;
599
624
  balance: string;
600
625
  }
626
+ /**
627
+ * Cause of a wallet balance change. Byte-mirrors `BalanceUpdateReason` in the
628
+ * backend (`taphub-user-service` `streams/wallet.go`). `''` is the documented
629
+ * unspecified/legacy value. Treat unknown reasons defensively (enum fallback) —
630
+ * the SDK passes through values it does not recognise rather than dropping the
631
+ * event.
632
+ */
633
+ type WalletBalanceReason = 'transfer' | 'refund' | 'reserve' | 'commit' | 'cancel' | 'bid_placed' | 'bid_won' | '';
634
+ /**
635
+ * Wallet-level balance event from the user-scoped MQTT topic
636
+ * `wallet/users/{userId}/balance`. Fires on deposit / agency transfer / refund /
637
+ * reserve / commit / cancel / bid settlement — i.e. every balance change, with
638
+ * NO game context (unlike the game-scoped `MqttBalanceEvent`).
639
+ *
640
+ * Money fields (`balance`, `reservedAmount`) are kept as wire strings verbatim;
641
+ * the SDK does NOT coerce decimal money to `number`. Consumers coerce at the
642
+ * app edge if they need a number.
643
+ */
644
+ interface MqttWalletBalanceEvent {
645
+ /** Wallet id (the backend `id` field). */
646
+ id: string;
647
+ /** Spendable balance, as the raw wire string. */
648
+ balance: string;
649
+ /** Amount held by PENDING reservations, as the raw wire string. */
650
+ reservedAmount: string;
651
+ /** Wallet currency (e.g. "USDC.e"). */
652
+ currency: string;
653
+ /** Cause of the change; unknown values are passed through as-is. */
654
+ reason: WalletBalanceReason;
655
+ }
601
656
  interface MqttConfigEvent {
602
657
  minBidAmount: number;
603
658
  maxBidAmount: number;
@@ -638,6 +693,20 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
638
693
  constructor(gameId: string);
639
694
  }
640
695
 
696
+ interface WalletChannelEvents {
697
+ walletBalanceUpdate: [MqttWalletBalanceEvent];
698
+ error: [Error];
699
+ }
700
+ /**
701
+ * User-scoped wallet channel. Emits `walletBalanceUpdate` for every balance
702
+ * change on `wallet/users/{userId}/balance`, independent of any game. Mirror of
703
+ * {@link GameChannel} but keyed by `userId` rather than `gameId`.
704
+ */
705
+ declare class WalletChannel extends EventEmitter<WalletChannelEvents> {
706
+ readonly userId: string;
707
+ constructor(userId: string);
708
+ }
709
+
641
710
  interface RealtimeModuleOptions {
642
711
  mqttEndpoint: string;
643
712
  /**
@@ -665,6 +734,24 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
665
734
  get _transport(): MqttTransport;
666
735
  subscribe(gameId: string, userId?: string | null): GameChannel;
667
736
  unsubscribe(gameId: string, userId?: string | null): void;
737
+ /**
738
+ * Subscribe to the user-scoped wallet balance stream for `userId`, topic
739
+ * `wallet/users/{userId}/balance`. Returns a `WalletChannel` that emits
740
+ * `walletBalanceUpdate` on every balance change (deposit / transfer / refund /
741
+ * reserve / commit / cancel / bid settlement) — independent of any game.
742
+ *
743
+ * Reference-counted by `userId`, mirroring `subscribe`/`unsubscribe`: repeat
744
+ * calls return the same channel and the topic is torn down only when balanced
745
+ * `unsubscribeWallet` calls bring the count to zero. The wallet lifecycle is
746
+ * fully independent of game subscriptions.
747
+ */
748
+ subscribeWallet(userId: string): WalletChannel;
749
+ /**
750
+ * Decrement the wallet subscription refcount for `userId`. Tears down the MQTT
751
+ * topic and removes the `WalletChannel` only when the count reaches zero.
752
+ * No-op if there is no active wallet subscription for `userId`.
753
+ */
754
+ unsubscribeWallet(userId: string): void;
668
755
  /**
669
756
  * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
670
757
  * Independent of game/agency — candle is market-wide public data.
@@ -901,4 +988,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
901
988
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
902
989
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
903
990
 
904
- export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type Game, GameChannel, type GameConfig, GameModule, type GamePairInfo, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type PlaceBidInput, RealtimeModule, type Sample, type SampleReason, type SignalSource, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
991
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type Game, GameChannel, type GameConfig, GameModule, type GamePairInfo, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type PlaceBidInput, RealtimeModule, type Sample, type SampleReason, type SignalSource, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
package/dist/index.d.ts CHANGED
@@ -222,7 +222,10 @@ declare class BidModule {
222
222
  signal?: AbortSignal;
223
223
  }): Promise<CancelBidResult>;
224
224
  listBids(opts?: {
225
+ /** @deprecated Use `statuses` instead. Folded into a one-element `statuses` list. */
225
226
  status?: BidStatus;
227
+ /** Include-semantics: return only bids whose status is in this list. */
228
+ statuses?: BidStatus[];
226
229
  limit?: number;
227
230
  offset?: number;
228
231
  gameId?: string;
@@ -482,7 +485,19 @@ interface MqttWireAgencyPairStats {
482
485
  maxCoef: number;
483
486
  ts: number;
484
487
  }
485
- type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig | MqttWireAgencyPairStats;
488
+ /**
489
+ * Wire payload on the user-scoped wallet topic `wallet/users/{userId}/balance`,
490
+ * published by `taphub-user-service` (`streams/wallet.go`). All fields are JSON
491
+ * strings. `reserved_amount` is snake_case on the wire.
492
+ */
493
+ interface MqttWireWalletBalance {
494
+ id: string;
495
+ balance: string;
496
+ reserved_amount: string;
497
+ currency: string;
498
+ reason: string;
499
+ }
500
+ type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig | MqttWireAgencyPairStats | MqttWireWalletBalance;
486
501
  type MqttMessageHandler = (gameId: string, topic: string, payload: MqttWirePayload) => void;
487
502
  type MqttErrorHandler = (err: Error) => void;
488
503
  type MqttLifecycleEvent = {
@@ -503,6 +518,7 @@ type MqttLifecycleEvent = {
503
518
  type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
504
519
  type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
505
520
  type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
521
+ type MqttWalletMessageHandler = (topic: string, payload: MqttWirePayload) => void;
506
522
  interface MqttTransport {
507
523
  /**
508
524
  * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
@@ -536,6 +552,15 @@ interface MqttTransport {
536
552
  subscribeAgencyPairStats(aid: string, gamePairId: string, onStats: MqttAgencyPairStatsHandler): void;
537
553
  /** Tear down the stats subscription for an `(aid, gamePairId)`. */
538
554
  unsubscribeAgencyPairStats(aid: string, gamePairId: string): void;
555
+ /**
556
+ * Subscribe to the user-scoped wallet balance stream, topic
557
+ * `wallet/users/{userId}/balance`. Independent of any game subscription — NOT
558
+ * torn down by `unsubscribeAll`. Idempotent: repeat calls for the same
559
+ * `userId` are deduplicated. Subscribed at QoS 0 to match the publisher.
560
+ */
561
+ subscribeWallet(userId: string, onMessage: MqttWalletMessageHandler, onError: MqttErrorHandler): void;
562
+ /** Tear down the wallet subscription for a `userId`. No-op if not subscribed. */
563
+ unsubscribeWallet(userId: string): void;
539
564
  close(): void;
540
565
  }
541
566
 
@@ -598,6 +623,36 @@ interface MqttBalanceEvent {
598
623
  userId: string;
599
624
  balance: string;
600
625
  }
626
+ /**
627
+ * Cause of a wallet balance change. Byte-mirrors `BalanceUpdateReason` in the
628
+ * backend (`taphub-user-service` `streams/wallet.go`). `''` is the documented
629
+ * unspecified/legacy value. Treat unknown reasons defensively (enum fallback) —
630
+ * the SDK passes through values it does not recognise rather than dropping the
631
+ * event.
632
+ */
633
+ type WalletBalanceReason = 'transfer' | 'refund' | 'reserve' | 'commit' | 'cancel' | 'bid_placed' | 'bid_won' | '';
634
+ /**
635
+ * Wallet-level balance event from the user-scoped MQTT topic
636
+ * `wallet/users/{userId}/balance`. Fires on deposit / agency transfer / refund /
637
+ * reserve / commit / cancel / bid settlement — i.e. every balance change, with
638
+ * NO game context (unlike the game-scoped `MqttBalanceEvent`).
639
+ *
640
+ * Money fields (`balance`, `reservedAmount`) are kept as wire strings verbatim;
641
+ * the SDK does NOT coerce decimal money to `number`. Consumers coerce at the
642
+ * app edge if they need a number.
643
+ */
644
+ interface MqttWalletBalanceEvent {
645
+ /** Wallet id (the backend `id` field). */
646
+ id: string;
647
+ /** Spendable balance, as the raw wire string. */
648
+ balance: string;
649
+ /** Amount held by PENDING reservations, as the raw wire string. */
650
+ reservedAmount: string;
651
+ /** Wallet currency (e.g. "USDC.e"). */
652
+ currency: string;
653
+ /** Cause of the change; unknown values are passed through as-is. */
654
+ reason: WalletBalanceReason;
655
+ }
601
656
  interface MqttConfigEvent {
602
657
  minBidAmount: number;
603
658
  maxBidAmount: number;
@@ -638,6 +693,20 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
638
693
  constructor(gameId: string);
639
694
  }
640
695
 
696
+ interface WalletChannelEvents {
697
+ walletBalanceUpdate: [MqttWalletBalanceEvent];
698
+ error: [Error];
699
+ }
700
+ /**
701
+ * User-scoped wallet channel. Emits `walletBalanceUpdate` for every balance
702
+ * change on `wallet/users/{userId}/balance`, independent of any game. Mirror of
703
+ * {@link GameChannel} but keyed by `userId` rather than `gameId`.
704
+ */
705
+ declare class WalletChannel extends EventEmitter<WalletChannelEvents> {
706
+ readonly userId: string;
707
+ constructor(userId: string);
708
+ }
709
+
641
710
  interface RealtimeModuleOptions {
642
711
  mqttEndpoint: string;
643
712
  /**
@@ -665,6 +734,24 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
665
734
  get _transport(): MqttTransport;
666
735
  subscribe(gameId: string, userId?: string | null): GameChannel;
667
736
  unsubscribe(gameId: string, userId?: string | null): void;
737
+ /**
738
+ * Subscribe to the user-scoped wallet balance stream for `userId`, topic
739
+ * `wallet/users/{userId}/balance`. Returns a `WalletChannel` that emits
740
+ * `walletBalanceUpdate` on every balance change (deposit / transfer / refund /
741
+ * reserve / commit / cancel / bid settlement) — independent of any game.
742
+ *
743
+ * Reference-counted by `userId`, mirroring `subscribe`/`unsubscribe`: repeat
744
+ * calls return the same channel and the topic is torn down only when balanced
745
+ * `unsubscribeWallet` calls bring the count to zero. The wallet lifecycle is
746
+ * fully independent of game subscriptions.
747
+ */
748
+ subscribeWallet(userId: string): WalletChannel;
749
+ /**
750
+ * Decrement the wallet subscription refcount for `userId`. Tears down the MQTT
751
+ * topic and removes the `WalletChannel` only when the count reaches zero.
752
+ * No-op if there is no active wallet subscription for `userId`.
753
+ */
754
+ unsubscribeWallet(userId: string): void;
668
755
  /**
669
756
  * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
670
757
  * Independent of game/agency — candle is market-wide public data.
@@ -901,4 +988,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
901
988
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
902
989
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
903
990
 
904
- export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type Game, GameChannel, type GameConfig, GameModule, type GamePairInfo, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type PlaceBidInput, RealtimeModule, type Sample, type SampleReason, type SignalSource, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
991
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type Game, GameChannel, type GameConfig, GameModule, type GamePairInfo, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type MqttWalletBalanceEvent, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type PlaceBidInput, RealtimeModule, type Sample, type SampleReason, type SignalSource, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type UserPnL, type UserPnLPeriod, type Wallet as UserWallet, type Wallet$1 as Wallet, type WalletBalanceReason, WalletChannel, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
package/dist/index.js CHANGED
@@ -366,6 +366,15 @@ function coerceStatus(raw) {
366
366
  console.warn(`Unknown bid status "${raw}", defaulting to "pending"`);
367
367
  return "pending";
368
368
  }
369
+ var SDK_TO_WIRE_STATUS = {
370
+ pending: "pending",
371
+ win: "won",
372
+ lose: "lost",
373
+ cancelled: "cancelled"
374
+ };
375
+ function toWireStatus(status) {
376
+ return SDK_TO_WIRE_STATUS[status];
377
+ }
369
378
  function normaliseBid(node) {
370
379
  if (typeof node.id !== "string" || node.id === "" || typeof node.user_id !== "string" || node.user_id === "" || typeof node.game_id !== "string" || node.game_id === "" || typeof node.status !== "string" || node.status === "") {
371
380
  throw new TaphubServerError("Invalid response from server", {
@@ -404,8 +413,8 @@ var PLACE_BID_MUTATION = `mutation PlaceBid($input: PlaceBidInput!) {
404
413
  id user_id game_id currency amount coefficient time1 time2 price1 price2 status payout slippage created_at
405
414
  }
406
415
  }`;
407
- var MY_BIDS_QUERY = `query MyBids($status: String, $limit: Int, $offset: Int, $gameId: ID) {
408
- myBids(status: $status, limit: $limit, offset: $offset, gameId: $gameId) {
416
+ var MY_BIDS_QUERY = `query MyBids($statuses: [BidStatus!], $limit: Int, $offset: Int, $gameId: ID) {
417
+ myBids(statuses: $statuses, limit: $limit, offset: $offset, gameId: $gameId) {
409
418
  id user_id game_id currency amount coefficient time1 time2 price1 price2 status payout slippage created_at
410
419
  }
411
420
  }`;
@@ -478,8 +487,9 @@ var BidModule = class {
478
487
  }
479
488
  async listBids(opts) {
480
489
  const variables = {};
481
- if (opts?.status !== void 0) {
482
- variables.status = opts.status;
490
+ const statusList = opts?.statuses ?? (opts?.status !== void 0 ? [opts.status] : void 0);
491
+ if (statusList !== void 0 && statusList.length > 0) {
492
+ variables.statuses = statusList.map(toWireStatus);
483
493
  }
484
494
  if (opts?.limit !== void 0) {
485
495
  variables.limit = opts.limit;
@@ -820,6 +830,10 @@ var GAME_SCOPED_SUFFIXES = ["config", "ideal_config"];
820
830
  var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
821
831
  var TOPIC_PREFIX = "game";
822
832
  var MARKET_TOPIC_PREFIX = "market";
833
+ var WALLET_TOPIC_PREFIX = "wallet";
834
+ function walletBalanceTopic(userId) {
835
+ return `${WALLET_TOPIC_PREFIX}/users/${userId}/balance`;
836
+ }
823
837
  function agencyPairStatsTopic(aid, gamePairId) {
824
838
  return `public/agency/${aid}/pair/${gamePairId}/stats`;
825
839
  }
@@ -840,6 +854,7 @@ function createMqttTransport(endpoint, opts = {}) {
840
854
  const subscriptions = /* @__PURE__ */ new Map();
841
855
  const candleSubscriptions = /* @__PURE__ */ new Map();
842
856
  const statsSubscriptions = /* @__PURE__ */ new Map();
857
+ const walletSubscriptions = /* @__PURE__ */ new Map();
843
858
  const { onLifecycle } = opts;
844
859
  let connectStartedAt = 0;
845
860
  function fireLifecycle(event) {
@@ -908,6 +923,18 @@ function createMqttTransport(endpoint, opts = {}) {
908
923
  statsSub.onStats(receivedTopic, payload2);
909
924
  return;
910
925
  }
926
+ const walletSub = [...walletSubscriptions.values()].find((s) => s.topic === receivedTopic);
927
+ if (walletSub) {
928
+ let payload2;
929
+ try {
930
+ payload2 = JSON.parse(message.toString());
931
+ } catch {
932
+ walletSub.onError(new Error(`Invalid JSON on topic ${receivedTopic}`));
933
+ return;
934
+ }
935
+ walletSub.onMessage(receivedTopic, payload2);
936
+ return;
937
+ }
911
938
  let matched;
912
939
  for (const sub of subscriptions.values()) {
913
940
  if (sub.topics.includes(receivedTopic)) {
@@ -982,6 +1009,19 @@ function createMqttTransport(endpoint, opts = {}) {
982
1009
  if (client) client.unsubscribe(entry.topic);
983
1010
  statsSubscriptions.delete(topic);
984
1011
  },
1012
+ subscribeWallet(userId, onMessage, onError) {
1013
+ if (walletSubscriptions.has(userId)) return;
1014
+ const topic = walletBalanceTopic(userId);
1015
+ const mqttClient = ensureConnected();
1016
+ walletSubscriptions.set(userId, { topic, onMessage, onError });
1017
+ mqttClient.subscribe(topic, { qos: 0 });
1018
+ },
1019
+ unsubscribeWallet(userId) {
1020
+ const entry = walletSubscriptions.get(userId);
1021
+ if (!entry) return;
1022
+ if (client) client.unsubscribe(entry.topic);
1023
+ walletSubscriptions.delete(userId);
1024
+ },
985
1025
  unsubscribeAll(gameId, userId) {
986
1026
  const matches = entriesForGame(gameId);
987
1027
  if (matches.length === 0) return;
@@ -1019,10 +1059,14 @@ function createMqttTransport(endpoint, opts = {}) {
1019
1059
  for (const entry of statsSubscriptions.values()) {
1020
1060
  client.unsubscribe(entry.topic);
1021
1061
  }
1062
+ for (const entry of walletSubscriptions.values()) {
1063
+ client.unsubscribe(entry.topic);
1064
+ }
1022
1065
  }
1023
1066
  subscriptions.clear();
1024
1067
  candleSubscriptions.clear();
1025
1068
  statsSubscriptions.clear();
1069
+ walletSubscriptions.clear();
1026
1070
  if (client) {
1027
1071
  client.end(true);
1028
1072
  client = null;
@@ -1032,7 +1076,7 @@ function createMqttTransport(endpoint, opts = {}) {
1032
1076
  }
1033
1077
 
1034
1078
  // src/modules/realtime/index.ts
1035
- import EventEmitter2 from "eventemitter3";
1079
+ import EventEmitter3 from "eventemitter3";
1036
1080
 
1037
1081
  // src/modules/realtime/GameChannel.ts
1038
1082
  import EventEmitter from "eventemitter3";
@@ -1044,6 +1088,16 @@ var GameChannel = class extends EventEmitter {
1044
1088
  }
1045
1089
  };
1046
1090
 
1091
+ // src/modules/realtime/WalletChannel.ts
1092
+ import EventEmitter2 from "eventemitter3";
1093
+ var WalletChannel = class extends EventEmitter2 {
1094
+ userId;
1095
+ constructor(userId) {
1096
+ super();
1097
+ this.userId = userId;
1098
+ }
1099
+ };
1100
+
1047
1101
  // src/modules/realtime/index.ts
1048
1102
  var TOPIC_SUFFIX_CANDLE = "candle";
1049
1103
  var TOPIC_SUFFIX_BID_RESULT = "bid_result";
@@ -1103,6 +1157,16 @@ function mapWireBalanceUpdate(raw) {
1103
1157
  balance: p.balance
1104
1158
  };
1105
1159
  }
1160
+ function mapWireWalletBalance(raw) {
1161
+ const p = raw;
1162
+ return {
1163
+ id: p.id,
1164
+ balance: p.balance,
1165
+ reservedAmount: p.reserved_amount,
1166
+ currency: p.currency,
1167
+ reason: p.reason ?? ""
1168
+ };
1169
+ }
1106
1170
  function mapWireConfig(raw) {
1107
1171
  const p = raw;
1108
1172
  return {
@@ -1140,9 +1204,11 @@ function mapWireToEvent(topic, payload) {
1140
1204
  function normaliseUserId2(userId) {
1141
1205
  return userId && userId !== "" ? userId : null;
1142
1206
  }
1143
- var RealtimeModule = class extends EventEmitter2 {
1207
+ var RealtimeModule = class extends EventEmitter3 {
1144
1208
  #transport;
1145
1209
  #entries = /* @__PURE__ */ new Map();
1210
+ #walletEntries = /* @__PURE__ */ new Map();
1211
+ // keyed by userId
1146
1212
  #agencyId;
1147
1213
  constructor(mqttEndpointOrOptions) {
1148
1214
  super();
@@ -1222,6 +1288,56 @@ var RealtimeModule = class extends EventEmitter2 {
1222
1288
  target.channel.removeAllListeners();
1223
1289
  this.#transport.unsubscribeAll(gameId, target.userId);
1224
1290
  }
1291
+ /**
1292
+ * Subscribe to the user-scoped wallet balance stream for `userId`, topic
1293
+ * `wallet/users/{userId}/balance`. Returns a `WalletChannel` that emits
1294
+ * `walletBalanceUpdate` on every balance change (deposit / transfer / refund /
1295
+ * reserve / commit / cancel / bid settlement) — independent of any game.
1296
+ *
1297
+ * Reference-counted by `userId`, mirroring `subscribe`/`unsubscribe`: repeat
1298
+ * calls return the same channel and the topic is torn down only when balanced
1299
+ * `unsubscribeWallet` calls bring the count to zero. The wallet lifecycle is
1300
+ * fully independent of game subscriptions.
1301
+ */
1302
+ subscribeWallet(userId) {
1303
+ const cleanUserId = normaliseUserId2(userId);
1304
+ if (!cleanUserId) {
1305
+ throw new TaphubError("userId is required to subscribe to wallet balance", {
1306
+ code: "UserIdRequired"
1307
+ });
1308
+ }
1309
+ const existing = this.#walletEntries.get(cleanUserId);
1310
+ if (existing) {
1311
+ existing.refcount += 1;
1312
+ return existing.channel;
1313
+ }
1314
+ const channel = new WalletChannel(cleanUserId);
1315
+ const onMessage = (_topic, payload) => {
1316
+ channel.emit("walletBalanceUpdate", mapWireWalletBalance(payload));
1317
+ };
1318
+ const onError = (err) => {
1319
+ channel.emit("error", err);
1320
+ };
1321
+ this.#walletEntries.set(cleanUserId, { userId: cleanUserId, channel, refcount: 1 });
1322
+ this.#transport.subscribeWallet(cleanUserId, onMessage, onError);
1323
+ return channel;
1324
+ }
1325
+ /**
1326
+ * Decrement the wallet subscription refcount for `userId`. Tears down the MQTT
1327
+ * topic and removes the `WalletChannel` only when the count reaches zero.
1328
+ * No-op if there is no active wallet subscription for `userId`.
1329
+ */
1330
+ unsubscribeWallet(userId) {
1331
+ const cleanUserId = normaliseUserId2(userId);
1332
+ if (!cleanUserId) return;
1333
+ const entry = this.#walletEntries.get(cleanUserId);
1334
+ if (!entry) return;
1335
+ entry.refcount -= 1;
1336
+ if (entry.refcount > 0) return;
1337
+ this.#walletEntries.delete(cleanUserId);
1338
+ entry.channel.removeAllListeners();
1339
+ this.#transport.unsubscribeWallet(cleanUserId);
1340
+ }
1225
1341
  /**
1226
1342
  * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
1227
1343
  * Independent of game/agency — candle is market-wide public data.
@@ -1265,6 +1381,10 @@ var RealtimeModule = class extends EventEmitter2 {
1265
1381
  entry.channel.removeAllListeners();
1266
1382
  }
1267
1383
  this.#entries.clear();
1384
+ for (const entry of this.#walletEntries.values()) {
1385
+ entry.channel.removeAllListeners();
1386
+ }
1387
+ this.#walletEntries.clear();
1268
1388
  this.#transport.close();
1269
1389
  }
1270
1390
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",