@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.
- 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 +2 -1
- package/dist/src/index.js +2 -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 +2 -10
- package/dist/src/plugins/hub-make-outcome-bet.js +15 -28
- package/dist/src/process-transfers/index.js +1 -1
- package/dist/src/process-withdrawal-request.js +1 -1
- package/dist/src/risk-policy.d.ts +20 -0
- package/dist/src/risk-policy.js +64 -0
- package/package.json +1 -1
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,8 @@ declare global {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
export { MakeOutcomeBetPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig,
|
|
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"
|
|
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);
|
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,6 +1,7 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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
|
+
}
|