@moneypot/hub 1.9.0-dev.1 → 1.9.0-dev.11

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.
@@ -132,7 +132,7 @@ export async function getTransferCursor(pgClient, { casinoId, }) {
132
132
  return row?.cursor;
133
133
  }
134
134
  export async function setTransferCursor(pgClient, { cursor, casinoId, }) {
135
- logger.debug(cursor, `[setTransferCursor] Setting cursor`);
135
+ logger.debug({ cursor }, `[setTransferCursor] Setting cursor`);
136
136
  await pgClient.query(`
137
137
  insert into hub_hidden.transfer_cursor (casino_id, cursor)
138
138
  values ($1, $2)
@@ -0,0 +1,9 @@
1
+ export declare function formatCurrency(amount: number, currency: {
2
+ displayUnitScale: number;
3
+ displayUnitName: string;
4
+ }, options?: {
5
+ excludeUnit?: boolean;
6
+ }): string;
7
+ export declare function pluralize(word: string, count: number, suffix?: string): string;
8
+ export declare function getDecimalPlaces(displayUnitScale: number): number;
9
+ export declare function truncateDecimalPrecision(value: number, decimals: number): number;
@@ -0,0 +1,40 @@
1
+ export function formatCurrency(amount, currency, options = {
2
+ excludeUnit: false,
3
+ }) {
4
+ const decimalPlaces = getDecimalPlaces(currency.displayUnitScale);
5
+ const scaledAmount = amount / (currency.displayUnitScale || 1);
6
+ const truncatedAmount = truncateDecimalPrecision(scaledAmount, decimalPlaces);
7
+ const formatter = new Intl.NumberFormat("en-US", {
8
+ minimumFractionDigits: decimalPlaces,
9
+ maximumFractionDigits: decimalPlaces,
10
+ useGrouping: true,
11
+ });
12
+ const formatted = formatter.format(truncatedAmount);
13
+ if (options.excludeUnit) {
14
+ return formatted;
15
+ }
16
+ return `${formatted} ${pluralize(currency.displayUnitName, truncatedAmount)}`;
17
+ }
18
+ export function pluralize(word, count, suffix = "s") {
19
+ return count === 1 ? word : word + suffix;
20
+ }
21
+ export function getDecimalPlaces(displayUnitScale) {
22
+ return displayUnitScale < 10 ? 0 : Math.log10(displayUnitScale);
23
+ }
24
+ export function truncateDecimalPrecision(value, decimals) {
25
+ if (decimals < 0) {
26
+ throw new Error("Decimals must be a non-negative integer");
27
+ }
28
+ if (!Number.isFinite(value)) {
29
+ return value;
30
+ }
31
+ const str = value.toString();
32
+ const dotIndex = str.indexOf(".");
33
+ if (dotIndex === -1) {
34
+ return value;
35
+ }
36
+ const truncatedStr = decimals === 0
37
+ ? str.substring(0, dotIndex)
38
+ : str.substring(0, dotIndex + decimals + 1);
39
+ return parseFloat(truncatedStr);
40
+ }
@@ -15,7 +15,8 @@ declare global {
15
15
  }
16
16
  }
17
17
  }
18
- export { MakeOutcomeBetPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, type RiskPolicy, type RiskLimits, } from "./plugins/hub-make-outcome-bet.js";
18
+ export { MakeOutcomeBetPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, } from "./plugins/hub-make-outcome-bet.js";
19
+ export { validateRisk, type RiskPolicy, type RiskPolicyArgs, type RiskLimits, } from "./risk-policy.js";
19
20
  export type PluginContext = Grafast.Context;
20
21
  export { defaultPlugins, type PluginIdentity, type UserSessionContext, } from "./server/graphile.config.js";
21
22
  export type ServerOptions = {
package/dist/src/index.js CHANGED
@@ -6,6 +6,7 @@ import { initializeTransferProcessors } from "./process-transfers/index.js";
6
6
  import { join } from "path";
7
7
  import { logger } from "./logger.js";
8
8
  export { MakeOutcomeBetPlugin, } from "./plugins/hub-make-outcome-bet.js";
9
+ export { validateRisk, } from "./risk-policy.js";
9
10
  export { defaultPlugins, } from "./server/graphile.config.js";
10
11
  async function initialize(options) {
11
12
  if (options.signal.aborted) {
@@ -22,7 +23,7 @@ async function initialize(options) {
22
23
  });
23
24
  }
24
25
  catch (e) {
25
- logger.error("Error upgrading core schema", e);
26
+ logger.error(e, "Error upgrading core schema");
26
27
  if (e instanceof DatabaseAheadError) {
27
28
  logger.error(`${"⚠️".repeat(10)}\n@moneypot/hub database was reset to prepare for a production release and you must reset your database to continue. Please see <https://www.npmjs.com/package/@moneypot/hub#change-log> for more info.`);
28
29
  process.exit(1);
@@ -1,3 +1,3 @@
1
- import { type Logger } from "pino";
1
+ import pino, { type Logger } from "pino";
2
2
  export { type Logger };
3
- export declare const logger: Logger;
3
+ export declare const logger: pino.Logger;
@@ -1,4 +1,4 @@
1
- import { pino } from "pino";
1
+ import pino from "pino";
2
2
  import { LOG_LEVEL, LOG_PRETTY, NODE_ENV } from "./config.js";
3
3
  function createLogger(options) {
4
4
  const prettify = LOG_PRETTY === undefined ? NODE_ENV !== "production" : LOG_PRETTY;
@@ -1,6 +1,7 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import { DbOutcome } from "../db/index.js";
3
3
  import { Result } from "../util.js";
4
+ import { RiskPolicy } from "../risk-policy.js";
4
5
  declare const InputSchema: z.ZodObject<{
5
6
  kind: z.ZodString;
6
7
  clientSeed: z.ZodString;
@@ -75,15 +76,6 @@ export type OutcomeBetConfig = {
75
76
  export type OutcomeBetConfigMap<BetKind extends string> = {
76
77
  [betKind in BetKind]: OutcomeBetConfig;
77
78
  };
78
- export type RiskLimits = {
79
- maxWager?: number;
80
- maxPayout?: number;
81
- };
82
- export type RiskPolicy = (args: {
83
- currency: string;
84
- wager: number;
85
- bankroll: number;
86
- }) => RiskLimits;
87
79
  export declare function MakeOutcomeBetPlugin<BetKind extends string>({ betConfigs }: {
88
80
  betConfigs: OutcomeBetConfigMap<BetKind>;
89
81
  }): GraphileConfig.Plugin;
@@ -1,6 +1,6 @@
1
1
  import { access, context, object, ObjectStep, sideEffect, } from "postgraphile/grafast";
2
2
  import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
- import { z } from "zod";
3
+ import * as z from "zod";
4
4
  import { GraphQLError } from "graphql";
5
5
  import { DbHashKind, dbLockPlayerBalanceAndHouseBankroll, exactlyOneRow, maybeOneRow, superuserPool, withPgPoolTransaction, } from "../db/index.js";
6
6
  import { assert } from "tsafe";
@@ -8,6 +8,7 @@ import { dbInsertHubHash, dbLockHubHashChain, } from "../hash-chain/db-hash-chai
8
8
  import { getIntermediateHash, getPreimageHash, } from "../hash-chain/get-hash.js";
9
9
  import { makeFinalHash, pickRandomOutcome } from "../hash-chain/util.js";
10
10
  import { logger } from "../logger.js";
11
+ import { validateRisk } from "../risk-policy.js";
11
12
  const FLOAT_EPSILON = 1e-10;
12
13
  function sum(ns) {
13
14
  return ns.reduce((a, b) => a + b, 0);
@@ -64,10 +65,6 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
64
65
  .function()
65
66
  .optional(),
66
67
  }));
67
- const RiskLimitsSchema = z.object({
68
- maxWager: z.number().finite().int().positive(),
69
- maxPayout: z.number().finite().int().positive(),
70
- });
71
68
  export function MakeOutcomeBetPlugin({ betConfigs }) {
72
69
  BetConfigsSchema.parse(betConfigs);
73
70
  const betKinds = Object.keys(betConfigs);
@@ -154,7 +151,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
154
151
  const dbCurrency = await superuserPool
155
152
  .query({
156
153
  text: `
157
- SELECT key
154
+ SELECT key, display_unit_name, display_unit_scale
158
155
  FROM hub.currency
159
156
  WHERE key = $1
160
157
  AND casino_id = $2
@@ -190,28 +187,18 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
190
187
  throw new GraphQLError("You cannot afford the worst outcome");
191
188
  }
192
189
  const maxProfitMultiplier = Math.max(...input.outcomes.map((o) => o.profit));
193
- const maxPayout = input.wager * maxProfitMultiplier;
194
- if (maxPayout > dbHouseBankroll.amount) {
195
- throw new GraphQLError(`House cannot cover potential payout (${maxPayout}). Bankroll: ${dbHouseBankroll.amount}`);
196
- }
197
- const riskLimitsResult = RiskLimitsSchema.safeParse(betConfig.riskPolicy
198
- ? betConfig.riskPolicy({
199
- currency: input.currency,
200
- wager: input.wager,
201
- bankroll: dbHouseBankroll.amount,
202
- })
203
- : {});
204
- if (!riskLimitsResult.success) {
205
- throw new GraphQLError(`Invalid risk policy: ${riskLimitsResult.error.issues[0].message}`);
206
- }
207
- const riskLimits = riskLimitsResult.data;
208
- if (riskLimits.maxWager != null &&
209
- input.wager > riskLimits.maxWager) {
210
- throw new GraphQLError(`Wager exceeds limit (${riskLimits.maxWager}). Your wager: ${input.wager}`);
211
- }
212
- if (riskLimits.maxPayout != null &&
213
- maxPayout > riskLimits.maxPayout) {
214
- throw new GraphQLError(`Payout exceeds limit (${riskLimits.maxPayout}). Your payout: ${maxPayout}`);
190
+ const maxPotentialPayout = input.wager * maxProfitMultiplier;
191
+ const riskResult = validateRisk({
192
+ currency: input.currency,
193
+ wager: input.wager,
194
+ bankroll: dbHouseBankroll.amount,
195
+ maxPotentialPayout,
196
+ riskPolicy: betConfig.riskPolicy,
197
+ displayUnitName: dbCurrency.display_unit_name,
198
+ displayUnitScale: dbCurrency.display_unit_scale,
199
+ });
200
+ if (!riskResult.ok) {
201
+ throw new GraphQLError(riskResult.error);
215
202
  }
216
203
  const dbHashChain = await dbLockHubHashChain(pgClient, {
217
204
  userId: session.user_id,
@@ -46,7 +46,7 @@ export function initializeTransferProcessors({ signal, }) {
46
46
  await listenForNewCasinos({ signal });
47
47
  }
48
48
  catch (e) {
49
- logger.error(`Error initializing transfer processors:`, e);
49
+ logger.error(e, `Error initializing transfer processors`);
50
50
  }
51
51
  })();
52
52
  }
@@ -139,7 +139,7 @@ export async function processWithdrawalRequests({ casinoId, graphqlClient, }) {
139
139
  });
140
140
  }
141
141
  catch (error) {
142
- logger.error(`Failed to process withdrawal request ${request.id}:`, error);
142
+ logger.error(error, `Failed to process withdrawal request ${request.id}`);
143
143
  }
144
144
  }
145
145
  }
@@ -0,0 +1,20 @@
1
+ import { Result } from "./util.js";
2
+ export type RiskPolicyArgs = {
3
+ currency: string;
4
+ wager: number;
5
+ bankroll: number;
6
+ maxPotentialPayout: number;
7
+ };
8
+ type AtLeastOneKey<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Required<Pick<T, Keys>> & Partial<Omit<T, Keys>> : never;
9
+ export type RiskLimits = AtLeastOneKey<{
10
+ maxWager?: number;
11
+ maxPayout?: number;
12
+ }>;
13
+ export type RiskPolicy = (args: RiskPolicyArgs) => RiskLimits;
14
+ export declare function validateRisk(options: RiskPolicyArgs & {
15
+ riskPolicy: RiskPolicy;
16
+ } & {
17
+ displayUnitName: string;
18
+ displayUnitScale: number;
19
+ }): Result<void, string>;
20
+ export {};
@@ -0,0 +1,64 @@
1
+ import { formatCurrency } from "./format-currency.js";
2
+ import { logger } from "./logger.js";
3
+ import { z } from "zod";
4
+ const RiskLimitsSchema = z
5
+ .object({
6
+ maxWager: z.number().positive().optional(),
7
+ maxPayout: z.number().positive().optional(),
8
+ })
9
+ .strict()
10
+ .refine((v) => v.maxWager !== undefined || v.maxPayout !== undefined, {
11
+ message: "Provide at least one of maxWager or maxPayout.",
12
+ });
13
+ export function validateRisk(options) {
14
+ const { wager, bankroll, maxPotentialPayout, riskPolicy } = options;
15
+ if (maxPotentialPayout > bankroll) {
16
+ return {
17
+ ok: false,
18
+ error: `House cannot cover potential payout (${formatCurrency(maxPotentialPayout, {
19
+ displayUnitName: options.displayUnitName,
20
+ displayUnitScale: options.displayUnitScale,
21
+ })}). Bankroll: ${formatCurrency(bankroll, {
22
+ displayUnitName: options.displayUnitName,
23
+ displayUnitScale: options.displayUnitScale,
24
+ })}`,
25
+ };
26
+ }
27
+ if (!riskPolicy) {
28
+ return { ok: true, value: undefined };
29
+ }
30
+ const limitsResult = RiskLimitsSchema.safeParse(riskPolicy(options));
31
+ if (!limitsResult.success) {
32
+ logger.error(limitsResult, "Invalid risk policy");
33
+ return {
34
+ ok: false,
35
+ error: "Invalid risk policy",
36
+ };
37
+ }
38
+ const limits = limitsResult.data;
39
+ if (limits.maxWager !== undefined && wager > limits.maxWager) {
40
+ return {
41
+ ok: false,
42
+ error: `Wager (${formatCurrency(wager, {
43
+ displayUnitName: options.displayUnitName,
44
+ displayUnitScale: options.displayUnitScale,
45
+ })}) exceeds limit (${formatCurrency(limits.maxWager, {
46
+ displayUnitName: options.displayUnitName,
47
+ displayUnitScale: options.displayUnitScale,
48
+ })})`,
49
+ };
50
+ }
51
+ if (limits.maxPayout !== undefined && maxPotentialPayout > limits.maxPayout) {
52
+ return {
53
+ ok: false,
54
+ error: `Payout (${formatCurrency(maxPotentialPayout, {
55
+ displayUnitName: options.displayUnitName,
56
+ displayUnitScale: options.displayUnitScale,
57
+ })}) exceeds limit (${formatCurrency(limits.maxPayout, {
58
+ displayUnitName: options.displayUnitName,
59
+ displayUnitScale: options.displayUnitScale,
60
+ })})`,
61
+ };
62
+ }
63
+ return { ok: true, value: undefined };
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.9.0-dev.1",
3
+ "version": "1.9.0-dev.11",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [