@taphubhq/sdk-core 0.13.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.cjs ADDED
@@ -0,0 +1,2513 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AuthModule: () => AuthModule,
34
+ BidModule: () => BidModule,
35
+ GameModule: () => GameModule,
36
+ LeaderboardModule: () => LeaderboardModule,
37
+ LocaleModule: () => LocaleModule,
38
+ NetworkQualityMonitor: () => NetworkQualityMonitor,
39
+ RealtimeModule: () => RealtimeModule,
40
+ TaphubAuthError: () => TaphubAuthError,
41
+ TaphubClient: () => TaphubClient,
42
+ TaphubError: () => TaphubError,
43
+ TaphubEventBus: () => TaphubEventBus,
44
+ TaphubNetworkError: () => TaphubNetworkError,
45
+ TaphubServerError: () => TaphubServerError,
46
+ TaphubSlippageError: () => TaphubSlippageError,
47
+ TaphubStorage: () => TaphubStorage,
48
+ TaphubValidationError: () => TaphubValidationError,
49
+ UserModule: () => UserModule,
50
+ adaptiveSimpson: () => adaptiveSimpson,
51
+ autoDetectStorage: () => autoDetectStorage,
52
+ calculateProbWin: () => calculateProbWin,
53
+ calculateProbWin_v2: () => calculateProbWin_v2,
54
+ errorFunction: () => errorFunction,
55
+ isCancelled: () => isCancelled,
56
+ isLoss: () => isLoss,
57
+ isPending: () => isPending,
58
+ isTerminal: () => isTerminal,
59
+ isWin: () => isWin,
60
+ normalCDF: () => normalCDF,
61
+ normalPDF: () => normalPDF
62
+ });
63
+ module.exports = __toCommonJS(index_exports);
64
+
65
+ // src/errors/index.ts
66
+ var TaphubError = class _TaphubError extends Error {
67
+ code;
68
+ details;
69
+ constructor(message, options) {
70
+ super(message);
71
+ this.name = "TaphubError";
72
+ this.code = options.code;
73
+ this.details = options.details;
74
+ Object.setPrototypeOf(this, _TaphubError.prototype);
75
+ }
76
+ };
77
+ var TaphubAuthError = class _TaphubAuthError extends TaphubError {
78
+ constructor(message, options) {
79
+ super(message, options);
80
+ this.name = "TaphubAuthError";
81
+ Object.setPrototypeOf(this, _TaphubAuthError.prototype);
82
+ }
83
+ };
84
+ var TaphubNetworkError = class _TaphubNetworkError extends TaphubError {
85
+ constructor(message, options) {
86
+ super(message, options);
87
+ this.name = "TaphubNetworkError";
88
+ Object.setPrototypeOf(this, _TaphubNetworkError.prototype);
89
+ }
90
+ };
91
+ var TaphubValidationError = class _TaphubValidationError extends TaphubError {
92
+ constructor(message, options) {
93
+ super(message, options);
94
+ this.name = "TaphubValidationError";
95
+ Object.setPrototypeOf(this, _TaphubValidationError.prototype);
96
+ }
97
+ };
98
+ var TaphubServerError = class _TaphubServerError extends TaphubError {
99
+ constructor(message, options) {
100
+ super(message, options);
101
+ this.name = "TaphubServerError";
102
+ Object.setPrototypeOf(this, _TaphubServerError.prototype);
103
+ }
104
+ };
105
+ var TaphubSlippageError = class _TaphubSlippageError extends TaphubValidationError {
106
+ clientCoef;
107
+ serverCoef;
108
+ slippage;
109
+ constructor(message, options) {
110
+ super(message, { code: options.code, details: options.details });
111
+ this.name = "TaphubSlippageError";
112
+ this.clientCoef = options.clientCoef;
113
+ this.serverCoef = options.serverCoef;
114
+ this.slippage = options.slippage;
115
+ Object.setPrototypeOf(this, _TaphubSlippageError.prototype);
116
+ }
117
+ };
118
+
119
+ // src/events/bus.ts
120
+ var TaphubEventBus = class {
121
+ target = new EventTarget();
122
+ on(event, handler) {
123
+ this.target.addEventListener(event, handler);
124
+ }
125
+ off(event, handler) {
126
+ this.target.removeEventListener(event, handler);
127
+ }
128
+ emit(event, payload) {
129
+ this.target.dispatchEvent(new CustomEvent(event, { detail: payload }));
130
+ }
131
+ };
132
+
133
+ // src/modules/auth/normalise.ts
134
+ function normaliseGoogleResponse(body) {
135
+ return {
136
+ accessToken: body.access_token,
137
+ user: {
138
+ id: body.user.id,
139
+ displayName: body.user.name ?? "",
140
+ agencyUid: body.user.agency_uid ?? null,
141
+ currency: body.user.currency ?? "",
142
+ wallet: {
143
+ id: body.user.default_wallet?.id ?? null,
144
+ balance: body.user.default_wallet?.amount ?? "0",
145
+ currency: body.user.default_wallet?.currency ?? "",
146
+ isEnabled: body.user.default_wallet?.is_enable ?? false
147
+ }
148
+ },
149
+ isDemo: false
150
+ };
151
+ }
152
+ function normaliseDemoResponse(body) {
153
+ return {
154
+ accessToken: body.access_token,
155
+ user: {
156
+ id: body.user.id,
157
+ displayName: body.user.username ?? "",
158
+ agencyUid: null,
159
+ currency: body.user.currency ?? "",
160
+ wallet: {
161
+ id: null,
162
+ balance: body.user.balance ?? "0",
163
+ currency: body.user.currency ?? "",
164
+ isEnabled: true
165
+ }
166
+ },
167
+ isDemo: body.demo ?? true
168
+ };
169
+ }
170
+ function normaliseCreateDemoUserResponse(body) {
171
+ const node = body.createDemoUser;
172
+ if (!node || typeof node.token !== "string" || node.token === "") {
173
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
174
+ }
175
+ if (!node.user || typeof node.user.id !== "string" || node.user.id === "") {
176
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
177
+ }
178
+ return {
179
+ accessToken: node.token,
180
+ user: {
181
+ id: node.user.id,
182
+ displayName: node.user.username ?? "",
183
+ agencyUid: null,
184
+ currency: "",
185
+ wallet: {
186
+ id: null,
187
+ balance: node.user.balance ?? "0",
188
+ currency: "",
189
+ isEnabled: true
190
+ }
191
+ },
192
+ isDemo: node.user.is_demo
193
+ };
194
+ }
195
+ function normaliseUserLoginResponse(body) {
196
+ const node = body.userLogin;
197
+ if (!node || typeof node.accessToken !== "string" || node.accessToken === "") {
198
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
199
+ }
200
+ if (!node.user || typeof node.user.id !== "string" || node.user.id === "") {
201
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
202
+ }
203
+ const wallet = node.user.defaultWallet;
204
+ return {
205
+ accessToken: node.accessToken,
206
+ user: {
207
+ id: node.user.id,
208
+ displayName: node.user.name,
209
+ agencyUid: node.user.agencyUid,
210
+ currency: node.user.currency,
211
+ wallet: {
212
+ id: wallet.id,
213
+ balance: String(wallet.amount),
214
+ currency: wallet.currency,
215
+ isEnabled: wallet.isEnable
216
+ }
217
+ },
218
+ isDemo: false
219
+ };
220
+ }
221
+ function validateLoginBody(body) {
222
+ if (typeof body.access_token !== "string" || body.access_token === "") {
223
+ throw new TaphubServerError("Invalid response from server", {
224
+ code: "InvalidResponse"
225
+ });
226
+ }
227
+ if (!body.user || typeof body.user.id !== "string") {
228
+ throw new TaphubServerError("Invalid response from server", {
229
+ code: "InvalidResponse"
230
+ });
231
+ }
232
+ }
233
+
234
+ // src/modules/auth/queries.ts
235
+ var CREATE_DEMO_USER_MUTATION = `
236
+ mutation CreateDemoUser($username: String) {
237
+ createDemoUser(username: $username) {
238
+ token
239
+ user {
240
+ id
241
+ username
242
+ wallet_address
243
+ balance
244
+ is_demo
245
+ }
246
+ }
247
+ }
248
+ `;
249
+ var USER_LOGIN_MUTATION = `
250
+ mutation UserLogin($input: LoginInput!) {
251
+ userLogin(input: $input) {
252
+ accessToken
253
+ user {
254
+ id
255
+ agencyUid
256
+ name
257
+ currency
258
+ defaultWallet {
259
+ id
260
+ amount
261
+ currency
262
+ isEnable
263
+ }
264
+ }
265
+ }
266
+ }
267
+ `;
268
+
269
+ // src/modules/auth/index.ts
270
+ var AuthModule = class {
271
+ #rest;
272
+ #graphql;
273
+ #graphqlUser;
274
+ #setToken;
275
+ #agencyId;
276
+ #onLoginSuccess;
277
+ #onLogout;
278
+ constructor(deps) {
279
+ this.#rest = deps.rest;
280
+ this.#graphql = deps.graphql;
281
+ this.#graphqlUser = deps.graphqlUser;
282
+ this.#setToken = deps.setToken;
283
+ this.#agencyId = deps.agencyId;
284
+ this.#onLoginSuccess = deps.onLoginSuccess;
285
+ this.#onLogout = deps.onLogout;
286
+ }
287
+ /** @deprecated REST auth endpoints are being retired; use `loginWithSession` instead. */
288
+ async loginWithGoogle(idToken) {
289
+ const body = {
290
+ id_token: idToken,
291
+ aid: this.#agencyId
292
+ };
293
+ const response = await this.#rest.post("/auth/google", body);
294
+ validateLoginBody(response);
295
+ const result = normaliseGoogleResponse(response);
296
+ this.#setToken(result.accessToken, { isDemo: false });
297
+ if (this.#onLoginSuccess) {
298
+ try {
299
+ await this.#onLoginSuccess();
300
+ } catch {
301
+ }
302
+ }
303
+ return result;
304
+ }
305
+ /** @deprecated REST auth endpoints are being retired; use `loginWithSession` or `createDemoUser` instead. */
306
+ async demoLogin(username) {
307
+ const body = username !== void 0 ? { aid: this.#agencyId, username } : { aid: this.#agencyId };
308
+ const response = await this.#rest.post("/demo-login", body);
309
+ validateLoginBody(response);
310
+ const result = normaliseDemoResponse(response);
311
+ this.#setToken(result.accessToken, { isDemo: true });
312
+ if (this.#onLoginSuccess) {
313
+ try {
314
+ await this.#onLoginSuccess();
315
+ } catch {
316
+ }
317
+ }
318
+ return result;
319
+ }
320
+ async createDemoUser(username, opts) {
321
+ const variables = username !== void 0 ? { username } : {};
322
+ const body = await this.#graphql.request(
323
+ CREATE_DEMO_USER_MUTATION,
324
+ variables,
325
+ opts
326
+ );
327
+ const result = normaliseCreateDemoUserResponse(body);
328
+ this.#setToken(result.accessToken, { isDemo: true });
329
+ if (this.#onLoginSuccess) {
330
+ try {
331
+ await this.#onLoginSuccess();
332
+ } catch {
333
+ }
334
+ }
335
+ return result;
336
+ }
337
+ async loginWithSession(sessionToken, opts) {
338
+ const variables = { input: { sessionToken, agencyId: this.#agencyId } };
339
+ const body = await this.#graphqlUser.request(
340
+ USER_LOGIN_MUTATION,
341
+ variables,
342
+ opts
343
+ );
344
+ const result = normaliseUserLoginResponse(body);
345
+ this.#setToken(result.accessToken, { isDemo: false });
346
+ if (this.#onLoginSuccess) {
347
+ try {
348
+ await this.#onLoginSuccess();
349
+ } catch {
350
+ }
351
+ }
352
+ return result;
353
+ }
354
+ async logout() {
355
+ this.#setToken(null);
356
+ if (this.#onLogout) {
357
+ try {
358
+ await this.#onLogout();
359
+ } catch {
360
+ }
361
+ }
362
+ }
363
+ };
364
+
365
+ // src/modules/bid/normalise.ts
366
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "win", "lose", "cancelled"]);
367
+ var PAST_TENSE_MAP = {
368
+ won: "win",
369
+ lost: "lose"
370
+ };
371
+ function coerceStatus(raw) {
372
+ if (VALID_STATUSES.has(raw)) return raw;
373
+ const mapped = PAST_TENSE_MAP[raw];
374
+ if (mapped) return mapped;
375
+ console.warn(`Unknown bid status "${raw}", defaulting to "pending"`);
376
+ return "pending";
377
+ }
378
+ function normaliseBid(node) {
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 === "") {
380
+ throw new TaphubServerError("Invalid response from server", {
381
+ code: "InvalidResponse"
382
+ });
383
+ }
384
+ return {
385
+ id: node.id,
386
+ userId: node.user_id,
387
+ gameId: node.game_id,
388
+ currency: node.currency,
389
+ amount: node.amount,
390
+ coefficient: node.coefficient,
391
+ time1: node.time1,
392
+ time2: node.time2,
393
+ price1: node.price1,
394
+ price2: node.price2,
395
+ status: coerceStatus(node.status),
396
+ payout: node.payout || null,
397
+ slippage: node.slippage,
398
+ createdAt: node.created_at || null
399
+ };
400
+ }
401
+ function normaliseBids(list) {
402
+ if (!Array.isArray(list)) {
403
+ throw new TaphubServerError("Invalid response from server", {
404
+ code: "InvalidResponse"
405
+ });
406
+ }
407
+ return list.map(normaliseBid);
408
+ }
409
+
410
+ // src/modules/bid/queries.ts
411
+ var PLACE_BID_MUTATION = `mutation PlaceBid($input: PlaceBidInput!) {
412
+ placeBid(input: $input) {
413
+ id user_id game_id currency amount coefficient time1 time2 price1 price2 status payout slippage created_at
414
+ }
415
+ }`;
416
+ var MY_BIDS_QUERY = `query MyBids($status: String, $limit: Int, $offset: Int, $gameId: ID) {
417
+ myBids(status: $status, limit: $limit, offset: $offset, gameId: $gameId) {
418
+ id user_id game_id currency amount coefficient time1 time2 price1 price2 status payout slippage created_at
419
+ }
420
+ }`;
421
+ var CANCEL_BID_MUTATION = `mutation CancelBid($input: CancelBidInput!) {
422
+ cancelBid(input: $input) {
423
+ bid { id user_id game_id currency amount coefficient time1 time2 price1 price2 status cancelled payout slippage created_at }
424
+ refund_amount
425
+ new_balance
426
+ }
427
+ }`;
428
+
429
+ // src/modules/bid/classifiers.ts
430
+ var isPending = (bid) => bid.status === "pending";
431
+ var isTerminal = (bid) => bid.status !== "pending";
432
+ var isWin = (bid) => bid.status === "win";
433
+ var isLoss = (bid) => bid.status === "lose";
434
+ var isCancelled = (bid) => bid.status === "cancelled";
435
+
436
+ // src/modules/bid/index.ts
437
+ var BidModule = class {
438
+ #graphql;
439
+ constructor(deps) {
440
+ this.#graphql = deps.graphql;
441
+ }
442
+ async placeBid(input, opts) {
443
+ const variables = {
444
+ input: {
445
+ game_id: input.gameId,
446
+ // wallet_id is forwarded as-is — grid-api enforces ownership +
447
+ // currency validation. See openspec/bid-260518-wallet-id D1/D8.
448
+ wallet_id: input.walletId,
449
+ time1: input.time1,
450
+ time2: input.time2,
451
+ price1: input.price1,
452
+ price2: input.price2,
453
+ coefficient: input.coefficient,
454
+ amount: input.amount,
455
+ slippage: input.slippage
456
+ }
457
+ };
458
+ try {
459
+ const body = await this.#graphql.request(
460
+ PLACE_BID_MUTATION,
461
+ variables,
462
+ opts
463
+ );
464
+ return normaliseBid(body.placeBid);
465
+ } catch (err) {
466
+ if (err instanceof TaphubValidationError && err.code === "NotExist") {
467
+ throw new TaphubValidationError("Game not found", {
468
+ code: "GameNotFound",
469
+ details: { gameId: input.gameId }
470
+ });
471
+ }
472
+ throw err;
473
+ }
474
+ }
475
+ async cancelBid(bidId, opts) {
476
+ const body = await this.#graphql.request(
477
+ CANCEL_BID_MUTATION,
478
+ { input: { bid_id: bidId } },
479
+ opts
480
+ );
481
+ const { bid, refund_amount, new_balance } = body.cancelBid;
482
+ return {
483
+ bid: normaliseBid(bid),
484
+ refundAmount: refund_amount,
485
+ newBalance: new_balance
486
+ };
487
+ }
488
+ async listBids(opts) {
489
+ const variables = {};
490
+ if (opts?.status !== void 0) {
491
+ variables.status = opts.status;
492
+ }
493
+ if (opts?.limit !== void 0) {
494
+ variables.limit = opts.limit;
495
+ }
496
+ if (opts?.offset !== void 0) {
497
+ variables.offset = opts.offset;
498
+ }
499
+ if (opts?.gameId !== void 0) {
500
+ variables.gameId = opts.gameId;
501
+ }
502
+ const body = await this.#graphql.request(
503
+ MY_BIDS_QUERY,
504
+ variables,
505
+ opts?.signal ? { signal: opts.signal } : void 0
506
+ );
507
+ return normaliseBids(body.myBids);
508
+ }
509
+ };
510
+
511
+ // src/modules/game/normalise.ts
512
+ function normaliseGame(node) {
513
+ if (typeof node.id !== "string" || node.id === "" || typeof node.pair !== "string" || node.pair === "" || typeof node.status !== "string" || node.status === "" || !node.config) {
514
+ throw new TaphubServerError("Invalid response from server", {
515
+ code: "InvalidResponse"
516
+ });
517
+ }
518
+ return {
519
+ id: node.id,
520
+ pair: node.pair,
521
+ status: node.status,
522
+ config: normaliseGameConfig(node.config),
523
+ createdAt: node.created_at || null
524
+ };
525
+ }
526
+ function normaliseGameConfig(node) {
527
+ return {
528
+ gridConfig: normaliseGridConfig(node.gridConfig),
529
+ constraints: normaliseConstraints(node.constraints),
530
+ acceptableBids: node.acceptableBids,
531
+ minBidAmount: node.minBidAmount,
532
+ maxBidAmount: node.maxBidAmount
533
+ };
534
+ }
535
+ function normaliseGridConfig(node) {
536
+ return {
537
+ cellSizeTime: node.cellSizeTime,
538
+ cellSizeValue: node.cellSizeValue,
539
+ candleSize: node.candleSize,
540
+ baseline: node.baseline,
541
+ baselineTime: node.baselineTime
542
+ };
543
+ }
544
+ function normaliseConstraints(node) {
545
+ return {
546
+ minBetTime: node.minBetTime,
547
+ maxBetTime: node.maxBetTime,
548
+ slippage: node.slippage,
549
+ priceMinRange: node.priceMinRange,
550
+ priceMaxRange: node.priceMaxRange,
551
+ coefMults: node.coefMults,
552
+ maxCoef: node.maxCoef
553
+ };
554
+ }
555
+ function normaliseCandles(list) {
556
+ if (!Array.isArray(list)) {
557
+ throw new TaphubServerError("Invalid response from server", {
558
+ code: "InvalidResponse"
559
+ });
560
+ }
561
+ return list.map((c) => ({
562
+ time: c.time,
563
+ o: c.o,
564
+ h: c.h,
565
+ l: c.l,
566
+ c: c.c,
567
+ volatility: c.volatility,
568
+ coefMults: c.coefMults
569
+ }));
570
+ }
571
+ function normaliseGamePairInfo(node) {
572
+ if (typeof node.id !== "string" || node.id === "" || typeof node.pair !== "string" || node.pair === "") {
573
+ throw new TaphubServerError("Invalid response from server", { code: "InvalidResponse" });
574
+ }
575
+ return {
576
+ id: node.id,
577
+ pair: node.pair,
578
+ gameplayId: node.gameplayId,
579
+ gameplayName: node.gameplayName,
580
+ source: node.source,
581
+ gameId: node.gameId ?? null
582
+ };
583
+ }
584
+
585
+ // src/modules/game/queries.ts
586
+ var GAME_QUERY = `query Game($pair: String!, $gameplaySlug: String) {
587
+ game(pair: $pair, gameplaySlug: $gameplaySlug) {
588
+ id pair status created_at
589
+ config {
590
+ gridConfig { cellSizeTime cellSizeValue candleSize baseline baselineTime }
591
+ constraints { minBetTime maxBetTime slippage priceMinRange priceMaxRange coefMults maxCoef }
592
+ acceptableBids minBidAmount maxBidAmount
593
+ }
594
+ }
595
+ }`;
596
+ var CHART_HISTORY_QUERY = `query ChartHistory($gameId: ID!, $limit: Int) {
597
+ chartHistory(gameId: $gameId, limit: $limit) {
598
+ time o h l c volatility coefMults
599
+ }
600
+ }`;
601
+ var BUILDER_AVAILABLE_GAME_PAIRS_QUERY = `query BuilderAvailableGamePairs($gameplayId: ID) {
602
+ builderAvailableGamePairs(gameplayId: $gameplayId) {
603
+ id
604
+ pair
605
+ gameplayId
606
+ gameplayName
607
+ source
608
+ gameId
609
+ }
610
+ }`;
611
+
612
+ // src/modules/game/index.ts
613
+ var GameModule = class {
614
+ #graphql;
615
+ constructor(deps) {
616
+ this.#graphql = deps.graphql;
617
+ }
618
+ async get(pair, opts) {
619
+ const variables = { pair };
620
+ if (opts?.gameplaySlug) variables.gameplaySlug = opts.gameplaySlug;
621
+ let body;
622
+ try {
623
+ body = await this.#graphql.request(GAME_QUERY, variables, opts);
624
+ } catch (err) {
625
+ if (err instanceof TaphubValidationError && err.code === "NotExist") {
626
+ throw new TaphubValidationError("Game not found for pair", {
627
+ code: "GameNotFound",
628
+ details: { pair }
629
+ });
630
+ }
631
+ throw err;
632
+ }
633
+ if (body.game === null) {
634
+ throw new TaphubValidationError("Game not found for pair", {
635
+ code: "GameNotFound",
636
+ details: { pair }
637
+ });
638
+ }
639
+ return normaliseGame(body.game);
640
+ }
641
+ /**
642
+ * Returns available game pairs, optionally filtered by gameplay.
643
+ *
644
+ * When called with a valid JWT (authenticated builder), each entry includes
645
+ * `gameId` — use it directly as the MQTT topic `game/{gameId}/candle`.
646
+ *
647
+ * @example
648
+ * const pairs = await client.game.availableGamePairs({ gameplayId: 'taptrading' });
649
+ * const game = await client.game.get(pairs[0].pair, { gameplaySlug: pairs[0].gameplayId });
650
+ * const ch = client.realtime?.subscribe(pairs[0].gameId ?? game.id, userId);
651
+ */
652
+ async availableGamePairs(opts) {
653
+ const variables = {};
654
+ if (opts?.gameplayId) variables.gameplayId = opts.gameplayId;
655
+ const body = await this.#graphql.request(
656
+ BUILDER_AVAILABLE_GAME_PAIRS_QUERY,
657
+ variables,
658
+ opts
659
+ );
660
+ return body.builderAvailableGamePairs.map(normaliseGamePairInfo);
661
+ }
662
+ async chartHistory(gameId, limit, opts) {
663
+ const variables = { gameId };
664
+ if (limit !== void 0) {
665
+ variables.limit = limit;
666
+ }
667
+ const body = await this.#graphql.request(
668
+ CHART_HISTORY_QUERY,
669
+ variables,
670
+ opts
671
+ );
672
+ return normaliseCandles(body.chartHistory);
673
+ }
674
+ };
675
+
676
+ // src/modules/leaderboard/normalise.ts
677
+ function normaliseLeaderboard(rows) {
678
+ if (!Array.isArray(rows)) {
679
+ throw new TaphubServerError("Invalid response from server", {
680
+ code: "InvalidResponse"
681
+ });
682
+ }
683
+ return rows.map((row) => ({
684
+ userId: row.user_id,
685
+ username: row.username,
686
+ rank: row.rank,
687
+ totalBids: row.total_bids,
688
+ totalWins: row.total_wins,
689
+ totalWagered: row.total_wagered,
690
+ totalPayout: row.total_payout,
691
+ gain: row.gain
692
+ }));
693
+ }
694
+
695
+ // src/modules/leaderboard/queries.ts
696
+ var LEADERBOARD_QUERY = `query Leaderboard($period: String!, $sort_by: String!) {
697
+ leaderboard(period: $period, sort_by: $sort_by) {
698
+ user_id username rank total_bids total_wins total_wagered total_payout gain
699
+ }
700
+ }`;
701
+
702
+ // src/modules/leaderboard/index.ts
703
+ var LeaderboardModule = class {
704
+ #graphql;
705
+ constructor(deps) {
706
+ this.#graphql = deps.graphql;
707
+ }
708
+ async list(args) {
709
+ const body = await this.#graphql.request(
710
+ LEADERBOARD_QUERY,
711
+ { period: args.period, sort_by: args.sortBy },
712
+ args.signal ? { signal: args.signal } : void 0
713
+ );
714
+ return normaliseLeaderboard(body.leaderboard);
715
+ }
716
+ };
717
+
718
+ // src/modules/locale/normalise.ts
719
+ function normaliseLocaleResponse(node) {
720
+ let translations = null;
721
+ if (node.translations !== null) {
722
+ try {
723
+ translations = JSON.parse(node.translations);
724
+ } catch (err) {
725
+ throw new TaphubValidationError("Malformed translations JSON string from backend", {
726
+ code: "InvalidLocaleResponse",
727
+ details: {
728
+ lang: node.lang,
729
+ version: node.version,
730
+ parseError: err instanceof Error ? err.message : String(err)
731
+ }
732
+ });
733
+ }
734
+ }
735
+ return {
736
+ lang: node.lang,
737
+ version: node.version,
738
+ notModified: node.notModified,
739
+ translations
740
+ };
741
+ }
742
+ function normaliseRefreshLocalesPayload(node) {
743
+ let versions;
744
+ try {
745
+ versions = JSON.parse(node.versions);
746
+ } catch (err) {
747
+ throw new TaphubValidationError("Malformed versions JSON string from backend", {
748
+ code: "InvalidRefreshLocalesPayload",
749
+ details: {
750
+ parseError: err instanceof Error ? err.message : String(err)
751
+ }
752
+ });
753
+ }
754
+ return {
755
+ refreshed: node.refreshed,
756
+ versions
757
+ };
758
+ }
759
+
760
+ // src/modules/locale/queries.ts
761
+ var LOCALES_QUERY = `query Locales($input: LocalesInput!) {
762
+ locales(input: $input) {
763
+ lang
764
+ version
765
+ notModified
766
+ translations
767
+ }
768
+ }`;
769
+ var REFRESH_LOCALES_MUTATION = `mutation RefreshLocales {
770
+ refreshLocales {
771
+ refreshed
772
+ versions
773
+ }
774
+ }`;
775
+
776
+ // src/modules/locale/index.ts
777
+ var LocaleModule = class {
778
+ #graphql;
779
+ constructor(deps) {
780
+ this.#graphql = deps.graphql;
781
+ }
782
+ /**
783
+ * Fetch translations for a single language.
784
+ *
785
+ * Pass the last-seen `version` as `knownVersion` to opt into not-modified
786
+ * short-circuit semantics: backend returns `{notModified: true, translations: null}`
787
+ * when the cached body still matches, and the caller keeps prior state.
788
+ *
789
+ * Errors surface as TaphubError subclasses with `extensions.code` codes from
790
+ * the backend (e.g. `LangNotSupported`, `InsufficientUpstream`, `FeatureDisabled`).
791
+ * Caller decides the fallback strategy.
792
+ */
793
+ async get(lang, knownVersion, opts) {
794
+ const input = { lang };
795
+ if (knownVersion !== void 0 && knownVersion !== "") {
796
+ input.knownVersion = knownVersion;
797
+ }
798
+ const body = await this.#graphql.request(LOCALES_QUERY, { input }, opts);
799
+ return normaliseLocaleResponse(body.locales);
800
+ }
801
+ /**
802
+ * Trigger an admin refresh — backend re-pulls the source Sheet, invalidates
803
+ * its cache, and writes fresh entries for every supported language.
804
+ *
805
+ * Requires `X-API-Key` header equal to backend `InternalApiKey`. The transport
806
+ * layer is responsible for attaching the header; this method does not handle
807
+ * auth concerns directly.
808
+ */
809
+ async refresh(opts) {
810
+ const body = await this.#graphql.request(
811
+ REFRESH_LOCALES_MUTATION,
812
+ {},
813
+ opts
814
+ );
815
+ return normaliseRefreshLocalesPayload(body.refreshLocales);
816
+ }
817
+ };
818
+
819
+ // src/transport/mqtt/index.ts
820
+ var import_mqtt = __toESM(require("mqtt"));
821
+ var GAME_SCOPED_SUFFIXES = ["candle", "config", "ideal_config"];
822
+ var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
823
+ var TOPIC_PREFIX = "game";
824
+ function topicFor(gameId, suffix, userId) {
825
+ if (USER_SCOPED_SUFFIXES.includes(suffix)) {
826
+ return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
827
+ }
828
+ return `${TOPIC_PREFIX}/${gameId}/${suffix}`;
829
+ }
830
+ function normaliseUserId(userId) {
831
+ return userId && userId !== "" ? userId : null;
832
+ }
833
+ function userScopedTopicsFor(gameId, userId) {
834
+ return USER_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s, userId));
835
+ }
836
+ function subKey(gameId, userId) {
837
+ return `${gameId}::${userId ?? ""}`;
838
+ }
839
+ function createMqttTransport(endpoint, opts = {}) {
840
+ let client = null;
841
+ const subscriptions = /* @__PURE__ */ new Map();
842
+ const { onLifecycle } = opts;
843
+ let connectStartedAt = 0;
844
+ function fireLifecycle(event) {
845
+ if (!onLifecycle) return;
846
+ try {
847
+ onLifecycle(event);
848
+ } catch {
849
+ }
850
+ }
851
+ function ensureConnected() {
852
+ if (client) return client;
853
+ connectStartedAt = Date.now();
854
+ client = import_mqtt.default.connect(endpoint, {
855
+ clean: true,
856
+ reconnectPeriod: 2e3,
857
+ connectTimeout: 1e4,
858
+ keepalive: 6
859
+ });
860
+ let lastPingreqAt = 0;
861
+ client.on("packetsend", (packet) => {
862
+ if (packet.cmd === "pingreq") {
863
+ lastPingreqAt = Date.now();
864
+ }
865
+ });
866
+ client.on("packetreceive", (packet) => {
867
+ if (packet.cmd === "pingresp" && lastPingreqAt > 0) {
868
+ const rttMs = Date.now() - lastPingreqAt;
869
+ lastPingreqAt = 0;
870
+ fireLifecycle({ kind: "ping", rttMs });
871
+ }
872
+ });
873
+ client.on("connect", () => {
874
+ fireLifecycle({ kind: "connect", rttMs: Date.now() - connectStartedAt });
875
+ });
876
+ client.on("reconnect", () => {
877
+ const rttMs = Date.now() - connectStartedAt;
878
+ connectStartedAt = Date.now();
879
+ fireLifecycle({ kind: "reconnect", rttMs });
880
+ });
881
+ client.on("close", () => {
882
+ fireLifecycle({ kind: "disconnect" });
883
+ });
884
+ client.on("error", (err) => {
885
+ fireLifecycle({ kind: "error", err });
886
+ });
887
+ client.on("message", (receivedTopic, message) => {
888
+ let matched;
889
+ for (const sub of subscriptions.values()) {
890
+ if (sub.topics.includes(receivedTopic)) {
891
+ matched = sub;
892
+ break;
893
+ }
894
+ }
895
+ if (!matched) return;
896
+ let payload;
897
+ try {
898
+ payload = JSON.parse(message.toString());
899
+ } catch {
900
+ matched.onError(new Error(`Invalid JSON on topic ${receivedTopic}`));
901
+ return;
902
+ }
903
+ matched.onMessage(matched.gameId, receivedTopic, payload);
904
+ });
905
+ return client;
906
+ }
907
+ function entriesForGame(gameId) {
908
+ const out = [];
909
+ for (const sub of subscriptions.values()) {
910
+ if (sub.gameId === gameId) out.push(sub);
911
+ }
912
+ return out;
913
+ }
914
+ return {
915
+ subscribe(gameId, userId, onMessage, onError) {
916
+ const cleanUserId = normaliseUserId(userId);
917
+ const key = subKey(gameId, cleanUserId);
918
+ if (subscriptions.has(key)) return;
919
+ const mqttClient = ensureConnected();
920
+ const fullTopics = [
921
+ ...GAME_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s)),
922
+ ...cleanUserId ? userScopedTopicsFor(gameId, cleanUserId) : []
923
+ ];
924
+ subscriptions.set(key, {
925
+ gameId,
926
+ topics: fullTopics,
927
+ userId: cleanUserId,
928
+ onMessage,
929
+ onError
930
+ });
931
+ for (const t of fullTopics) {
932
+ mqttClient.subscribe(t, { qos: t.endsWith("candle") ? 0 : 1 });
933
+ }
934
+ },
935
+ unsubscribeAll(gameId, userId) {
936
+ const matches = entriesForGame(gameId);
937
+ if (matches.length === 0) return;
938
+ let target;
939
+ if (userId !== void 0) {
940
+ const cleanUserId = normaliseUserId(userId);
941
+ target = matches.find((m) => m.userId === cleanUserId);
942
+ if (!target) return;
943
+ } else if (matches.length === 1) {
944
+ target = matches[0];
945
+ } else {
946
+ const activeUserIds = matches.map((m) => m.userId ?? "<anonymous>");
947
+ throw new TaphubError(
948
+ `Ambiguous unsubscribe for gameId='${gameId}': multiple active userIds (${activeUserIds.join(", ")}). Pass userId explicitly to disambiguate.`,
949
+ { code: "AmbiguousUnsubscribe", details: { gameId, activeUserIds } }
950
+ );
951
+ }
952
+ if (client) {
953
+ for (const t of target.topics) {
954
+ client.unsubscribe(t);
955
+ }
956
+ }
957
+ subscriptions.delete(subKey(target.gameId, target.userId));
958
+ },
959
+ close() {
960
+ if (client) {
961
+ for (const sub of subscriptions.values()) {
962
+ for (const t of sub.topics) {
963
+ client.unsubscribe(t);
964
+ }
965
+ }
966
+ }
967
+ subscriptions.clear();
968
+ if (client) {
969
+ client.end(true);
970
+ client = null;
971
+ }
972
+ }
973
+ };
974
+ }
975
+
976
+ // src/modules/realtime/index.ts
977
+ var import_eventemitter32 = __toESM(require("eventemitter3"));
978
+
979
+ // src/modules/realtime/GameChannel.ts
980
+ var import_eventemitter3 = __toESM(require("eventemitter3"));
981
+ var GameChannel = class extends import_eventemitter3.default {
982
+ gameId;
983
+ constructor(gameId) {
984
+ super();
985
+ this.gameId = gameId;
986
+ }
987
+ };
988
+
989
+ // src/modules/realtime/index.ts
990
+ var TOPIC_SUFFIX_CANDLE = "candle";
991
+ var TOPIC_SUFFIX_BID_RESULT = "bid_result";
992
+ var TOPIC_SUFFIX_BALANCE_UPDATE = "balance_update";
993
+ var TOPIC_SUFFIX_CONFIG = "config";
994
+ var TOPIC_SUFFIX_IDEAL_CONFIG = "ideal_config";
995
+ function mapWireCandle(raw) {
996
+ const p = raw;
997
+ const c = p.candle ?? p;
998
+ const result = {
999
+ type: p.type,
1000
+ time: c.time,
1001
+ o: c.o,
1002
+ h: c.h,
1003
+ l: c.l,
1004
+ c: c.c
1005
+ };
1006
+ if (c.volatility !== void 0) result.volatility = c.volatility;
1007
+ if (c.coef_mults !== void 0) result.coefMults = c.coef_mults;
1008
+ if (c.coefMults !== void 0) result.coefMults = c.coefMults;
1009
+ return result;
1010
+ }
1011
+ function mapWireBidResult(raw) {
1012
+ const p = raw;
1013
+ switch (p.type) {
1014
+ case "accepted": {
1015
+ const data = {
1016
+ bid: p.bid
1017
+ };
1018
+ if (typeof p.balance === "string") data.balance = p.balance;
1019
+ return { event: "bidAccepted", data };
1020
+ }
1021
+ case "won": {
1022
+ const data = {
1023
+ bidId: p.bidId,
1024
+ payout: p.payout,
1025
+ userId: p.user_id
1026
+ };
1027
+ if (typeof p.balance === "string") data.balance = p.balance;
1028
+ return { event: "bidWon", data };
1029
+ }
1030
+ case "lost": {
1031
+ return {
1032
+ event: "bidLost",
1033
+ data: { bidId: p.bidId }
1034
+ };
1035
+ }
1036
+ default:
1037
+ console.warn(`Unknown bid_result type "${String(p.type)}"`);
1038
+ return null;
1039
+ }
1040
+ }
1041
+ function mapWireBalanceUpdate(raw) {
1042
+ const p = raw;
1043
+ return {
1044
+ userId: p.userId,
1045
+ balance: p.balance
1046
+ };
1047
+ }
1048
+ function mapWireConfig(raw) {
1049
+ const p = raw;
1050
+ return {
1051
+ minBidAmount: p.min_bid_amount,
1052
+ maxBidAmount: p.max_bid_amount,
1053
+ acceptableBids: p.acceptable_bids
1054
+ };
1055
+ }
1056
+ function mapWireIdealConfig(raw) {
1057
+ const p = raw;
1058
+ return {
1059
+ cellSizeValue: p.cellSizeValue,
1060
+ currentPrice: p.currentPrice,
1061
+ reason: p.reason ?? "volatility_shift"
1062
+ };
1063
+ }
1064
+ function mapWireToEvent(topic, payload) {
1065
+ if (topic.endsWith(`/${TOPIC_SUFFIX_CANDLE}`)) {
1066
+ return { event: "candle", data: mapWireCandle(payload) };
1067
+ }
1068
+ if (topic.endsWith(`/${TOPIC_SUFFIX_BID_RESULT}`)) {
1069
+ return mapWireBidResult(payload);
1070
+ }
1071
+ if (topic.endsWith(`/${TOPIC_SUFFIX_BALANCE_UPDATE}`)) {
1072
+ return { event: "balanceUpdate", data: mapWireBalanceUpdate(payload) };
1073
+ }
1074
+ if (topic.endsWith(`/${TOPIC_SUFFIX_IDEAL_CONFIG}`)) {
1075
+ return { event: "idealConfigUpdate", data: mapWireIdealConfig(payload) };
1076
+ }
1077
+ if (topic.endsWith(`/${TOPIC_SUFFIX_CONFIG}`)) {
1078
+ return { event: "configUpdate", data: mapWireConfig(payload) };
1079
+ }
1080
+ return null;
1081
+ }
1082
+ function normaliseUserId2(userId) {
1083
+ return userId && userId !== "" ? userId : null;
1084
+ }
1085
+ function subKey2(gameId, userId) {
1086
+ return `${gameId}::${userId ?? ""}`;
1087
+ }
1088
+ var RealtimeModule = class extends import_eventemitter32.default {
1089
+ #transport;
1090
+ #entries = /* @__PURE__ */ new Map();
1091
+ constructor(mqttEndpointOrOptions) {
1092
+ super();
1093
+ if (typeof mqttEndpointOrOptions === "string") {
1094
+ this.#transport = createMqttTransport(mqttEndpointOrOptions);
1095
+ } else {
1096
+ this.#transport = mqttEndpointOrOptions.transport ?? createMqttTransport(mqttEndpointOrOptions.mqttEndpoint, {
1097
+ onLifecycle: mqttEndpointOrOptions.onMqttLifecycle
1098
+ });
1099
+ }
1100
+ }
1101
+ /** @internal Access transport for testing */
1102
+ get _transport() {
1103
+ return this.#transport;
1104
+ }
1105
+ subscribe(gameId, userId) {
1106
+ const cleanUserId = normaliseUserId2(userId);
1107
+ const key = subKey2(gameId, cleanUserId);
1108
+ const existing = this.#entries.get(key);
1109
+ if (existing) {
1110
+ existing.refcount += 1;
1111
+ return existing.channel;
1112
+ }
1113
+ const channel = new GameChannel(gameId);
1114
+ const onMessage = (_gid, topic, payload) => {
1115
+ const mapped = mapWireToEvent(topic, payload);
1116
+ if (mapped) {
1117
+ if (mapped.event === "candle") channel.emit("candle", mapped.data);
1118
+ else if (mapped.event === "bidAccepted") channel.emit("bidAccepted", mapped.data);
1119
+ else if (mapped.event === "bidWon") channel.emit("bidWon", mapped.data);
1120
+ else if (mapped.event === "bidLost") channel.emit("bidLost", mapped.data);
1121
+ else if (mapped.event === "balanceUpdate") channel.emit("balanceUpdate", mapped.data);
1122
+ else if (mapped.event === "configUpdate") channel.emit("configUpdate", mapped.data);
1123
+ else if (mapped.event === "idealConfigUpdate")
1124
+ channel.emit("idealConfigUpdate", mapped.data);
1125
+ }
1126
+ };
1127
+ const onError = (err) => {
1128
+ channel.emit("error", err);
1129
+ };
1130
+ this.#entries.set(key, {
1131
+ gameId,
1132
+ userId: cleanUserId,
1133
+ channel,
1134
+ refcount: 1,
1135
+ onMessage,
1136
+ onError
1137
+ });
1138
+ this.#transport.subscribe(gameId, cleanUserId, onMessage, onError);
1139
+ return channel;
1140
+ }
1141
+ unsubscribe(gameId, userId) {
1142
+ const matches = [];
1143
+ for (const entry of this.#entries.values()) {
1144
+ if (entry.gameId === gameId) matches.push(entry);
1145
+ }
1146
+ if (matches.length === 0) return;
1147
+ let target;
1148
+ if (userId !== void 0) {
1149
+ const cleanUserId = normaliseUserId2(userId);
1150
+ target = matches.find((m) => m.userId === cleanUserId);
1151
+ if (!target) return;
1152
+ } else if (matches.length === 1) {
1153
+ target = matches[0];
1154
+ } else {
1155
+ const activeUserIds = matches.map((m) => m.userId ?? "<anonymous>");
1156
+ throw new TaphubError(
1157
+ `Ambiguous unsubscribe for gameId='${gameId}': multiple active userIds (${activeUserIds.join(", ")}). Pass userId explicitly to disambiguate.`,
1158
+ { code: "AmbiguousUnsubscribe", details: { gameId, activeUserIds } }
1159
+ );
1160
+ }
1161
+ target.refcount -= 1;
1162
+ if (target.refcount > 0) return;
1163
+ const key = subKey2(target.gameId, target.userId);
1164
+ this.#entries.delete(key);
1165
+ target.channel.removeAllListeners();
1166
+ this.#transport.unsubscribeAll(gameId, target.userId);
1167
+ }
1168
+ disconnect() {
1169
+ for (const entry of this.#entries.values()) {
1170
+ entry.channel.removeAllListeners();
1171
+ }
1172
+ this.#entries.clear();
1173
+ this.#transport.close();
1174
+ }
1175
+ };
1176
+
1177
+ // src/modules/user/normalise.ts
1178
+ function normaliseMeResponse(body) {
1179
+ const me = body.me;
1180
+ if (typeof me.id !== "string" || me.id === "" || typeof me.username !== "string" || me.username === "" || typeof me.balance !== "string" || me.balance === "" || typeof me.is_demo !== "boolean") {
1181
+ throw new TaphubServerError("Invalid response from server", {
1182
+ code: "InvalidResponse"
1183
+ });
1184
+ }
1185
+ const walletAddress = me.wallet_address || null;
1186
+ return {
1187
+ id: me.id,
1188
+ username: me.username,
1189
+ walletAddress,
1190
+ balance: me.balance,
1191
+ isDemo: me.is_demo
1192
+ };
1193
+ }
1194
+ function normaliseCurrencies(body) {
1195
+ if (!Array.isArray(body)) {
1196
+ throw new TaphubServerError("Invalid response from server", {
1197
+ code: "InvalidResponse"
1198
+ });
1199
+ }
1200
+ return body.map((entry) => ({
1201
+ code: entry.code,
1202
+ unit: entry.unit,
1203
+ unitSymbol: entry.unit_symbol
1204
+ }));
1205
+ }
1206
+ function normaliseWalletNode(node) {
1207
+ if (node === null || typeof node !== "object") {
1208
+ throw new TaphubServerError("Invalid response from server", {
1209
+ code: "InvalidResponse"
1210
+ });
1211
+ }
1212
+ const w = node;
1213
+ if (typeof w.id !== "string" || w.id === "" || typeof w.amount !== "string" || w.amount === "" || typeof w.currency !== "string" || w.currency === "" || typeof w.isEnable !== "boolean") {
1214
+ throw new TaphubServerError("Invalid response from server", {
1215
+ code: "InvalidResponse"
1216
+ });
1217
+ }
1218
+ return {
1219
+ id: w.id,
1220
+ amount: w.amount,
1221
+ currency: w.currency,
1222
+ isEnable: w.isEnable
1223
+ };
1224
+ }
1225
+ function normaliseMyWalletsResponse(body) {
1226
+ if (!body || !Array.isArray(body.myWallets)) {
1227
+ throw new TaphubServerError("Invalid response from server", {
1228
+ code: "InvalidResponse"
1229
+ });
1230
+ }
1231
+ return body.myWallets.map(normaliseWalletNode);
1232
+ }
1233
+ function normaliseMyWalletByCurrencyResponse(body) {
1234
+ if (!body) {
1235
+ throw new TaphubServerError("Invalid response from server", {
1236
+ code: "InvalidResponse"
1237
+ });
1238
+ }
1239
+ return normaliseWalletNode(body.myWalletByCurrency);
1240
+ }
1241
+
1242
+ // src/modules/user/queries.ts
1243
+ var ME_QUERY = "query Me { me { id username wallet_address balance is_demo } }";
1244
+ var MY_WALLETS_QUERY = "query MyWallets { myWallets { id amount currency isEnable } }";
1245
+ var MY_WALLET_BY_CURRENCY_QUERY = "query MyWalletByCurrency($currency: String!) { myWalletByCurrency(currency: $currency) { id amount currency isEnable } }";
1246
+
1247
+ // src/modules/user/index.ts
1248
+ var UserModule = class {
1249
+ #rest;
1250
+ #graphql;
1251
+ #graphqlUser;
1252
+ #currencies = null;
1253
+ constructor(deps) {
1254
+ this.#rest = deps.rest;
1255
+ this.#graphql = deps.graphql;
1256
+ this.#graphqlUser = deps.graphqlUser;
1257
+ }
1258
+ async me() {
1259
+ const body = await this.#graphql.request(ME_QUERY);
1260
+ return normaliseMeResponse(body);
1261
+ }
1262
+ async wallets(opts) {
1263
+ const body = await this.#graphqlUser.request(
1264
+ MY_WALLETS_QUERY,
1265
+ void 0,
1266
+ opts
1267
+ );
1268
+ return normaliseMyWalletsResponse(body);
1269
+ }
1270
+ async walletByCurrency(currency, opts) {
1271
+ const body = await this.#graphqlUser.request(
1272
+ MY_WALLET_BY_CURRENCY_QUERY,
1273
+ { currency },
1274
+ opts
1275
+ );
1276
+ return normaliseMyWalletByCurrencyResponse(body);
1277
+ }
1278
+ get currencies() {
1279
+ return this.#currencies;
1280
+ }
1281
+ async refreshCurrencies() {
1282
+ const body = await this.#rest.post("/currency/list-enable");
1283
+ const currencies = normaliseCurrencies(body);
1284
+ this.#currencies = currencies;
1285
+ return currencies;
1286
+ }
1287
+ clearCurrencies() {
1288
+ this.#currencies = null;
1289
+ }
1290
+ };
1291
+
1292
+ // src/network/classify.ts
1293
+ var TIMEOUT_MS = 15e3;
1294
+ function isAbortError(err) {
1295
+ if (err === null || err === void 0) return false;
1296
+ if (typeof err !== "object") return false;
1297
+ const name = err.name;
1298
+ return name === "AbortError";
1299
+ }
1300
+ var EMPTY_GQL_ERRORS = (errors) => !errors || Array.isArray(errors) && errors.length === 0;
1301
+ function classifyHttpOutcome(outcome, ctx) {
1302
+ if (outcome.onlineHint === false) {
1303
+ return {
1304
+ ts: ctx.now,
1305
+ rtt: Number.POSITIVE_INFINITY,
1306
+ source: ctx.source,
1307
+ reason: "offline"
1308
+ };
1309
+ }
1310
+ if (outcome.rtt > TIMEOUT_MS) {
1311
+ return {
1312
+ ts: ctx.now,
1313
+ rtt: TIMEOUT_MS,
1314
+ source: ctx.source,
1315
+ reason: "timeout"
1316
+ };
1317
+ }
1318
+ const status = outcome.httpStatus;
1319
+ if (status !== null && status !== void 0) {
1320
+ if (status >= 500) {
1321
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "backend5xx" };
1322
+ }
1323
+ if (status >= 400) {
1324
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "backend4xx" };
1325
+ }
1326
+ if (!EMPTY_GQL_ERRORS(outcome.gqlErrors)) {
1327
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "gqlError" };
1328
+ }
1329
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "ok" };
1330
+ }
1331
+ if (outcome.threw) {
1332
+ return {
1333
+ ts: ctx.now,
1334
+ rtt: outcome.rtt,
1335
+ source: ctx.source,
1336
+ reason: "network"
1337
+ };
1338
+ }
1339
+ return { ts: ctx.now, rtt: outcome.rtt, source: ctx.source, reason: "ok" };
1340
+ }
1341
+ function classifyBackendHealth(samples) {
1342
+ const httpSamples = samples.filter((s) => s.source === "graphql" || s.source === "rest");
1343
+ if (httpSamples.length === 0) return "ok";
1344
+ const total = httpSamples.length;
1345
+ const fiveXxCount = httpSamples.filter((s) => s.reason === "backend5xx").length;
1346
+ const fiveXxRate = fiveXxCount / total;
1347
+ if (fiveXxRate >= 0.5) return "down";
1348
+ if (fiveXxRate >= 0.2) return "degraded";
1349
+ const gqlSamples = httpSamples.filter((s) => s.source === "graphql");
1350
+ if (gqlSamples.length > 0) {
1351
+ const gqlErrorRate = gqlSamples.filter((s) => s.reason === "gqlError").length / gqlSamples.length;
1352
+ if (gqlErrorRate >= 0.3) return "degraded";
1353
+ }
1354
+ return "ok";
1355
+ }
1356
+ var isPoor = (m) => m.rtt > 450 || m.jitter > 150 || m.lossRate > 0.05;
1357
+ var canLeavePoor = (m) => m.rtt < 350 && m.jitter < 120 && m.lossRate < 0.03;
1358
+ var isFairUpper = (rtt) => rtt > 180;
1359
+ var canReachGood = (rtt) => rtt < 120;
1360
+ function classifyNetworkLevel(metrics, current) {
1361
+ if (current === "offline") {
1362
+ return isPoor(metrics) ? "poor" : "fair";
1363
+ }
1364
+ if (current === "good") {
1365
+ if (isPoor(metrics)) return "fair";
1366
+ if (isFairUpper(metrics.rtt)) return "fair";
1367
+ return "good";
1368
+ }
1369
+ if (current === "fair") {
1370
+ if (isPoor(metrics)) return "poor";
1371
+ if (canReachGood(metrics.rtt)) return "good";
1372
+ return "fair";
1373
+ }
1374
+ if (canLeavePoor(metrics)) return "fair";
1375
+ return "poor";
1376
+ }
1377
+
1378
+ // src/network/RttSmoother.ts
1379
+ var EMA_ALPHA = 0.25;
1380
+ var OUTLIER_MULTIPLIER = 3;
1381
+ var DEBOUNCE_DEGRADE = 2;
1382
+ var DEBOUNCE_IMPROVE = 5;
1383
+ var LEVEL_RANK = {
1384
+ good: 0,
1385
+ fair: 1,
1386
+ poor: 2,
1387
+ offline: 3
1388
+ };
1389
+ var RttSmoother = class {
1390
+ ema = 0;
1391
+ level = "good";
1392
+ committed = "good";
1393
+ candidate = "good";
1394
+ candidateCount = 0;
1395
+ overrideOffline = false;
1396
+ consecutiveOutliers = 0;
1397
+ add(rtt, metrics) {
1398
+ if (this.ema > 0 && rtt > OUTLIER_MULTIPLIER * this.ema) {
1399
+ this.consecutiveOutliers += 1;
1400
+ if (this.consecutiveOutliers < 2) return;
1401
+ } else {
1402
+ this.consecutiveOutliers = 0;
1403
+ }
1404
+ this.ema = this.ema === 0 ? rtt : EMA_ALPHA * rtt + (1 - EMA_ALPHA) * this.ema;
1405
+ const next = classifyNetworkLevel(
1406
+ { rtt, jitter: metrics.jitter, lossRate: metrics.lossRate },
1407
+ this.committed
1408
+ );
1409
+ if (next === this.candidate) {
1410
+ this.candidateCount += 1;
1411
+ } else {
1412
+ this.candidate = next;
1413
+ this.candidateCount = 1;
1414
+ }
1415
+ const required = this.requiredSamplesFor(next);
1416
+ if (this.candidateCount >= required) {
1417
+ this.committed = next;
1418
+ }
1419
+ this.level = this.overrideOffline ? "offline" : this.committed;
1420
+ }
1421
+ forceOffline() {
1422
+ this.overrideOffline = true;
1423
+ this.level = "offline";
1424
+ }
1425
+ releaseOffline() {
1426
+ this.overrideOffline = false;
1427
+ this.level = this.committed;
1428
+ }
1429
+ reset() {
1430
+ this.ema = 0;
1431
+ this.committed = "good";
1432
+ this.candidate = "good";
1433
+ this.candidateCount = 0;
1434
+ this.consecutiveOutliers = 0;
1435
+ this.overrideOffline = false;
1436
+ this.level = "good";
1437
+ }
1438
+ requiredSamplesFor(next) {
1439
+ return LEVEL_RANK[next] > LEVEL_RANK[this.committed] ? DEBOUNCE_DEGRADE : DEBOUNCE_IMPROVE;
1440
+ }
1441
+ };
1442
+
1443
+ // src/network/NetworkQualityMonitor.ts
1444
+ var WINDOW_MS = 6e4;
1445
+ var WINDOW_MAX_SAMPLES_PER_SOURCE = 30;
1446
+ var OFFLINE_HOLD_MS = 1e4;
1447
+ var TICK_INTERVAL_MS = 5e3;
1448
+ var COLD_START_REAL_SAMPLE_THRESHOLD = 3;
1449
+ var EFFECTIVE_TYPE_TO_RTT = {
1450
+ "4g": 80,
1451
+ "3g": 250,
1452
+ "2g": 700,
1453
+ "slow-2g": 1500
1454
+ };
1455
+ function emptyBuckets() {
1456
+ return { graphql: [], rest: [], mqtt: [], browser: [], connection: [] };
1457
+ }
1458
+ function isHttpSource(s) {
1459
+ return s === "graphql" || s === "rest";
1460
+ }
1461
+ var NetworkQualityMonitor = class {
1462
+ bus;
1463
+ buckets = emptyBuckets();
1464
+ smoother = new RttSmoother();
1465
+ mqttConnected = false;
1466
+ mqttDisconnectedAt = null;
1467
+ lastSuccessfulHttpAt = null;
1468
+ committedNetwork = "good";
1469
+ committedBackend = "ok";
1470
+ tickHandle = null;
1471
+ onlineListener = null;
1472
+ offlineListener = null;
1473
+ disposed = false;
1474
+ constructor(opts) {
1475
+ this.bus = opts.bus;
1476
+ this.attachBrowserListeners();
1477
+ this.startTick();
1478
+ }
1479
+ addSample(sample, prevMqttConnectedOverride) {
1480
+ if (this.disposed) return;
1481
+ const bucket = this.buckets[sample.source];
1482
+ bucket.push(sample);
1483
+ if (bucket.length > WINDOW_MAX_SAMPLES_PER_SOURCE) {
1484
+ bucket.shift();
1485
+ }
1486
+ if (sample.source !== "connection" && sample.source !== "browser") {
1487
+ if (sample.reason === "ok") {
1488
+ this.lastSuccessfulHttpAt = sample.ts;
1489
+ this.smoother.add(sample.rtt, this.deriveMetrics());
1490
+ } else if (sample.reason === "network" || sample.reason === "timeout") {
1491
+ this.smoother.add(sample.rtt, this.deriveMetrics());
1492
+ }
1493
+ }
1494
+ this.recompute(prevMqttConnectedOverride);
1495
+ }
1496
+ seedFromConnection(seed) {
1497
+ const rtt = this.resolveSeedRtt(seed);
1498
+ if (rtt === null) return;
1499
+ this.addSample({
1500
+ ts: this.now(),
1501
+ rtt,
1502
+ source: "connection",
1503
+ reason: "ok"
1504
+ });
1505
+ }
1506
+ notifyMqttConnect(rttMs) {
1507
+ const prevMqttConnected = this.mqttConnected;
1508
+ this.mqttConnected = true;
1509
+ this.mqttDisconnectedAt = null;
1510
+ this.addSample(
1511
+ {
1512
+ ts: this.now(),
1513
+ rtt: rttMs,
1514
+ source: "mqtt",
1515
+ reason: "ok"
1516
+ },
1517
+ prevMqttConnected
1518
+ );
1519
+ }
1520
+ notifyMqttDisconnect() {
1521
+ const prevMqttConnected = this.mqttConnected;
1522
+ this.mqttConnected = false;
1523
+ this.mqttDisconnectedAt = this.now();
1524
+ this.recompute(prevMqttConnected);
1525
+ }
1526
+ notifyMqttPing(rttMs) {
1527
+ this.addSample({
1528
+ ts: this.now(),
1529
+ rtt: rttMs,
1530
+ source: "mqtt",
1531
+ reason: "ok"
1532
+ });
1533
+ }
1534
+ subscribe(listener) {
1535
+ const wrapped = ((evt) => {
1536
+ const detail = evt.detail;
1537
+ if (detail) listener(detail);
1538
+ });
1539
+ this.bus.on("network:change", wrapped);
1540
+ return () => this.bus.off("network:change", wrapped);
1541
+ }
1542
+ getCurrent() {
1543
+ this.pruneWindow();
1544
+ return this.snapshot();
1545
+ }
1546
+ reset() {
1547
+ const prev = this.snapshot();
1548
+ for (const key of Object.keys(this.buckets)) {
1549
+ this.buckets[key] = [];
1550
+ }
1551
+ this.smoother.reset();
1552
+ this.mqttConnected = false;
1553
+ this.mqttDisconnectedAt = null;
1554
+ this.lastSuccessfulHttpAt = null;
1555
+ this.committedNetwork = "good";
1556
+ this.committedBackend = "ok";
1557
+ const current = this.snapshot();
1558
+ if (prev.network !== current.network || prev.backend !== current.backend) {
1559
+ this.bus.emit("network:change", { previous: prev, current });
1560
+ }
1561
+ }
1562
+ tick() {
1563
+ this.pruneWindow();
1564
+ this.recompute();
1565
+ }
1566
+ dispose() {
1567
+ if (this.disposed) return;
1568
+ this.disposed = true;
1569
+ if (this.tickHandle !== null) {
1570
+ clearInterval(this.tickHandle);
1571
+ this.tickHandle = null;
1572
+ }
1573
+ this.detachBrowserListeners();
1574
+ }
1575
+ resolveSeedRtt(seed) {
1576
+ if (typeof seed.rtt === "number" && seed.rtt > 0) return seed.rtt;
1577
+ if (seed.effectiveType && seed.effectiveType in EFFECTIVE_TYPE_TO_RTT) {
1578
+ return EFFECTIVE_TYPE_TO_RTT[seed.effectiveType] ?? null;
1579
+ }
1580
+ return null;
1581
+ }
1582
+ startTick() {
1583
+ if (typeof setInterval === "undefined") return;
1584
+ this.tickHandle = setInterval(() => this.tick(), TICK_INTERVAL_MS);
1585
+ }
1586
+ attachBrowserListeners() {
1587
+ if (typeof globalThis === "undefined") return;
1588
+ const w = globalThis;
1589
+ if (typeof w.addEventListener !== "function") return;
1590
+ this.onlineListener = () => this.tick();
1591
+ this.offlineListener = () => this.tick();
1592
+ try {
1593
+ w.addEventListener("online", this.onlineListener);
1594
+ w.addEventListener("offline", this.offlineListener);
1595
+ } catch {
1596
+ this.onlineListener = null;
1597
+ this.offlineListener = null;
1598
+ }
1599
+ }
1600
+ detachBrowserListeners() {
1601
+ const w = globalThis;
1602
+ if (typeof w.removeEventListener !== "function") return;
1603
+ try {
1604
+ if (this.onlineListener) w.removeEventListener("online", this.onlineListener);
1605
+ if (this.offlineListener) w.removeEventListener("offline", this.offlineListener);
1606
+ } catch {
1607
+ }
1608
+ }
1609
+ pruneWindow() {
1610
+ const cutoff = this.now() - WINDOW_MS;
1611
+ for (const key of Object.keys(this.buckets)) {
1612
+ this.buckets[key] = this.buckets[key].filter((s) => s.ts >= cutoff);
1613
+ }
1614
+ }
1615
+ recompute(prevMqttConnectedOverride) {
1616
+ this.pruneWindow();
1617
+ const prev = this.snapshot();
1618
+ const allSamples = this.allHttpAndMqttSamples();
1619
+ const realSamples = allSamples.filter((s) => s.source !== "connection");
1620
+ const useConnection = realSamples.length < COLD_START_REAL_SAMPLE_THRESHOLD;
1621
+ const effectiveSamples = useConnection ? allSamples : realSamples;
1622
+ const httpSamples = effectiveSamples.filter((s) => isHttpSource(s.source));
1623
+ const backend = classifyBackendHealth(httpSamples);
1624
+ const metrics = this.deriveMetricsFrom(effectiveSamples);
1625
+ const candidate = classifyNetworkLevel(
1626
+ { rtt: metrics.emaForLevel, jitter: metrics.jitter, lossRate: metrics.lossRate },
1627
+ this.committedNetwork
1628
+ );
1629
+ let nextNetwork = this.smoother.level;
1630
+ if (effectiveSamples.length === 0) {
1631
+ nextNetwork = this.committedNetwork;
1632
+ } else if (this.smoother.ema > 0) {
1633
+ nextNetwork = this.smoother.level;
1634
+ } else {
1635
+ nextNetwork = candidate;
1636
+ }
1637
+ if (this.isHardOffline()) {
1638
+ this.smoother.forceOffline();
1639
+ nextNetwork = "offline";
1640
+ } else if (this.smoother.level === "offline") {
1641
+ this.smoother.releaseOffline();
1642
+ nextNetwork = this.smoother.level;
1643
+ }
1644
+ const prevMqttConnected = prevMqttConnectedOverride ?? prev.mqttConnected;
1645
+ const changed = nextNetwork !== this.committedNetwork || backend !== this.committedBackend || this.mqttConnected !== prevMqttConnected;
1646
+ this.committedNetwork = nextNetwork;
1647
+ this.committedBackend = backend;
1648
+ if (changed) {
1649
+ const current = this.snapshot();
1650
+ this.bus.emit("network:change", { previous: prev, current });
1651
+ }
1652
+ }
1653
+ deriveMetrics() {
1654
+ return this.deriveMetricsFrom(this.allHttpAndMqttSamples());
1655
+ }
1656
+ deriveMetricsFrom(samples) {
1657
+ const httpSamples = samples.filter((s) => isHttpSource(s.source));
1658
+ const total = httpSamples.length;
1659
+ const lossCount = httpSamples.filter(
1660
+ (s) => s.reason === "network" || s.reason === "timeout" || s.reason === "offline"
1661
+ ).length;
1662
+ const lossRate = total > 0 ? lossCount / total : 0;
1663
+ const okRtts = httpSamples.filter(
1664
+ (s) => s.reason === "ok" || s.reason === "backend5xx" || s.reason === "backend4xx" || s.reason === "gqlError"
1665
+ ).map((s) => s.rtt).filter((r) => Number.isFinite(r));
1666
+ let jitter = 0;
1667
+ if (okRtts.length > 1) {
1668
+ const mean = okRtts.reduce((a, b) => a + b, 0) / okRtts.length;
1669
+ const variance = okRtts.reduce((a, b) => a + (b - mean) ** 2, 0) / okRtts.length;
1670
+ jitter = Math.sqrt(variance);
1671
+ }
1672
+ const connectionFallback = samples.filter((s) => s.source === "connection" && Number.isFinite(s.rtt)).slice(-1)[0]?.rtt;
1673
+ const emaForLevel = this.smoother.ema > 0 ? this.smoother.ema : okRtts.length > 0 ? okRtts[okRtts.length - 1] ?? 0 : connectionFallback ?? 0;
1674
+ return { jitter, lossRate, emaForLevel };
1675
+ }
1676
+ isHardOffline() {
1677
+ if (typeof navigator !== "undefined" && navigator && navigator.onLine === false) {
1678
+ return true;
1679
+ }
1680
+ const now = this.now();
1681
+ const recent = this.allHttpAndMqttSamples().filter(
1682
+ (s) => s.ts >= now - OFFLINE_HOLD_MS && (s.source === "graphql" || s.source === "rest")
1683
+ );
1684
+ if (recent.length >= 3 && recent.every(
1685
+ (s) => s.reason === "network" || s.reason === "timeout" || s.reason === "offline"
1686
+ )) {
1687
+ return true;
1688
+ }
1689
+ if (this.mqttDisconnectedAt !== null && now - this.mqttDisconnectedAt > OFFLINE_HOLD_MS && (this.lastSuccessfulHttpAt === null || now - this.lastSuccessfulHttpAt > OFFLINE_HOLD_MS)) {
1690
+ return true;
1691
+ }
1692
+ return false;
1693
+ }
1694
+ allHttpAndMqttSamples() {
1695
+ return [
1696
+ ...this.buckets.graphql,
1697
+ ...this.buckets.rest,
1698
+ ...this.buckets.mqtt,
1699
+ ...this.buckets.connection
1700
+ ];
1701
+ }
1702
+ snapshot() {
1703
+ const samples = this.allHttpAndMqttSamples();
1704
+ const metrics = this.deriveMetricsFrom(samples);
1705
+ return {
1706
+ network: this.committedNetwork,
1707
+ backend: this.committedBackend,
1708
+ rtt: Math.round(metrics.emaForLevel),
1709
+ jitter: Math.round(metrics.jitter),
1710
+ lossRate: metrics.lossRate,
1711
+ mqttConnected: this.mqttConnected,
1712
+ samplesInWindow: samples.filter((s) => s.source !== "connection").length,
1713
+ lastUpdated: this.now()
1714
+ };
1715
+ }
1716
+ now() {
1717
+ return Date.now();
1718
+ }
1719
+ };
1720
+
1721
+ // src/network/probes/connectionProbe.ts
1722
+ function attachConnectionProbe(monitor) {
1723
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
1724
+ const connection = nav?.connection;
1725
+ if (!connection) {
1726
+ return { dispose: () => void 0 };
1727
+ }
1728
+ const emit = () => {
1729
+ const seed = {
1730
+ rtt: connection.rtt,
1731
+ effectiveType: connection.effectiveType
1732
+ };
1733
+ monitor.seedFromConnection(seed);
1734
+ };
1735
+ emit();
1736
+ const listener = () => emit();
1737
+ connection.addEventListener?.("change", listener);
1738
+ return {
1739
+ dispose() {
1740
+ connection.removeEventListener?.("change", listener);
1741
+ }
1742
+ };
1743
+ }
1744
+
1745
+ // src/network/probes/graphqlProbe.ts
1746
+ function createGraphqlProbe(monitor) {
1747
+ return (event) => {
1748
+ if (isAbortError(event.threw)) return;
1749
+ const sample = classifyHttpOutcome(
1750
+ {
1751
+ rtt: event.endedAt - event.startedAt,
1752
+ httpStatus: event.httpStatus,
1753
+ gqlErrors: event.gqlErrors ?? null,
1754
+ threw: event.threw
1755
+ },
1756
+ { now: event.endedAt, source: "graphql" }
1757
+ );
1758
+ monitor.addSample(sample);
1759
+ };
1760
+ }
1761
+
1762
+ // src/network/probes/mqttProbe.ts
1763
+ function createMqttProbe(monitor) {
1764
+ return (event) => {
1765
+ switch (event.kind) {
1766
+ case "connect":
1767
+ case "reconnect":
1768
+ monitor.notifyMqttConnect(event.rttMs);
1769
+ return;
1770
+ case "disconnect":
1771
+ monitor.notifyMqttDisconnect();
1772
+ return;
1773
+ case "ping":
1774
+ monitor.notifyMqttPing(event.rttMs);
1775
+ return;
1776
+ case "error":
1777
+ return;
1778
+ }
1779
+ };
1780
+ }
1781
+
1782
+ // src/network/probes/restProbe.ts
1783
+ function createRestProbe(monitor) {
1784
+ return (event) => {
1785
+ if (isAbortError(event.threw)) return;
1786
+ const sample = classifyHttpOutcome(
1787
+ {
1788
+ rtt: event.endedAt - event.startedAt,
1789
+ httpStatus: event.httpStatus,
1790
+ threw: event.threw
1791
+ },
1792
+ { now: event.endedAt, source: "rest" }
1793
+ );
1794
+ monitor.addSample(sample);
1795
+ };
1796
+ }
1797
+
1798
+ // src/storage/index.ts
1799
+ var MemoryAdapter = class {
1800
+ store = /* @__PURE__ */ new Map();
1801
+ get(key) {
1802
+ return this.store.get(key) ?? null;
1803
+ }
1804
+ set(key, value) {
1805
+ this.store.set(key, value);
1806
+ }
1807
+ remove(key) {
1808
+ this.store.delete(key);
1809
+ }
1810
+ };
1811
+ var BrowserStorageAdapter = class {
1812
+ storage;
1813
+ constructor(storage) {
1814
+ this.storage = storage;
1815
+ }
1816
+ get(key) {
1817
+ return this.storage.getItem(key);
1818
+ }
1819
+ set(key, value) {
1820
+ this.storage.setItem(key, value);
1821
+ }
1822
+ remove(key) {
1823
+ this.storage.removeItem(key);
1824
+ }
1825
+ };
1826
+ function autoDetectStorage() {
1827
+ if (typeof localStorage !== "undefined") {
1828
+ return TaphubStorage.localStorage();
1829
+ }
1830
+ return TaphubStorage.memory();
1831
+ }
1832
+ var TaphubStorage = {
1833
+ memory() {
1834
+ return new MemoryAdapter();
1835
+ },
1836
+ localStorage() {
1837
+ if (typeof localStorage === "undefined") {
1838
+ throw new TaphubError("localStorage is not available in this environment", {
1839
+ code: "StorageUnavailable"
1840
+ });
1841
+ }
1842
+ return new BrowserStorageAdapter(localStorage);
1843
+ },
1844
+ sessionStorage() {
1845
+ if (typeof sessionStorage === "undefined") {
1846
+ throw new TaphubError("sessionStorage is not available in this environment", {
1847
+ code: "StorageUnavailable"
1848
+ });
1849
+ }
1850
+ return new BrowserStorageAdapter(sessionStorage);
1851
+ }
1852
+ };
1853
+
1854
+ // src/transport/shared/errors.ts
1855
+ function mapNetworkErrorToTaphubError(err) {
1856
+ if (err instanceof DOMException && err.name === "AbortError") {
1857
+ throw new TaphubNetworkError("Request aborted", { code: "Aborted" });
1858
+ }
1859
+ if (err instanceof Error && (err.name === "AbortError" || err.message.includes("aborted"))) {
1860
+ throw new TaphubNetworkError("Request aborted", { code: "Aborted" });
1861
+ }
1862
+ throw new TaphubNetworkError(err instanceof Error ? err.message : "Network error", {
1863
+ code: "NetworkUnreachable"
1864
+ });
1865
+ }
1866
+
1867
+ // src/transport/shared/headers.ts
1868
+ function buildHeaders(opts) {
1869
+ const headers = new Headers();
1870
+ if (opts.token !== null) {
1871
+ headers.set("Authorization", `Bearer ${opts.token}`);
1872
+ }
1873
+ if (opts.hasBody) {
1874
+ headers.set("Content-Type", "application/json");
1875
+ }
1876
+ if (opts.agencyId) {
1877
+ headers.set("x-builder-code", opts.agencyId);
1878
+ }
1879
+ return headers;
1880
+ }
1881
+
1882
+ // src/transport/graphql/errors.ts
1883
+ var AUTH_ERROR_CODES = [
1884
+ "Unauthorized",
1885
+ "TokenExpired",
1886
+ "NotAuthenticated",
1887
+ "ErrAccessTokenInvalid",
1888
+ "ErrSessionInvalid"
1889
+ ];
1890
+ var SLIPPAGE_CODE = "Bid_CoefficientMismatch";
1891
+ function extractSlippageMeta(entry) {
1892
+ const meta = entry?.extensions?.meta;
1893
+ if (!meta) return null;
1894
+ const clientCoef = meta.clientCoef;
1895
+ const serverCoef = meta.serverCoef;
1896
+ const slippage = meta.slippage;
1897
+ if (typeof clientCoef !== "number" || typeof serverCoef !== "number" || typeof slippage !== "number" || !Number.isFinite(clientCoef) || !Number.isFinite(serverCoef) || !Number.isFinite(slippage)) {
1898
+ return null;
1899
+ }
1900
+ return { clientCoef, serverCoef, slippage };
1901
+ }
1902
+ function isAuthCode(code) {
1903
+ return AUTH_ERROR_CODES.includes(code);
1904
+ }
1905
+ function firstErrorCode(body) {
1906
+ const code = body.errors?.[0]?.extensions?.code;
1907
+ if (typeof code === "string") return code;
1908
+ return "Unknown";
1909
+ }
1910
+ function firstErrorMessage(body) {
1911
+ return body.errors?.[0]?.message ?? "GraphQL error";
1912
+ }
1913
+ function buildDetails(body) {
1914
+ const details = {};
1915
+ if (body.errors && body.errors.length > 0) {
1916
+ details.errors = body.errors;
1917
+ }
1918
+ if (body.data !== void 0 && body.data !== null) {
1919
+ details.partialData = body.data;
1920
+ }
1921
+ return details;
1922
+ }
1923
+ function mapGraphQLHttpError(status, body) {
1924
+ const parsed = typeof body === "object" && body !== null ? body : null;
1925
+ if (status === 401) {
1926
+ const code = parsed?.errors?.[0]?.extensions?.code ?? "Unauthorized";
1927
+ const message = parsed?.errors?.[0]?.message ?? "Unauthorized";
1928
+ throw new TaphubAuthError(message, {
1929
+ code,
1930
+ details: parsed ? buildDetails(parsed) : body
1931
+ });
1932
+ }
1933
+ if (status >= 400 && status < 500) {
1934
+ const code = parsed?.errors?.[0]?.extensions?.code ?? "BadRequest";
1935
+ const message = parsed?.errors?.[0]?.message ?? "Bad Request";
1936
+ throw new TaphubValidationError(message, {
1937
+ code,
1938
+ details: parsed ? buildDetails(parsed) : body
1939
+ });
1940
+ }
1941
+ if (status >= 500) {
1942
+ if (!parsed) {
1943
+ throw new TaphubServerError("Invalid response from server", {
1944
+ code: "InvalidResponse",
1945
+ details: body ?? void 0
1946
+ });
1947
+ }
1948
+ const code = parsed.errors?.[0]?.extensions?.code ?? "ServerError";
1949
+ const message = parsed.errors?.[0]?.message ?? "Internal Server Error";
1950
+ throw new TaphubServerError(message, {
1951
+ code,
1952
+ details: buildDetails(parsed)
1953
+ });
1954
+ }
1955
+ throw new TaphubServerError("Unexpected status", {
1956
+ code: "ServerError",
1957
+ details: body ?? void 0
1958
+ });
1959
+ }
1960
+ function mapGraphQLLevelError(body) {
1961
+ const code = firstErrorCode(body);
1962
+ const message = firstErrorMessage(body);
1963
+ if (isAuthCode(code)) {
1964
+ throw new TaphubAuthError(message, {
1965
+ code,
1966
+ details: buildDetails(body)
1967
+ });
1968
+ }
1969
+ if (code === SLIPPAGE_CODE) {
1970
+ const meta = extractSlippageMeta(body.errors?.[0]);
1971
+ if (meta) {
1972
+ throw new TaphubSlippageError(message, {
1973
+ code,
1974
+ clientCoef: meta.clientCoef,
1975
+ serverCoef: meta.serverCoef,
1976
+ slippage: meta.slippage,
1977
+ details: buildDetails(body)
1978
+ });
1979
+ }
1980
+ }
1981
+ throw new TaphubValidationError(message, {
1982
+ code,
1983
+ details: buildDetails(body)
1984
+ });
1985
+ }
1986
+
1987
+ // src/transport/graphql/index.ts
1988
+ function extractOp(query) {
1989
+ const named = query.match(/(?:query|mutation|subscription)\s+([A-Za-z]\w*)/);
1990
+ if (named) return named[1];
1991
+ return query.match(/{\s*([A-Za-z]\w*)/)?.[1] ?? "unknown";
1992
+ }
1993
+ function createGraphQLTransport(deps) {
1994
+ const { baseUrl, getToken, agencyId, onRequest } = deps;
1995
+ function resolveFetch() {
1996
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
1997
+ if (!fetchImpl) {
1998
+ throw new TaphubError(
1999
+ "fetch is not available. Provide a custom fetch or ensure globalThis.fetch exists.",
2000
+ {
2001
+ code: "FetchUnavailable"
2002
+ }
2003
+ );
2004
+ }
2005
+ return fetchImpl;
2006
+ }
2007
+ return {
2008
+ async request(query, variables, opts) {
2009
+ const fetchImpl = resolveFetch();
2010
+ const base = baseUrl.replace(/\/+$/, "");
2011
+ const op = extractOp(query);
2012
+ const url = `${base}?${op}`;
2013
+ const token = getToken();
2014
+ const headers = buildHeaders({ token, hasBody: true, agencyId });
2015
+ const body = { query };
2016
+ if (variables !== void 0) {
2017
+ body.variables = variables;
2018
+ }
2019
+ const startedAt = Date.now();
2020
+ let httpStatus = null;
2021
+ let gqlErrors = null;
2022
+ let threw = null;
2023
+ const fireProbe = () => {
2024
+ if (!onRequest) return;
2025
+ try {
2026
+ onRequest({
2027
+ startedAt,
2028
+ endedAt: Date.now(),
2029
+ httpStatus,
2030
+ gqlErrors,
2031
+ threw
2032
+ });
2033
+ } catch {
2034
+ }
2035
+ };
2036
+ try {
2037
+ let response;
2038
+ try {
2039
+ response = await fetchImpl.call(void 0, url, {
2040
+ method: "POST",
2041
+ headers,
2042
+ body: JSON.stringify(body),
2043
+ signal: opts?.signal
2044
+ });
2045
+ } catch (err) {
2046
+ threw = err;
2047
+ mapNetworkErrorToTaphubError(err);
2048
+ }
2049
+ httpStatus = response.status;
2050
+ let responseBody;
2051
+ try {
2052
+ responseBody = await response.json();
2053
+ } catch {
2054
+ throw new TaphubServerError("Invalid response from server", {
2055
+ code: "InvalidResponse"
2056
+ });
2057
+ }
2058
+ if (!response.ok) {
2059
+ mapGraphQLHttpError(response.status, responseBody);
2060
+ }
2061
+ const graphqlBody = responseBody;
2062
+ if (graphqlBody.errors && graphqlBody.errors.length > 0) {
2063
+ gqlErrors = graphqlBody.errors;
2064
+ mapGraphQLLevelError(graphqlBody);
2065
+ }
2066
+ if (graphqlBody.data === void 0 || graphqlBody.data === null) {
2067
+ throw new TaphubServerError("Invalid response from server", {
2068
+ code: "InvalidResponse"
2069
+ });
2070
+ }
2071
+ return graphqlBody.data;
2072
+ } catch (err) {
2073
+ if (threw === null) threw = err;
2074
+ throw err;
2075
+ } finally {
2076
+ fireProbe();
2077
+ }
2078
+ }
2079
+ };
2080
+ }
2081
+
2082
+ // src/transport/rest/errors.ts
2083
+ function tryParseBody(body) {
2084
+ if (typeof body === "object" && body !== null && "code" in body) {
2085
+ return body;
2086
+ }
2087
+ return null;
2088
+ }
2089
+ function mapHttpToTaphubError(status, body) {
2090
+ const parsed = tryParseBody(body);
2091
+ const code = parsed?.code ?? "";
2092
+ const details = body ?? void 0;
2093
+ if (status === 401) {
2094
+ throw new TaphubAuthError(parsed?.message ?? "Unauthorized", {
2095
+ code: code || "Unauthorized",
2096
+ details
2097
+ });
2098
+ }
2099
+ if (status >= 400 && status < 500) {
2100
+ throw new TaphubValidationError(parsed?.message ?? "Bad Request", {
2101
+ code: code || "BadRequest",
2102
+ details
2103
+ });
2104
+ }
2105
+ if (status >= 500) {
2106
+ if (!parsed) {
2107
+ throw new TaphubServerError("Invalid response from server", {
2108
+ code: "InvalidResponse",
2109
+ details
2110
+ });
2111
+ }
2112
+ throw new TaphubServerError(parsed.message ?? "Internal Server Error", {
2113
+ code: code || "ServerError",
2114
+ details
2115
+ });
2116
+ }
2117
+ throw new TaphubServerError("Unexpected status", {
2118
+ code: "ServerError",
2119
+ details
2120
+ });
2121
+ }
2122
+
2123
+ // src/transport/rest/url.ts
2124
+ var API_PREFIX = "/api/user/v1";
2125
+ function buildUrl(baseUrl, path) {
2126
+ const base = baseUrl.replace(/\/+$/, "");
2127
+ const cleanPath = path.replace(/^\/+/, "");
2128
+ return `${base}${API_PREFIX}/${cleanPath}`;
2129
+ }
2130
+
2131
+ // src/transport/rest/index.ts
2132
+ function createRestTransport(deps) {
2133
+ const { baseUrl, getToken, onRequest } = deps;
2134
+ function resolveFetch() {
2135
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
2136
+ if (!fetchImpl) {
2137
+ throw new TaphubError(
2138
+ "fetch is not available. Provide a custom fetch or ensure globalThis.fetch exists.",
2139
+ {
2140
+ code: "FetchUnavailable"
2141
+ }
2142
+ );
2143
+ }
2144
+ return fetchImpl;
2145
+ }
2146
+ async function request(method, path, body, opts) {
2147
+ const fetchImpl = resolveFetch();
2148
+ const url = buildUrl(baseUrl, path);
2149
+ const token = getToken();
2150
+ const hasBody = body !== void 0;
2151
+ const headers = buildHeaders({ token, hasBody });
2152
+ const startedAt = Date.now();
2153
+ let httpStatus = null;
2154
+ let threw = null;
2155
+ const fireProbe = () => {
2156
+ if (!onRequest) return;
2157
+ try {
2158
+ onRequest({ startedAt, endedAt: Date.now(), httpStatus, threw });
2159
+ } catch {
2160
+ }
2161
+ };
2162
+ try {
2163
+ let response;
2164
+ try {
2165
+ response = await fetchImpl.call(void 0, url, {
2166
+ method,
2167
+ headers,
2168
+ body: hasBody ? JSON.stringify(body) : void 0,
2169
+ signal: opts?.signal
2170
+ });
2171
+ } catch (err) {
2172
+ threw = err;
2173
+ mapNetworkErrorToTaphubError(err);
2174
+ }
2175
+ httpStatus = response.status;
2176
+ let responseBody;
2177
+ try {
2178
+ responseBody = await response.json();
2179
+ } catch {
2180
+ throw new TaphubServerError("Invalid response from server", {
2181
+ code: "InvalidResponse"
2182
+ });
2183
+ }
2184
+ if (!response.ok) {
2185
+ mapHttpToTaphubError(response.status, responseBody);
2186
+ }
2187
+ const envelope = responseBody;
2188
+ if (envelope.code !== "Success" || !("data" in envelope)) {
2189
+ throw new TaphubServerError("Invalid response from server", {
2190
+ code: "InvalidResponse"
2191
+ });
2192
+ }
2193
+ return envelope.data;
2194
+ } catch (err) {
2195
+ if (threw === null) threw = err;
2196
+ throw err;
2197
+ } finally {
2198
+ fireProbe();
2199
+ }
2200
+ }
2201
+ return {
2202
+ get(path, opts) {
2203
+ return request("GET", path, void 0, opts);
2204
+ },
2205
+ post(path, body, opts) {
2206
+ return request("POST", path, body, opts);
2207
+ }
2208
+ };
2209
+ }
2210
+
2211
+ // src/client.ts
2212
+ function tokenStorageKey(agencyId) {
2213
+ return `taphub:${agencyId}:token`;
2214
+ }
2215
+ function isDemoStorageKey(agencyId) {
2216
+ return `taphub:${agencyId}:isDemo`;
2217
+ }
2218
+ var TaphubClient = class {
2219
+ agencyId;
2220
+ endpoint;
2221
+ storage;
2222
+ /** @readonly User module — reassignment has no effect at runtime. */
2223
+ user;
2224
+ /** @readonly Auth module — reassignment has no effect at runtime. */
2225
+ auth;
2226
+ /** @readonly Game module — reassignment has no effect at runtime. */
2227
+ game;
2228
+ /** @readonly Bid module — reassignment has no effect at runtime. */
2229
+ bid;
2230
+ /** @readonly Leaderboard module — reassignment has no effect at runtime. */
2231
+ leaderboard;
2232
+ /** @readonly Locale module — reassignment has no effect at runtime. */
2233
+ locale;
2234
+ /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
2235
+ realtime;
2236
+ /** @readonly Network quality monitor — always present (operates passively). */
2237
+ network;
2238
+ bus;
2239
+ #token;
2240
+ #isDemo;
2241
+ #tokenKey;
2242
+ #isDemoKey;
2243
+ #rest;
2244
+ #graphql;
2245
+ #graphqlUser;
2246
+ constructor(config) {
2247
+ if (!config.agencyId) {
2248
+ throw new TaphubValidationError("agencyId is required", {
2249
+ code: "InvalidConfig",
2250
+ details: { field: "agencyId" }
2251
+ });
2252
+ }
2253
+ try {
2254
+ new URL(config.endpoint);
2255
+ } catch {
2256
+ throw new TaphubValidationError("endpoint must be a valid absolute URL", {
2257
+ code: "InvalidConfig",
2258
+ details: { field: "endpoint" }
2259
+ });
2260
+ }
2261
+ this.agencyId = config.agencyId;
2262
+ this.endpoint = config.endpoint;
2263
+ this.storage = config.storage ?? autoDetectStorage();
2264
+ this.bus = new TaphubEventBus();
2265
+ this.network = new NetworkQualityMonitor({ bus: this.bus });
2266
+ const graphqlProbe = createGraphqlProbe(this.network);
2267
+ const restProbe = createRestProbe(this.network);
2268
+ this.#tokenKey = tokenStorageKey(this.agencyId);
2269
+ this.#isDemoKey = isDemoStorageKey(this.agencyId);
2270
+ this.#token = this.storage.get(this.#tokenKey);
2271
+ this.#isDemo = this.storage.get(this.#isDemoKey) === "1";
2272
+ this.#rest = createRestTransport({
2273
+ baseUrl: this.endpoint,
2274
+ getToken: () => this.getToken(),
2275
+ fetch: config.fetch,
2276
+ onRequest: restProbe
2277
+ });
2278
+ const baseApi = this.endpoint.replace(/\/+$/, "");
2279
+ this.#graphql = createGraphQLTransport({
2280
+ baseUrl: `${baseApi}/grid-api/grid-gql`,
2281
+ getToken: () => this.getToken(),
2282
+ agencyId: this.agencyId,
2283
+ fetch: config.fetch,
2284
+ onRequest: graphqlProbe
2285
+ });
2286
+ this.#graphqlUser = createGraphQLTransport({
2287
+ baseUrl: `${baseApi}/taphub-user-service/th-user-gql`,
2288
+ getToken: () => this.getToken(),
2289
+ agencyId: this.agencyId,
2290
+ fetch: config.fetch,
2291
+ onRequest: graphqlProbe
2292
+ });
2293
+ this.user = new UserModule({
2294
+ rest: this.#rest,
2295
+ graphql: this.#graphql,
2296
+ graphqlUser: this.#graphqlUser
2297
+ });
2298
+ this.auth = new AuthModule({
2299
+ rest: this.#rest,
2300
+ graphql: this.#graphql,
2301
+ graphqlUser: this.#graphqlUser,
2302
+ setToken: (t, opts) => this.setToken(t, opts),
2303
+ agencyId: this.agencyId,
2304
+ onLoginSuccess: () => this.user.refreshCurrencies().then(() => void 0).catch(() => void 0),
2305
+ onLogout: () => {
2306
+ this.user.clearCurrencies();
2307
+ }
2308
+ });
2309
+ this.game = new GameModule({ graphql: this.#graphql });
2310
+ this.bid = new BidModule({ graphql: this.#graphql });
2311
+ this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
2312
+ this.locale = new LocaleModule({ graphql: this.#graphql });
2313
+ this.realtime = config.mqttEndpoint ? new RealtimeModule({
2314
+ mqttEndpoint: config.mqttEndpoint,
2315
+ onMqttLifecycle: createMqttProbe(this.network)
2316
+ }) : void 0;
2317
+ attachConnectionProbe(this.network);
2318
+ Object.defineProperty(this, "user", {
2319
+ value: this.user,
2320
+ writable: false,
2321
+ enumerable: true,
2322
+ configurable: false
2323
+ });
2324
+ Object.defineProperty(this, "auth", {
2325
+ value: this.auth,
2326
+ writable: false,
2327
+ enumerable: true,
2328
+ configurable: false
2329
+ });
2330
+ Object.defineProperty(this, "game", {
2331
+ value: this.game,
2332
+ writable: false,
2333
+ enumerable: true,
2334
+ configurable: false
2335
+ });
2336
+ Object.defineProperty(this, "bid", {
2337
+ value: this.bid,
2338
+ writable: false,
2339
+ enumerable: true,
2340
+ configurable: false
2341
+ });
2342
+ Object.defineProperty(this, "leaderboard", {
2343
+ value: this.leaderboard,
2344
+ writable: false,
2345
+ enumerable: true,
2346
+ configurable: false
2347
+ });
2348
+ if (this.realtime) {
2349
+ Object.defineProperty(this, "realtime", {
2350
+ value: this.realtime,
2351
+ writable: false,
2352
+ enumerable: true,
2353
+ configurable: false
2354
+ });
2355
+ }
2356
+ Object.defineProperty(this, "network", {
2357
+ value: this.network,
2358
+ writable: false,
2359
+ enumerable: true,
2360
+ configurable: false
2361
+ });
2362
+ }
2363
+ getToken() {
2364
+ return this.#token;
2365
+ }
2366
+ setToken(token, opts) {
2367
+ this.#token = token;
2368
+ if (token === null) {
2369
+ this.#isDemo = false;
2370
+ this.storage.remove(this.#tokenKey);
2371
+ this.storage.remove(this.#isDemoKey);
2372
+ return;
2373
+ }
2374
+ const isDemo = opts?.isDemo ?? false;
2375
+ this.#isDemo = isDemo;
2376
+ this.storage.set(this.#tokenKey, token);
2377
+ this.storage.set(this.#isDemoKey, isDemo ? "1" : "0");
2378
+ }
2379
+ isDemo() {
2380
+ return this.#isDemo;
2381
+ }
2382
+ /** @internal Used by module integrations to access the REST transport. Not part of the public API. */
2383
+ get _rest() {
2384
+ return this.#rest;
2385
+ }
2386
+ /** @internal Used by module integrations to access the GraphQL transport. Not part of the public API. */
2387
+ get _graphql() {
2388
+ return this.#graphql;
2389
+ }
2390
+ on(event, handler) {
2391
+ this.bus.on(event, handler);
2392
+ }
2393
+ off(event, handler) {
2394
+ this.bus.off(event, handler);
2395
+ }
2396
+ };
2397
+
2398
+ // src/utils/coefficient.ts
2399
+ function errorFunction(x) {
2400
+ const a1 = 0.254829592;
2401
+ const a2 = -0.284496736;
2402
+ const a3 = 1.421413741;
2403
+ const a4 = -1.453152027;
2404
+ const a5 = 1.061405429;
2405
+ const p = 0.3275911;
2406
+ const sign = x >= 0 ? 1 : -1;
2407
+ const absX = Math.abs(x);
2408
+ const t = 1 / (1 + p * absX);
2409
+ const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-absX * absX);
2410
+ return sign * y;
2411
+ }
2412
+ function normalCDF(x) {
2413
+ return 0.5 * (1 + errorFunction(x / Math.SQRT2));
2414
+ }
2415
+ function normalPDF(x) {
2416
+ return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
2417
+ }
2418
+ function adaptiveSimpson(f, a, b, tolerance = 1e-6, maxDepth = 50) {
2419
+ function simpson(a2, b2, fa2, fm2, fb2) {
2420
+ return (b2 - a2) / 6 * (fa2 + 4 * fm2 + fb2);
2421
+ }
2422
+ function recurse(a2, b2, fa2, fm2, fb2, whole2, depth) {
2423
+ const m = (a2 + b2) / 2;
2424
+ const m1 = (a2 + m) / 2;
2425
+ const m2 = (m + b2) / 2;
2426
+ const fm1 = f(m1);
2427
+ const fm22 = f(m2);
2428
+ const left = simpson(a2, m, fa2, fm1, fm2);
2429
+ const right = simpson(m, b2, fm2, fm22, fb2);
2430
+ const combined = left + right;
2431
+ if (depth >= maxDepth || Math.abs(combined - whole2) <= 15 * tolerance) {
2432
+ return combined + (combined - whole2) / 15;
2433
+ }
2434
+ return recurse(a2, m, fa2, fm1, fm2, left, depth + 1) + recurse(m, b2, fm2, fm22, fb2, right, depth + 1);
2435
+ }
2436
+ const fa = f(a);
2437
+ const fm = f((a + b) / 2);
2438
+ const fb = f(b);
2439
+ const whole = simpson(a, b, fa, fm, fb);
2440
+ return recurse(a, b, fa, fm, fb, whole, 0);
2441
+ }
2442
+ function calculateProbWin(time1, time2, _price1, price2, creationTime, currentPrice, volatility) {
2443
+ if (volatility <= 0 || time2 <= time1) {
2444
+ return 0;
2445
+ }
2446
+ const _elapsed = time1 - creationTime;
2447
+ const remaining = time2 - time1;
2448
+ const totalDuration = time2 - creationTime;
2449
+ if (totalDuration <= 0) {
2450
+ return 0;
2451
+ }
2452
+ const targetMove = price2 - currentPrice;
2453
+ const timeFraction = remaining / totalDuration;
2454
+ const scaledVolatility = volatility * Math.sqrt(timeFraction);
2455
+ if (scaledVolatility === 0) {
2456
+ return currentPrice >= price2 ? 1 : 0;
2457
+ }
2458
+ const z = targetMove / scaledVolatility;
2459
+ const prob = 1 - normalCDF(z);
2460
+ return Math.max(0, Math.min(1, prob));
2461
+ }
2462
+ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, currentPrice, volatility) {
2463
+ if (volatility <= 0 || time2 <= time1) {
2464
+ return 0;
2465
+ }
2466
+ const elapsed = time1 - creationTime;
2467
+ if (elapsed <= 0) {
2468
+ return 0;
2469
+ }
2470
+ const remaining = time2 - time1;
2471
+ const totalDuration = time2 - creationTime;
2472
+ const timeFraction = remaining / totalDuration;
2473
+ const scaledVolatility = volatility * Math.sqrt(timeFraction);
2474
+ if (scaledVolatility === 0) {
2475
+ return currentPrice >= price2 ? 1 : 0;
2476
+ }
2477
+ const targetMove = price2 - currentPrice;
2478
+ const z = targetMove / scaledVolatility;
2479
+ const prob = 1 - normalCDF(z);
2480
+ return Math.max(0, Math.min(1, prob));
2481
+ }
2482
+ // Annotate the CommonJS export names for ESM import in node:
2483
+ 0 && (module.exports = {
2484
+ AuthModule,
2485
+ BidModule,
2486
+ GameModule,
2487
+ LeaderboardModule,
2488
+ LocaleModule,
2489
+ NetworkQualityMonitor,
2490
+ RealtimeModule,
2491
+ TaphubAuthError,
2492
+ TaphubClient,
2493
+ TaphubError,
2494
+ TaphubEventBus,
2495
+ TaphubNetworkError,
2496
+ TaphubServerError,
2497
+ TaphubSlippageError,
2498
+ TaphubStorage,
2499
+ TaphubValidationError,
2500
+ UserModule,
2501
+ adaptiveSimpson,
2502
+ autoDetectStorage,
2503
+ calculateProbWin,
2504
+ calculateProbWin_v2,
2505
+ errorFunction,
2506
+ isCancelled,
2507
+ isLoss,
2508
+ isPending,
2509
+ isTerminal,
2510
+ isWin,
2511
+ normalCDF,
2512
+ normalPDF
2513
+ });