@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 +126 -6
- package/dist/index.d.mts +89 -2
- package/dist/index.d.ts +89 -2
- package/dist/index.js +126 -6
- package/package.json +1 -1
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($
|
|
473
|
-
myBids(
|
|
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
|
-
|
|
547
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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($
|
|
408
|
-
myBids(
|
|
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
|
-
|
|
482
|
-
|
|
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
|
|
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
|
|
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
|
};
|