@moneypot/hub 1.17.1 → 1.18.0-dev.1

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.
@@ -82,7 +82,9 @@ export class DatabaseNotifier extends stream.EventEmitter {
82
82
  }
83
83
  export async function getTransferCursor(pgClient, { casinoId, }) {
84
84
  const row = await pgClient
85
- .query("select cursor from hub_hidden.transfer_cursor where casino_id = $1", [casinoId])
85
+ .query("select cursor from hub_hidden.transfer_cursor where casino_id = $1", [
86
+ casinoId,
87
+ ])
86
88
  .then(maybeOneRow);
87
89
  return row?.cursor;
88
90
  }
@@ -1,10 +1,13 @@
1
- import { DbBalance, DbBankroll, DbCurrency, DbSession } from "./types.js";
1
+ import { DbBalance, DbBankroll, DbCasino, DbCurrency, DbSession } from "./types.js";
2
2
  import { PgClientInTransaction, QueryExecutor } from "./index.js";
3
3
  export declare function dbGetActiveSessionById(pgClient: QueryExecutor, sessionId: string): Promise<DbSession | undefined>;
4
4
  export declare function dbGetCasinoCurrencyByKey(pgClient: QueryExecutor, { currencyKey, casinoId }: {
5
5
  currencyKey: string;
6
6
  casinoId: string;
7
7
  }): Promise<DbCurrency | undefined>;
