@moneypot/hub 1.8.0 → 1.9.0-dev.10

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/README.md CHANGED
@@ -76,6 +76,13 @@ insert into hub.api_key default values returning key;
76
76
 
77
77
  ## Changelog
78
78
 
79
+ ### 1.8.x
80
+
81
+ - Added [pino](https://getpino.io) for logging.
82
+ - Added `LOG_LEVEL` environment variable to set the logging level. (default: `info`)
83
+ - Added `LOG_PRETTY` environment variable to force enable/disable pretty logging (requires `npm install pino-pretty`).
84
+ - Exposed `@moneypot/hub/logger`. Example: `import { logger } from "@moneypot/hub/logger";`
85
+
79
86
  ### 1.7.x
80
87
 
81
88
  Updated Hub to handle breaking changes in moneypot-server GraphQL transfer API.
@@ -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,7 @@ declare global {
15
15
  }
16
16
  }
17
17
  }
18
- export { MakeOutcomeBetPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, } from "./plugins/hub-make-outcome-bet.js";
18
+ export { MakeOutcomeBetPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, type RiskPolicy, type RiskLimits, } from "./plugins/hub-make-outcome-bet.js";
19
19
  export type PluginContext = Grafast.Context;
20
20
  export { defaultPlugins, type PluginIdentity, type UserSessionContext, } from "./server/graphile.config.js";
21
21
  export type ServerOptions = {
package/dist/src/index.js CHANGED
@@ -22,7 +22,7 @@ async function initialize(options) {
22
22
  });
23
23
  }
