@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 +7 -0
- package/dist/src/db/index.js +1 -1
- package/dist/src/format-currency.d.ts +9 -0
- package/dist/src/format-currency.js +40 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +1 -1
- package/dist/src/plugins/hub-make-outcome-bet.d.ts +12 -1
- package/dist/src/plugins/hub-make-outcome-bet.js +54 -6
- package/dist/src/process-transfers/index.js +1 -1
- package/dist/src/process-withdrawal-request.js +1 -1
- package/package.json +1 -1
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.
|
package/dist/src/db/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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"
|
|
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);
|
package/dist/src/logger.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/logger.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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}
|
|
142
|
+
logger.error(error, `Failed to process withdrawal request ${request.id}`);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
}
|