8
+ export declare function dbGetHouseBankrolls(pgClient: QueryExecutor, { casinoId }: {
9
+ casinoId: DbCasino["id"];
10
+ }): Promise<Pick<DbBankroll, "id" | "currency_key" | "amount">[]>;
8
11
  export declare function dbLockPlayerBalance(pgClient: PgClientInTransaction, { userId, casinoId, experienceId, currencyKey, }: {
9
12
  userId: string;
10
13
  casinoId: string;
@@ -13,20 +13,29 @@ export async function dbGetCasinoCurrencyByKey(pgClient, { currencyKey, casinoId
13
13
  .query(`
14
14
  SELECT *
15
15
  FROM hub.currency
16
- WHERE key = $1
16
+ WHERE key = $1
17
17
  AND casino_id = $2
18
18
  `, [currencyKey, casinoId])
19
19
  .then(maybeOneRow);
20
20
  }
21
+ export async function dbGetHouseBankrolls(pgClient, { casinoId }) {
22
+ return pgClient
23
+ .query(`
24
+ SELECT id, currency_key, amount
25
+ FROM hub.bankroll
26
+ WHERE casino_id = $1
27
+ `, [casinoId])
28
+ .then((res) => res.rows);
29
+ }
21
30
  export async function dbLockPlayerBalance(pgClient, { userId, casinoId, experienceId, currencyKey, }) {
22
31
  return pgClient
23
32
  .query(`
24
- SELECT *
25
- FROM hub.balance
26
- WHERE user_id = $1
27
- AND casino_id = $2
28
- AND experience_id = $3
29
- AND currency_key = $4
33
+ SELECT *
34
+ FROM hub.balance
35
+ WHERE user_id = $1
36
+ AND casino_id = $2
37
+ AND experience_id = $3
38
+ AND currency_key = $4
30
39
  FOR UPDATE
31
40
  `, [userId, casinoId, experienceId, currencyKey])
32
41
  .then(maybeOneRow)
@@ -35,10 +44,10 @@ export async function dbLockPlayerBalance(pgClient, { userId, casinoId, experien
35
44
  export async function dbLockHouseBankroll(pgClient, { casinoId, currencyKey }) {
36
45
  return pgClient
37
46
  .query(`
38
- SELECT *
39
- FROM hub.bankroll
40
- WHERE casino_id = $1
41
- AND currency_key = $2
47
+ SELECT *
48
+ FROM hub.bankroll
49
+ WHERE casino_id = $1
50
+ AND currency_key = $2
42
51
  FOR UPDATE
43
52
  `, [casinoId, currencyKey])
44
53
  .then(maybeOneRow)
@@ -4,7 +4,7 @@ export const HubBadHashChainErrorPlugin = makeExtendSchemaPlugin(() => {
4
4
  return {
5
5
  typeDefs: gql `
6
6
  type HubBadHashChainError {
7
- message: String
7
+ message: String!
8
8
  }
9
9
  `,
10
10
  objects: {
@@ -46,7 +46,7 @@ export const HubChatCreateUserMessagePlugin = extendSchema((build) => {
46
46
  }
47
47
 
48
48
  union HubChatCreateUserMessageResult =
49
- HubChatCreateUserMessageSuccess
49
+ | HubChatCreateUserMessageSuccess
50
50
  | HubChatUserRateLimited
51
51
  | HubChatUserMuted
52
52
 
@@ -35,7 +35,7 @@ export const HubChatSubscriptionPlugin = extendSchema((build) => {
35
35
  }
36
36
 
37
37
  union HubChatSubscriptionPayload =
38
- HubChatSubscriptionNewMessage
38
+ | HubChatSubscriptionNewMessage
39
39
  | HubChatSubscriptionMuted
40
40
  | HubChatSubscriptionUnmuted
41
41
 
@@ -2,7 +2,7 @@ import { access, context, object, ObjectStep, sideEffect, } from "postgraphile/g
2
2
  import { gql, extendSchema } from "postgraphile/utils";
3
3
  import * as z from "zod/v4";
4
4
  import { GraphQLError } from "graphql";
5
- import { DbHashKind, dbLockHouseBankroll, dbLockPlayerBalance, exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../db/index.js";
5
+ import { DbHashKind, dbGetHouseBankrolls, dbLockHouseBankroll, dbLockPlayerBalance, exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../db/index.js";
6
6
  import { assert } from "tsafe";
7
7
  import { dbInsertHubHash, dbLockHubHashChain, } from "../hash-chain/db-hash-chain.js";
8
8
  import { getIntermediateHash } from "../hash-chain/get-hash.js";
@@ -92,7 +92,12 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
92
92
  bet: HubOutcomeBet!
93
93
  }
94
94
 
95
- union HubMakeOutcomeBetResult = HubMakeOutcomeBetSuccess | HubBadHashChainError
95
+ type HubRiskError {
96
+ message: String!
97
+ riskLimits: HubRiskLimit!
98
+ }
99
+
100
+ union HubMakeOutcomeBetResult = HubMakeOutcomeBetSuccess | HubBadHashChainError | HubRiskError
96
101
 
97
102
  type HubMakeOutcomeBetPayload {
98
103
  result: HubMakeOutcomeBetResult!
@@ -101,10 +106,58 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
101
106
  extend type Mutation {
102
107
  hubMakeOutcomeBet(input: HubMakeOutcomeBetInput!): HubMakeOutcomeBetPayload
103
108
  }
109
+
110
+ type HubRiskLimit {
111
+ maxPayout: Float!
112
+ maxWager: Float
113
+ }
114
+
115
+ type HubRiskLimitWithCurrency {
116
+ currency: String!
117
+ maxPayout: Float!
118
+ maxWager: Float
119
+ }
120
+
121
+ extend type Query {
122
+ hubRiskLimits(betKind: BetKind!): [HubRiskLimitWithCurrency!]!
123
+ }
104
124
  `;
105
125
  return {
106
126
  typeDefs,
107
127
  objects: {
128
+ Query: {
129
+ plans: {
130
+ hubRiskLimits: (_, { $betKind }) => {
131
+ const $identity = context().get("identity");
132
+ const $superuserPool = context().get("superuserPool");
133
+ const $result = sideEffect([$identity, $superuserPool, $betKind], async ([identity, superuserPool, betKind]) => {
134
+ if (identity?.kind !== "user") {
135
+ throw new GraphQLError("Unauthorized");
136
+ }
137
+ const betConfig = betConfigs[betKind];
138
+ if (!betConfig) {
139
+ throw new GraphQLError(`Invalid bet kind`);
140
+ }
141
+ const dbHouseBankrolls = await dbGetHouseBankrolls(superuserPool, {
142
+ casinoId: identity.session.casino_id,
143
+ });
144
+ const limitsWithCurrency = dbHouseBankrolls.map((bankroll) => {
145
+ const limits = betConfig.riskPolicy({
146
+ type: "get-limits",
147
+ currency: bankroll.currency_key,
148
+ bankroll: bankroll.amount,
149
+ });
150
+ return {
151
+ currency: bankroll.currency_key,
152
+ ...limits,
153
+ };
154
+ });
155
+ return limitsWithCurrency;
156
+ });
157
+ return $result;
158
+ },
159
+ },
160
+ },
108
161
  Mutation: {
109
162
  plans: {
110
163
  hubMakeOutcomeBet: (_, { $input }) => {
@@ -284,18 +337,28 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
284
337
  }
285
338
  const maxProfitMultiplier = Math.max(...input.outcomes.map((o) => o.profit));
286
339
  const maxPotentialPayout = input.wager * maxProfitMultiplier;
340
+ const riskLimits = betConfig.riskPolicy({
341
+ type: "get-limits",
342
+ currency: input.currency,
343
+ bankroll: dbHouseBankroll.amount,
344
+ });
287
345
  const riskResult = validateRisk({
346
+ type: "validate-bet",
288
347
  currency: input.currency,
289
348
  wager: input.wager,
290
349
  bankroll: dbHouseBankroll.amount,
291
350
  maxPotentialPayout,
292
- riskPolicy: betConfig.riskPolicy,
351
+ riskLimits,
293
352
  displayUnitName: dbCurrency.display_unit_name,
294
353
  displayUnitScale: dbCurrency.display_unit_scale,
295
354
  outcomes: input.outcomes,
296
355
  });
297
356
  if (!riskResult.ok) {
298
- throw new GraphQLError(riskResult.error);
357
+ return {
358
+ __typename: "HubRiskError",
359
+ message: riskResult.error.message,
360
+ riskLimits: riskResult.error.riskLimits,
361
+ };
299
362
  }
300
363
  await pgClient.query({
301
364
  text: `
@@ -354,14 +417,14 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
354
417
  .query({
355
418
  text: `
356
419
  INSERT INTO hub.outcome_bet (
357
- user_id,
358
- casino_id,
359
- experience_id,
420
+ user_id,
421
+ casino_id,
422
+ experience_id,
360
423
  hash_id,
361
424
  kind,
362
- currency_key,
363
- wager,
364
- profit,
425
+ currency_key,
426
+ wager,
427
+ profit,
365
428
  outcomes,
366
429
  outcome_idx,
367
430
  metadata
@@ -422,6 +485,28 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
422
485
  },
423
486
  },
424
487
  },
488
+ HubRiskError: {
489
+ assertStep: ObjectStep,
490
+ plans: {
491
+ message($data) {
492
+ return access($data, "message");
493
+ },
494
+ riskLimits($data) {
495
+ return access($data, "riskLimits");
496
+ },
497
+ },
498
+ },
499
+ HubRiskLimit: {
500
+ assertStep: ObjectStep,
501
+ plans: {
502
+ maxWager($data) {
503
+ return access($data, "maxWager");
504
+ },
505
+ maxPayout($data) {
506
+ return access($data, "maxPayout");
507
+ },
508
+ },
509
+ },
425
510
  HubMakeOutcomeBetPayload: {
426
511
  assertStep: ObjectStep,
427
512
  plans: {
@@ -107,8 +107,7 @@ export async function processTransfer({ casinoId, controllerId, transfer, graphq
107
107
  }
108
108
  logger.debug(data, "MP_CLAIM_TRANSFER response");
109
109
  if (data.claimTransfer?.result.__typename !== "ClaimTransferSuccess") {
110
- if (data.claimTransfer?.result.__typename ===
111
- "InvalidTransferStatus" &&
110
+ if (data.claimTransfer?.result.__typename === "InvalidTransferStatus" &&
112
111
  data.claimTransfer?.result.currentStatus ===
113
112
  TransferStatusKind.Completed) {
114
113
  logger.info(`Transfer ${transfer.id} already claimed (status: COMPLETED), skipping claim but attempting deposit insert`);
@@ -1,20 +1,29 @@
1
1
  import { DbOutcome } from "./db/types.js";
2
2
  import { Result } from "./util.js";
3
3
  export type RiskPolicyArgs = {
4
+ type: "validate-bet";
4
5
  currency: string;
5
6
  wager: number;
6
7
  bankroll: number;
7
8
  maxPotentialPayout: number;
8
9
  outcomes: DbOutcome[];
9
10
  };
11
+ export type BetLimitPolicyArgs = {
12
+ type: "get-limits";
13
+ currency: string;
14
+ bankroll: number;
15
+ };
10
16
  export type RiskLimits = {
11
17
  maxWager?: number;
12
18
  maxPayout: number;
13
19
  };
14
- export type RiskPolicy = (args: RiskPolicyArgs) => RiskLimits;
20
+ export type RiskPolicy = (args: RiskPolicyArgs | BetLimitPolicyArgs) => RiskLimits;
15
21
  export declare function validateRisk(options: RiskPolicyArgs & {
16
- riskPolicy: RiskPolicy;
22
+ riskLimits: RiskLimits;
17
23
  } & {
18
24
  displayUnitName: string;
19
25
  displayUnitScale: number;
20
- }): Result<void, string>;
26
+ }): Result<void, {
27
+ message: string;
28
+ riskLimits: RiskLimits;
29
+ }>;
@@ -1,5 +1,4 @@
1
1
  import { formatCurrency } from "./format-currency.js";
2
- import { logger } from "./logger.js";
3
2
  import { z } from "zod/v4";
4
3
  const RiskLimitsSchema = z
5
4
  .object({
@@ -8,53 +7,45 @@ const RiskLimitsSchema = z
8
7
  })
9
8
  .strict();
10
9
  export function validateRisk(options) {
11
- const { wager, bankroll, maxPotentialPayout, riskPolicy } = options;
10
+ const { wager, bankroll, maxPotentialPayout } = options;
12
11
  if (maxPotentialPayout > bankroll) {
12
+ const message = `House cannot cover potential payout (${formatCurrency(maxPotentialPayout, {
13
+ displayUnitName: options.displayUnitName,
14
+ displayUnitScale: options.displayUnitScale,
15
+ })}). Bankroll: ${formatCurrency(bankroll, {
16
+ displayUnitName: options.displayUnitName,
17
+ displayUnitScale: options.displayUnitScale,
18
+ })}`;
13
19
  return {
14
20
  ok: false,
15
- error: `House cannot cover potential payout (${formatCurrency(maxPotentialPayout, {
16
- displayUnitName: options.displayUnitName,
17
- displayUnitScale: options.displayUnitScale,
18
- })}). Bankroll: ${formatCurrency(bankroll, {
19
- displayUnitName: options.displayUnitName,
20
- displayUnitScale: options.displayUnitScale,
21
- })}`,
21
+ error: { message, riskLimits: options.riskLimits },
22
22
  };
23
23
  }
24
- if (!riskPolicy) {
25
- return { ok: true, value: undefined };
26
- }
27
- const limitsResult = RiskLimitsSchema.safeParse(riskPolicy(options));
28
- if (!limitsResult.success) {
29
- logger.error(limitsResult, "Invalid risk policy");
30
- return {
31
- ok: false,
32
- error: "Invalid risk policy",
33
- };
34
- }
35
- const limits = limitsResult.data;
36
- if (limits.maxWager !== undefined && wager > limits.maxWager) {
24
+ if (options.riskLimits.maxWager !== undefined &&
25
+ wager > options.riskLimits.maxWager) {
26
+ const message = `Wager (${formatCurrency(wager, {
27
+ displayUnitName: options.displayUnitName,
28
+ displayUnitScale: options.displayUnitScale,
29
+ })}) exceeds limit (${formatCurrency(options.riskLimits.maxWager, {
30
+ displayUnitName: options.displayUnitName,
31
+ displayUnitScale: options.displayUnitScale,
32
+ })})`;
37
33
  return {
38
34
  ok: false,
39
- error: `Wager (${formatCurrency(wager, {
40
- displayUnitName: options.displayUnitName,
41
- displayUnitScale: options.displayUnitScale,
42
- })}) exceeds limit (${formatCurrency(limits.maxWager, {
43
- displayUnitName: options.displayUnitName,
44
- displayUnitScale: options.displayUnitScale,
45
- })})`,
35
+ error: { message, riskLimits: options.riskLimits },
46
36
  };
47
37
  }
48
- if (maxPotentialPayout > limits.maxPayout) {
38
+ if (maxPotentialPayout > options.riskLimits.maxPayout) {
39
+ const message = `Payout (${formatCurrency(maxPotentialPayout, {
40
+ displayUnitName: options.displayUnitName,
41
+ displayUnitScale: options.displayUnitScale,
42
+ })}) exceeds limit (${formatCurrency(options.riskLimits.maxPayout, {
43
+ displayUnitName: options.displayUnitName,
44
+ displayUnitScale: options.displayUnitScale,
45
+ })})`;
49
46
  return {
50
47
  ok: false,
51
- error: `Payout (${formatCurrency(maxPotentialPayout, {
52
- displayUnitName: options.displayUnitName,
53
- displayUnitScale: options.displayUnitScale,
54
- })}) exceeds limit (${formatCurrency(limits.maxPayout, {
55
- displayUnitName: options.displayUnitName,
56
- displayUnitScale: options.displayUnitScale,
57
- })})`,
48
+ error: { message, riskLimits: options.riskLimits },
58
49
  };
59
50
  }
60
51
  return { ok: true, value: undefined };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.17.1",
3
+ "version": "1.18.0-dev.1",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [
@@ -37,6 +37,7 @@
37
37
  "check": "tsc --noEmit && npm run check-dashboard",
38
38
  "check-tests": "tsc -p tests/tsconfig.json --noEmit",
39
39
  "check-dashboard": "cd ./dashboard && npm run check",
40
+ "format": "prettier --write \"**/*.ts\"",
40
41
  "lint": "eslint --fix . && npm run lint-dashboard",
41
42
  "lint-dashboard": "cd ./dashboard && npm run lint",
42
43
  "prepublishOnly": "npm run build",
@@ -77,6 +78,7 @@
77
78
  "eslint": "^9.8.0",
78
79
  "globals": "^16.0.0",
79
80
  "pino-pretty": "^13.0.0",
81
+ "prettier": "^3.6.2",
80
82
  "supertest": "^7.0.0",
81
83
  "tsx": "^4.20.3",
82
84
  "typescript": "^5.4.5",