@moneypot/hub 1.17.1 → 1.18.0-dev.2

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,70 @@ 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 HubBulkRiskLimit {
116
+ betKind: BetKind!
117
+ currency: String!
118
+ maxPayout: Float!
119
+ maxWager: Float
120
+ }
121
+
122
+ extend type Query {
123
+ hubRiskLimits(betKinds: [BetKind!]!): [HubBulkRiskLimit!]!
124
+ }
104
125
  `;
105
126
  return {
106
127
  typeDefs,
107
128
  objects: {
129
+ Query: {
130
+ plans: {
131
+ hubRiskLimits: (_, { $betKinds }) => {
132
+ const $identity = context().get("identity");
133
+ const $superuserPool = context().get("superuserPool");
134
+ const $result = sideEffect([$identity, $superuserPool, $betKinds], async ([identity, superuserPool, inputBetKinds]) => {
135
+ if (identity?.kind !== "user") {
136
+ throw new GraphQLError("Unauthorized");
137
+ }
138
+ if (inputBetKinds.length > 5) {
139
+ throw new GraphQLError("Maximum 5 bet kinds allowed");
140
+ }
141
+ if (new Set(inputBetKinds).size !== inputBetKinds.length) {
142
+ throw new GraphQLError("Duplicate bet kinds not allowed");
143
+ }
144
+ for (const kind of inputBetKinds) {
145
+ if (!betConfigs[kind]) {
146
+ throw new GraphQLError(`Invalid bet kind: ${kind}`);
147
+ }
148
+ }
149
+ const dbHouseBankrolls = await dbGetHouseBankrolls(superuserPool, {
150
+ casinoId: identity.session.casino_id,
151
+ });
152
+ const limits = inputBetKinds.flatMap((betKind) => {
153
+ const betConfig = betConfigs[betKind];
154
+ return dbHouseBankrolls.map((bankroll) => {
155
+ const riskLimits = betConfig.riskPolicy({
156
+ type: "get-limits",
157
+ currency: bankroll.currency_key,
158
+ bankroll: bankroll.amount,
159
+ });
160
+ return {
161
+ betKind,
162
+ currency: bankroll.currency_key,
163
+ ...riskLimits,
164
+ };
165
+ });
166
+ });
167
+ return limits;
168
+ });
169
+ return $result;
170
+ },
171
+ },
172
+ },
108
173
  Mutation: {
109
174
  plans: {
110
175
  hubMakeOutcomeBet: (_, { $input }) => {
@@ -284,18 +349,28 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
284
349
  }
285
350
  const maxProfitMultiplier = Math.max(...input.outcomes.map((o) => o.profit));
286
351
  const maxPotentialPayout = input.wager * maxProfitMultiplier;
352
+ const riskLimits = betConfig.riskPolicy({
353
+ type: "get-limits",
354
+ currency: input.currency,
355
+ bankroll: dbHouseBankroll.amount,
356
+ });
287
357
  const riskResult = validateRisk({
358
+ type: "validate-bet",
288
359
  currency: input.currency,
289
360
  wager: input.wager,
290
361
  bankroll: dbHouseBankroll.amount,
291
362
  maxPotentialPayout,
292
- riskPolicy: betConfig.riskPolicy,
363
+ riskLimits,
293
364
  displayUnitName: dbCurrency.display_unit_name,
294
365
  displayUnitScale: dbCurrency.display_unit_scale,
295
366
  outcomes: input.outcomes,
296
367
  });
297
368
  if (!riskResult.ok) {
298
- throw new GraphQLError(riskResult.error);
369
+ return {
370
+ __typename: "HubRiskError",
371
+ message: riskResult.error.message,
372
+ riskLimits: riskResult.error.riskLimits,
373
+ };
299
374
  }
300
375
  await pgClient.query({
301
376
  text: `
@@ -354,14 +429,14 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
354
429
  .query({
355
430
  text: `
356
431
  INSERT INTO hub.outcome_bet (
357
- user_id,
358
- casino_id,
359
- experience_id,
432
+ user_id,
433
+ casino_id,
434
+ experience_id,
360
435
  hash_id,
361
436
  kind,
362
- currency_key,
363
- wager,
364
- profit,
437
+ currency_key,
438
+ wager,
439
+ profit,
365
440
  outcomes,
366
441
  outcome_idx,
367
442
  metadata
@@ -422,6 +497,28 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
422
497
  },
423
498
  },
424
499
  },
500
+ HubRiskError: {
501
+ assertStep: ObjectStep,
502
+ plans: {
503
+ message($data) {
504
+ return access($data, "message");
505
+ },
506
+ riskLimits($data) {
507
+ return access($data, "riskLimits");
508
+ },
509
+ },
510
+ },
511
+ HubRiskLimit: {
512
+ assertStep: ObjectStep,
513
+ plans: {
514
+ maxWager($data) {
515
+ return access($data, "maxWager");
516
+ },
517
+ maxPayout($data) {
518
+ return access($data, "maxPayout");
519
+ },
520
+ },
521
+ },
425
522
  HubMakeOutcomeBetPayload: {
426
523
  assertStep: ObjectStep,
427
524
  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.2",
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",