24
24
  catch (e) {
25
- logger.error("Error upgrading core schema", e);
25
+ logger.error(e, "Error upgrading core schema");
26
26
  if (e instanceof DatabaseAheadError) {
27
27
  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
28
  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,4 +1,4 @@
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
4
  declare const InputSchema: z.ZodObject<{
@@ -66,6 +66,7 @@ type FinalizeMetadataData = {
66
66
  };
67
67
  export type OutcomeBetConfig = {
68
68
  houseEdge: number;
69
+ riskPolicy: RiskPolicy;
69
70
  saveOutcomes: boolean;
70
71
  allowLossBeyondWager?: boolean;
71
72
  initializeMetadataFromUntrustedUserInput?: (input: Input) => Result<Metadata, string>;
@@ -74,6 +75,16 @@ export type OutcomeBetConfig = {
74
75
  export type OutcomeBetConfigMap<BetKind extends string> = {
75
76
  [betKind in BetKind]: OutcomeBetConfig;
76
77
  };
78
+ type AtLeastOneKey<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Required<Pick<T, Keys>> & Partial<Omit<T, Keys>> : never;
79
+ export type RiskLimits = AtLeastOneKey<{
80
+ maxWager?: number;
81
+ maxPayout?: number;
82
+ }>;
83
+ export type RiskPolicy = (args: {
84
+ currency: string;
85
+ wager: number;
86
+ bankroll: number;
87
+ }) => RiskLimits;
77
88
  export declare function MakeOutcomeBetPlugin<BetKind extends string>({ betConfigs }: {
78
89
  betConfigs: OutcomeBetConfigMap<BetKind>;
79
90
  }): 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 { formatCurrency } from "../format-currency.js";
11
12
  const FLOAT_EPSILON = 1e-10;
12
13
  function sum(ns) {
13
14
  return ns.reduce((a, b) => a + b, 0);
@@ -55,6 +56,7 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
55
56
  .gte(0, "House edge must be >= 0")
56
57
  .lte(1, "House edge must be <= 1"),
57
58
  saveOutcomes: z.boolean(),
59
+ riskPolicy: z.function(),
58
60
  allowLossBeyondWager: z.boolean().default(false),
59
61
  initializeMetadataFromUntrustedUserInput: z
60
62
  .function()
@@ -63,6 +65,15 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
63
65
  .function()
64
66
  .optional(),
65
67
  }));
68
+ const RiskLimitsSchema = z
69
+ .object({
70
+ maxWager: z.number().positive().optional(),
71
+ maxPayout: z.number().positive().optional(),
72
+ })
73
+ .strict()
74
+ .refine((v) => v.maxWager !== undefined || v.maxPayout !== undefined, {
75
+ message: "Provide at least one of maxWager or maxPayout.",
76
+ });
66
77
  export function MakeOutcomeBetPlugin({ betConfigs }) {
67
78
  BetConfigsSchema.parse(betConfigs);
68
79
  const betKinds = Object.keys(betConfigs);
@@ -149,7 +160,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
149
160
  const dbCurrency = await superuserPool
150
161
  .query({
151
162
  text: `
152
- SELECT key
163
+ SELECT key, display_unit_name, display_unit_scale
153
164
  FROM hub.currency
154
165
  WHERE key = $1
155
166
  AND casino_id = $2
@@ -185,10 +196,47 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
185
196
  throw new GraphQLError("You cannot afford the worst outcome");
186
197
  }
187
198
  const maxProfitMultiplier = Math.max(...input.outcomes.map((o) => o.profit));
188
- const maxPotentialPayout = input.wager * maxProfitMultiplier;
189
- const maxAllowablePayout = dbHouseBankroll.amount * 0.01;
190
- if (maxPotentialPayout > maxAllowablePayout) {
191
- throw new GraphQLError(`House risk limit exceeded. Max payout: ${maxPotentialPayout.toFixed(4)}`);
199
+ const maxPayout = input.wager * maxProfitMultiplier;
200
+ if (maxPayout > dbHouseBankroll.amount) {
201
+ throw new GraphQLError(`House cannot cover potential payout (${formatCurrency(maxPayout, {
202
+ displayUnitName: dbCurrency.display_unit_name,
203
+ displayUnitScale: dbCurrency.display_unit_scale,
204
+ })}). Bankroll: ${formatCurrency(dbHouseBankroll.amount, {
205
+ displayUnitName: dbCurrency.display_unit_name,
206
+ displayUnitScale: dbCurrency.display_unit_scale,
207
+ })}`);
208
+ }
209
+ const riskLimitsResult = RiskLimitsSchema.safeParse(betConfig.riskPolicy
210
+ ? betConfig.riskPolicy({
211
+ currency: input.currency,
212
+ wager: input.wager,
213
+ bankroll: dbHouseBankroll.amount,
214
+ })
215
+ : {});
216
+ if (!riskLimitsResult.success) {
217
+ logger.error(riskLimitsResult.error, "Invalid risk policy");
218
+ throw new GraphQLError("Invalid risk policy");
219
+ }
220
+ const riskLimits = riskLimitsResult.data;
221
+ if (riskLimits.maxWager != null &&
222
+ input.wager > riskLimits.maxWager) {
223
+ throw new GraphQLError(`Wager exceeds limit (${formatCurrency(riskLimits.maxWager, {
224
+ displayUnitName: dbCurrency.display_unit_name,
225
+ displayUnitScale: dbCurrency.display_unit_scale,
226
+ })}). Your wager: ${formatCurrency(input.wager, {
227
+ displayUnitName: dbCurrency.display_unit_name,
228
+ displayUnitScale: dbCurrency.display_unit_scale,
229
+ })}`);
230
+ }
231
+ if (riskLimits.maxPayout != null &&
232
+ maxPayout > riskLimits.maxPayout) {
233
+ throw new GraphQLError(`Payout exceeds limit (${formatCurrency(riskLimits.maxPayout, {
234
+ displayUnitName: dbCurrency.display_unit_name,
235
+ displayUnitScale: dbCurrency.display_unit_scale,
236
+ })}). Your payout: ${formatCurrency(maxPayout, {
237
+ displayUnitName: dbCurrency.display_unit_name,
238
+ displayUnitScale: dbCurrency.display_unit_scale,
239
+ })}`);
192
240
  }
193
241
  const dbHashChain = await dbLockHubHashChain(pgClient, {
194
242
  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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.8.0",
3
+ "version": "1.9.0-dev.10",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [