@moneypot/hub 1.18.0-dev.2 → 1.18.0-dev.4
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 +8 -0
- package/dist/src/custom-game-config.d.ts +7 -0
- package/dist/src/custom-game-config.js +1 -0
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.js +7 -4
- package/dist/src/plugins/hub-game-config-plugin.d.ts +15 -0
- package/dist/src/plugins/hub-game-config-plugin.js +25 -0
- package/dist/src/plugins/hub-make-outcome-bet.d.ts +4 -4
- package/dist/src/plugins/hub-make-outcome-bet.js +12 -82
- package/dist/src/plugins/hub-risk-limits.d.ts +6 -0
- package/dist/src/plugins/hub-risk-limits.js +128 -0
- package/dist/src/process-withdrawal-request.d.ts +1 -7
- package/dist/src/process-withdrawal-request.js +1 -146
- package/dist/src/risk-policy.d.ts +0 -1
- package/dist/src/risk-policy.js +0 -15
- package/dist/src/server/graphile.config.d.ts +3 -1
- package/dist/src/server/graphile.config.js +14 -2
- package/dist/src/server/index.d.ts +5 -2
- package/dist/src/server/index.js +6 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -77,6 +77,14 @@ insert into hub.api_key default values returning key;
|
|
|
77
77
|
|
|
78
78
|
You should always keep your hub server up to date as soon as possible. Never install an old, intermediate version: it may leave you without an automatic upgrade path.
|
|
79
79
|
|
|
80
|
+
### 1.18.x
|
|
81
|
+
|
|
82
|
+
- `MakeOutcomeBetPlugin` replaced by `HubGameConfigPlugin({ outcomeBetConfigs,
|
|
83
|
+
customGameConfigs })`
|
|
84
|
+
- Added graphql query `hubRiskLimits(gameKinds: [DICE])` to get risk limits for
|
|
85
|
+
the game kinds configured in `HubGameConfigPlugin`.
|
|
86
|
+
- This lets experiences display the max payout for each kind of bet.
|
|
87
|
+
|
|
80
88
|
### 1.17.x
|
|
81
89
|
|
|
82
90
|
- Postgres transactions now default to read committed (postgres default)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -17,22 +17,25 @@ declare global {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
export {
|
|
20
|
+
export { HubGameConfigPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, } from "./plugins/hub-game-config-plugin.js";
|
|
21
|
+
export { type CustomGameConfig, type CustomGameConfigMap, } from "./custom-game-config.js";
|
|
21
22
|
export { validateRisk, type RiskPolicy, type RiskPolicyArgs, type RiskLimits, } from "./risk-policy.js";
|
|
22
23
|
export type PluginContext = Grafast.Context;
|
|
23
|
-
export { defaultPlugins, type PluginIdentity, type UserSessionContext, } from "./server/graphile.config.js";
|
|
24
|
+
export { defaultPlugins, type PluginIdentity, type UserSessionContext, type HubPlugin, } from "./server/graphile.config.js";
|
|
24
25
|
export type ConfigureAppArgs = {
|
|
25
26
|
app: Express;
|
|
26
27
|
superuserPool: ServerContext["superuserPool"];
|
|
27
28
|
};
|
|
29
|
+
import type { HubPlugin } from "./server/graphile.config.js";
|
|
28
30
|
export type ServerOptions = {
|
|
29
31
|
configureApp?: (args: ConfigureAppArgs) => void;
|
|
30
|
-
plugins?: readonly
|
|
32
|
+
plugins?: readonly HubPlugin[];
|
|
31
33
|
extraPgSchemas?: string[];
|
|
32
34
|
exportSchemaSDLPath?: string;
|
|
33
35
|
userDatabaseMigrationsPath?: string;
|
|
34
36
|
enableChat?: boolean;
|
|
35
37
|
enablePlayground?: boolean;
|
|
38
|
+
port?: number;
|
|
36
39
|
};
|
|
37
40
|
export declare function runMigrations(options: {
|
|
38
41
|
userDatabaseMigrationsPath?: string;
|
package/dist/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { initializeTransferProcessors } from "./process-transfers/index.js";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { logger } from "./logger.js";
|
|
8
8
|
import { createServerContext, closeServerContext, } from "./context.js";
|
|
9
|
-
export {
|
|
9
|
+
export { HubGameConfigPlugin, } from "./plugins/hub-game-config-plugin.js";
|
|
10
10
|
export { validateRisk, } from "./risk-policy.js";
|
|
11
11
|
export { defaultPlugins, } from "./server/graphile.config.js";
|
|
12
12
|
export async function runMigrations(options) {
|
|
@@ -28,6 +28,7 @@ export async function runMigrations(options) {
|
|
|
28
28
|
pgClient,
|
|
29
29
|
dirname: join(import.meta.dirname, "pg-versions"),
|
|
30
30
|
schemaName: "hub_core_versions",
|
|
31
|
+
silent: process.env.NODE_ENV === "test",
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
catch (e) {
|
|
@@ -43,6 +44,7 @@ export async function runMigrations(options) {
|
|
|
43
44
|
pgClient,
|
|
44
45
|
dirname: options.userDatabaseMigrationsPath,
|
|
45
46
|
schemaName: "hub_user_versions",
|
|
47
|
+
silent: process.env.NODE_ENV === "test",
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -71,7 +73,7 @@ async function initialize(options) {
|
|
|
71
73
|
}
|
|
72
74
|
export async function startAndListen(options) {
|
|
73
75
|
if (options.plugins && options.plugins.some((p) => typeof p === "function")) {
|
|
74
|
-
throw new Error("`plugins` should be an array of
|
|
76
|
+
throw new Error("`plugins` should be an array of HubPlugin but one of the items is a function. Did you forget to call it?");
|
|
75
77
|
}
|
|
76
78
|
if (options.userDatabaseMigrationsPath &&
|
|
77
79
|
!options.userDatabaseMigrationsPath.startsWith("/")) {
|
|
@@ -98,6 +100,7 @@ export async function startAndListen(options) {
|
|
|
98
100
|
context,
|
|
99
101
|
enableChat: options.enableChat ?? true,
|
|
100
102
|
enablePlayground: options.enablePlayground ?? true,
|
|
103
|
+
port: options.port ?? config.PORT,
|
|
101
104
|
});
|
|
102
105
|
const gracefulShutdown = async ({ exit = true } = {}) => {
|
|
103
106
|
if (isShuttingDown) {
|
|
@@ -128,9 +131,9 @@ export async function startAndListen(options) {
|
|
|
128
131
|
process.on("SIGINT", gracefulShutdown);
|
|
129
132
|
process.on("SIGTERM", gracefulShutdown);
|
|
130
133
|
}
|
|
131
|
-
return hubServer.listen().then(() => {
|
|
134
|
+
return hubServer.listen().then(({ port }) => {
|
|
132
135
|
return {
|
|
133
|
-
port
|
|
136
|
+
port,
|
|
134
137
|
stop: async () => {
|
|
135
138
|
await gracefulShutdown({ exit: false });
|
|
136
139
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type OutcomeBetConfig, type OutcomeBetConfigMap } from "./hub-make-outcome-bet.js";
|
|
2
|
+
import { CustomGameConfigMap } from "../custom-game-config.js";
|
|
3
|
+
export type { OutcomeBetConfig, OutcomeBetConfigMap };
|
|
4
|
+
declare const HUB_GAME_CONFIG_PLUGIN_BRAND: unique symbol;
|
|
5
|
+
export type HubGameConfigPlugin = {
|
|
6
|
+
readonly [HUB_GAME_CONFIG_PLUGIN_BRAND]: true;
|
|
7
|
+
readonly outcomeBetConfigs: OutcomeBetConfigMap<string>;
|
|
8
|
+
readonly customGameConfigs?: CustomGameConfigMap<string>;
|
|
9
|
+
};
|
|
10
|
+
export declare function isHubGameConfigPlugin(p: unknown): p is HubGameConfigPlugin;
|
|
11
|
+
export declare function expandHubGameConfigPlugin(config: HubGameConfigPlugin): GraphileConfig.Plugin[];
|
|
12
|
+
export declare function HubGameConfigPlugin<OutcomeBetKind extends string, CustomGameKind extends string = never>({ outcomeBetConfigs, customGameConfigs, }: {
|
|
13
|
+
outcomeBetConfigs: OutcomeBetConfigMap<OutcomeBetKind>;
|
|
14
|
+
customGameConfigs?: CustomGameConfigMap<CustomGameKind>;
|
|
15
|
+
}): HubGameConfigPlugin;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MakeOutcomeBetPlugin, } from "./hub-make-outcome-bet.js";
|
|
2
|
+
import { HubRiskLimitsPlugin } from "./hub-risk-limits.js";
|
|
3
|
+
const HUB_GAME_CONFIG_PLUGIN_BRAND = Symbol("HubGameConfigPlugin");
|
|
4
|
+
export function isHubGameConfigPlugin(p) {
|
|
5
|
+
return (typeof p === "object" &&
|
|
6
|
+
p !== null &&
|
|
7
|
+
HUB_GAME_CONFIG_PLUGIN_BRAND in p &&
|
|
8
|
+
p[HUB_GAME_CONFIG_PLUGIN_BRAND] === true);
|
|
9
|
+
}
|
|
10
|
+
export function expandHubGameConfigPlugin(config) {
|
|
11
|
+
return [
|
|
12
|
+
MakeOutcomeBetPlugin({ outcomeBetConfigs: config.outcomeBetConfigs }),
|
|
13
|
+
HubRiskLimitsPlugin({
|
|
14
|
+
outcomeBetConfigs: config.outcomeBetConfigs,
|
|
15
|
+
customGameConfigs: config.customGameConfigs,
|
|
16
|
+
}),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
export function HubGameConfigPlugin({ outcomeBetConfigs, customGameConfigs, }) {
|
|
20
|
+
return {
|
|
21
|
+
[HUB_GAME_CONFIG_PLUGIN_BRAND]: true,
|
|
22
|
+
outcomeBetConfigs,
|
|
23
|
+
customGameConfigs,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -33,10 +33,10 @@ export type OutcomeBetConfig = {
|
|
|
33
33
|
initializeMetadataFromUntrustedUserInput?: (input: Input) => Result<Metadata, string>;
|
|
34
34
|
finalizeMetadata?: (validatedMetadata: Metadata, data: FinalizeMetadataData) => Metadata;
|
|
35
35
|
};
|
|
36
|
-
export type OutcomeBetConfigMap<
|
|
37
|
-
[
|
|
36
|
+
export type OutcomeBetConfigMap<OutcomeBetKind extends string> = {
|
|
37
|
+
[kind in OutcomeBetKind]: OutcomeBetConfig;
|
|
38
38
|
};
|
|
39
|
-
export declare function MakeOutcomeBetPlugin<
|
|
40
|
-
|
|
39
|
+
export declare function MakeOutcomeBetPlugin<OutcomeBetKind extends string>({ outcomeBetConfigs, }: {
|
|
40
|
+
outcomeBetConfigs: OutcomeBetConfigMap<OutcomeBetKind>;
|
|
41
41
|
}): GraphileConfig.Plugin;
|
|
42
42
|
export {};
|
|
@@ -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,
|
|
5
|
+
import { DbHashKind, 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";
|
|
@@ -68,18 +68,19 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
|
|
|
68
68
|
.function()
|
|
69
69
|
.optional(),
|
|
70
70
|
}));
|
|
71
|
-
export function MakeOutcomeBetPlugin({
|
|
72
|
-
BetConfigsSchema.parse(
|
|
73
|
-
const
|
|
71
|
+
export function MakeOutcomeBetPlugin({ outcomeBetConfigs, }) {
|
|
72
|
+
BetConfigsSchema.parse(outcomeBetConfigs);
|
|
73
|
+
const outcomeBetKinds = Object.keys(outcomeBetConfigs);
|
|
74
|
+
const outcomeBetKindEnumValues = outcomeBetKinds.length > 0 ? outcomeBetKinds : ["_NONE"];
|
|
74
75
|
return extendSchema((build) => {
|
|
75
76
|
const outcomeBetTable = build.input.pgRegistry.pgResources.hub_outcome_bet;
|
|
76
77
|
const typeDefs = gql `
|
|
77
|
-
enum
|
|
78
|
-
${
|
|
78
|
+
enum OutcomeBetKind {
|
|
79
|
+
${outcomeBetKindEnumValues.join("\n")}
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
input HubMakeOutcomeBetInput {
|
|
82
|
-
kind:
|
|
83
|
+
kind: OutcomeBetKind!
|
|
83
84
|
wager: Int!
|
|
84
85
|
currency: String!
|
|
85
86
|
outcomes: [HubOutcomeInput!]!
|
|
@@ -106,70 +107,10 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
106
107
|
extend type Mutation {
|
|
107
108
|
hubMakeOutcomeBet(input: HubMakeOutcomeBetInput!): HubMakeOutcomeBetPayload
|
|
108
109
|
}
|
|
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
|
-
}
|
|
125
110
|
`;
|
|
126
111
|
return {
|
|
127
112
|
typeDefs,
|
|
128
113
|
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
|
-
},
|
|
173
114
|
Mutation: {
|
|
174
115
|
plans: {
|
|
175
116
|
hubMakeOutcomeBet: (_, { $input }) => {
|
|
@@ -180,10 +121,10 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
180
121
|
throw new GraphQLError("Unauthorized");
|
|
181
122
|
}
|
|
182
123
|
let input;
|
|
183
|
-
let
|
|
124
|
+
let outcomeBetKind;
|
|
184
125
|
try {
|
|
185
126
|
input = InputSchema.parse(rawInput);
|
|
186
|
-
|
|
127
|
+
outcomeBetKind = BetKindSchema.parse(rawInput.kind);
|
|
187
128
|
}
|
|
188
129
|
catch (e) {
|
|
189
130
|
if (e instanceof z.ZodError) {
|
|
@@ -195,9 +136,9 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
195
136
|
input.currency !== "HOUSE") {
|
|
196
137
|
throw new GraphQLError("Playground users can only bet with HOUSE currency");
|
|
197
138
|
}
|
|
198
|
-
const betConfig =
|
|
139
|
+
const betConfig = outcomeBetConfigs[outcomeBetKind];
|
|
199
140
|
if (!betConfig) {
|
|
200
|
-
throw new GraphQLError(`Invalid bet kind`);
|
|
141
|
+
throw new GraphQLError(`Invalid outcome bet kind`);
|
|
201
142
|
}
|
|
202
143
|
if (!betConfig.allowLossBeyondWager) {
|
|
203
144
|
const minProfit = Math.min(...input.outcomes.map((o) => o.profit));
|
|
@@ -508,17 +449,6 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
508
449
|
},
|
|
509
450
|
},
|
|
510
451
|
},
|
|
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
|
-
},
|
|
522
452
|
HubMakeOutcomeBetPayload: {
|
|
523
453
|
assertStep: ObjectStep,
|
|
524
454
|
plans: {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { OutcomeBetConfigMap } from "./hub-make-outcome-bet.js";
|
|
2
|
+
import { CustomGameConfigMap } from "../custom-game-config.js";
|
|
3
|
+
export declare function HubRiskLimitsPlugin<OutcomeBetKind extends string, CustomGameKind extends string = never>({ outcomeBetConfigs, customGameConfigs, }: {
|
|
4
|
+
outcomeBetConfigs: OutcomeBetConfigMap<OutcomeBetKind>;
|
|
5
|
+
customGameConfigs?: CustomGameConfigMap<CustomGameKind>;
|
|
6
|
+
}): GraphileConfig.Plugin;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { context, sideEffect, access } from "postgraphile/grafast";
|
|
2
|
+
import { gql, extendSchema } from "postgraphile/utils";
|
|
3
|
+
import { GraphQLError } from "graphql";
|
|
4
|
+
import { dbGetHouseBankrolls } from "../db/index.js";
|
|
5
|
+
export function HubRiskLimitsPlugin({ outcomeBetConfigs, customGameConfigs, }) {
|
|
6
|
+
const outcomeBetKinds = Object.keys(outcomeBetConfigs);
|
|
7
|
+
const customGameKinds = customGameConfigs
|
|
8
|
+
? Object.keys(customGameConfigs)
|
|
9
|
+
: [];
|
|
10
|
+
if (customGameConfigs) {
|
|
11
|
+
const outcomeBetKindSet = new Set(outcomeBetKinds);
|
|
12
|
+
for (const kind of customGameKinds) {
|
|
13
|
+
if (outcomeBetKindSet.has(kind)) {
|
|
14
|
+
throw new Error(`Key collision: "${kind}" exists in both outcomeBetConfigs and customGameConfigs`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const anyGameKinds = [...outcomeBetKinds, ...customGameKinds];
|
|
19
|
+
const anyGameKindEnumValues = anyGameKinds.length > 0 ? anyGameKinds : ["_NONE"];
|
|
20
|
+
const mergedConfigs = {};
|
|
21
|
+
for (const kind of outcomeBetKinds) {
|
|
22
|
+
mergedConfigs[kind] = outcomeBetConfigs[kind];
|
|
23
|
+
}
|
|
24
|
+
if (customGameConfigs) {
|
|
25
|
+
for (const kind of customGameKinds) {
|
|
26
|
+
mergedConfigs[kind] = customGameConfigs[kind];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return extendSchema((build) => {
|
|
30
|
+
const typeDefs = gql `
|
|
31
|
+
enum AnyGameKind {
|
|
32
|
+
${anyGameKindEnumValues.join("\n")}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
${customGameKinds.length > 0
|
|
36
|
+
? `enum CustomGameKind {
|
|
37
|
+
${customGameKinds.join("\n")}
|
|
38
|
+
}`
|
|
39
|
+
: ""}
|
|
40
|
+
|
|
41
|
+
type HubRiskLimit {
|
|
42
|
+
maxPayout: Float!
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type HubBulkRiskLimit {
|
|
46
|
+
gameKind: AnyGameKind!
|
|
47
|
+
currency: String!
|
|
48
|
+
maxPayout: Float!
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
extend type Query {
|
|
52
|
+
hubRiskLimits(gameKinds: [AnyGameKind!]!): [HubBulkRiskLimit!]!
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
return {
|
|
56
|
+
typeDefs,
|
|
57
|
+
objects: {
|
|
58
|
+
Query: {
|
|
59
|
+
plans: {
|
|
60
|
+
hubRiskLimits: (_, { $gameKinds }) => {
|
|
61
|
+
const $identity = context().get("identity");
|
|
62
|
+
const $superuserPool = context().get("superuserPool");
|
|
63
|
+
const $result = sideEffect([$identity, $superuserPool, $gameKinds], async ([identity, superuserPool, inputGameKinds]) => {
|
|
64
|
+
if (identity?.kind !== "user") {
|
|
65
|
+
throw new GraphQLError("Unauthorized");
|
|
66
|
+
}
|
|
67
|
+
if (inputGameKinds.length > 10) {
|
|
68
|
+
throw new GraphQLError("Maximum 10 game kinds allowed");
|
|
69
|
+
}
|
|
70
|
+
if (new Set(inputGameKinds).size !== inputGameKinds.length) {
|
|
71
|
+
throw new GraphQLError("Duplicate game kinds not allowed");
|
|
72
|
+
}
|
|
73
|
+
for (const kind of inputGameKinds) {
|
|
74
|
+
if (kind === "_NONE" || !mergedConfigs[kind]) {
|
|
75
|
+
throw new GraphQLError(`Invalid game kind: ${kind}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (anyGameKinds.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const dbHouseBankrolls = await dbGetHouseBankrolls(superuserPool, {
|
|
82
|
+
casinoId: identity.session.casino_id,
|
|
83
|
+
});
|
|
84
|
+
const limits = inputGameKinds.flatMap((gameKind) => {
|
|
85
|
+
const config = mergedConfigs[gameKind];
|
|
86
|
+
return dbHouseBankrolls.map((bankroll) => {
|
|
87
|
+
const riskLimits = config.riskPolicy({
|
|
88
|
+
type: "get-limits",
|
|
89
|
+
currency: bankroll.currency_key,
|
|
90
|
+
bankroll: bankroll.amount,
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
gameKind,
|
|
94
|
+
currency: bankroll.currency_key,
|
|
95
|
+
maxPayout: riskLimits.maxPayout,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
return limits;
|
|
100
|
+
});
|
|
101
|
+
return $result;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
HubRiskLimit: {
|
|
106
|
+
plans: {
|
|
107
|
+
maxPayout($data) {
|
|
108
|
+
return access($data, "maxPayout");
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
HubBulkRiskLimit: {
|
|
113
|
+
plans: {
|
|
114
|
+
gameKind($data) {
|
|
115
|
+
return access($data, "gameKind");
|
|
116
|
+
},
|
|
117
|
+
currency($data) {
|
|
118
|
+
return access($data, "currency");
|
|
119
|
+
},
|
|
120
|
+
maxPayout($data) {
|
|
121
|
+
return access($data, "maxPayout");
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}, "HubRiskLimitsPlugin");
|
|
128
|
+
}
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { GraphQLClient } from "graphql-request";
|
|
3
|
-
export declare function processWithdrawalRequests({ casinoId, graphqlClient, pool, }: {
|
|
4
|
-
casinoId: string;
|
|
5
|
-
graphqlClient: GraphQLClient;
|
|
6
|
-
pool: pg.Pool;
|
|
7
|
-
}): Promise<void>;
|
|
1
|
+
export {};
|
|
@@ -1,146 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { TransferStatusKind } from "./__generated__/graphql.js";
|
|
3
|
-
import { withPgPoolTransaction } from "./db/index.js";
|
|
4
|
-
import { maybeOneRow, exactlyOneRow } from "./db/util.js";
|
|
5
|
-
import { START_PENDING_EXPERIENCE_TRANSFER } from "./graphql-queries.js";
|
|
6
|
-
import { logger } from "./logger.js";
|
|
7
|
-
async function processWithdrawalRequest({ requestId, graphqlClient, pool, }) {
|
|
8
|
-
return withPgPoolTransaction(pool, async (pgClient) => {
|
|
9
|
-
const dbWithdrawalRequest = await pgClient
|
|
10
|
-
.query({
|
|
11
|
-
text: `
|
|
12
|
-
select *
|
|
13
|
-
from hub.withdrawal_request
|
|
14
|
-
where id = $1
|
|
15
|
-
|
|
16
|
-
FOR UPDATE
|
|
17
|
-
`,
|
|
18
|
-
values: [requestId],
|
|
19
|
-
})
|
|
20
|
-
.then(maybeOneRow);
|
|
21
|
-
if (!dbWithdrawalRequest) {
|
|
22
|
-
throw new Error("Withdrawal request not found");
|
|
23
|
-
}
|
|
24
|
-
if (dbWithdrawalRequest.mp_transfer_id) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const dbUser = await pgClient
|
|
28
|
-
.query({
|
|
29
|
-
text: `select * from hub.user where id = $1`,
|
|
30
|
-
values: [dbWithdrawalRequest.user_id],
|
|
31
|
-
})
|
|
32
|
-
.then(exactlyOneRow);
|
|
33
|
-
const dbExperience = await pgClient
|
|
34
|
-
.query({
|
|
35
|
-
text: `select * from hub.experience where id = $1`,
|
|
36
|
-
values: [dbWithdrawalRequest.experience_id],
|
|
37
|
-
})
|
|
38
|
-
.then(exactlyOneRow);
|
|
39
|
-
const mpResult = await graphqlClient
|
|
40
|
-
.request({
|
|
41
|
-
document: START_PENDING_EXPERIENCE_TRANSFER,
|
|
42
|
-
variables: {
|
|
43
|
-
mpUserId: dbUser.mp_user_id,
|
|
44
|
-
mpExperienceId: dbExperience.mp_experience_id,
|
|
45
|
-
amount: dbWithdrawalRequest.amount,
|
|
46
|
-
currency: dbWithdrawalRequest.currency_key,
|
|
47
|
-
metadata: JSON.stringify({
|
|
48
|
-
id: `withdrawal_request:${dbWithdrawalRequest.id}`,
|
|
49
|
-
}),
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
.then((res) => res.transferCurrencyExperienceToUser?.result);
|
|
53
|
-
if (!mpResult?.__typename) {
|
|
54
|
-
throw new Error(`Failed to start MP transfer: ${mpResult}`);
|
|
55
|
-
}
|
|
56
|
-
if (mpResult.__typename === "TakeRequestAlreadyTerminal") {
|
|
57
|
-
if (mpResult.takeRequest.experienceTransfer) {
|
|
58
|
-
const mpTransferId = mpResult.takeRequest.experienceTransfer.id;
|
|
59
|
-
await pgClient.query({
|
|
60
|
-
text: `
|
|
61
|
-
update hub.withdrawal_request
|
|
62
|
-
set mp_transfer_id = $2
|
|
63
|
-
where id = $1
|
|
64
|
-
`,
|
|
65
|
-
values: [dbWithdrawalRequest.id, mpTransferId],
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (mpResult?.__typename !== "TransferSuccess" &&
|
|
70
|
-
mpResult?.__typename !== "TransferMetadataIdExists") {
|
|
71
|
-
throw new Error(`Failed to start MP transfer: ${mpResult}`);
|
|
72
|
-
}
|
|
73
|
-
const mpTransferId = mpResult.transfer.id;
|
|
74
|
-
const mpStatus = mpResult.transfer.status;
|
|
75
|
-
await pgClient
|
|
76
|
-
.query({
|
|
77
|
-
text: `
|
|
78
|
-
update hub.withdrawal_request
|
|
79
|
-
set mp_transfer_id = $2
|
|
80
|
-
where id = $1
|
|
81
|
-
`,
|
|
82
|
-
values: [dbWithdrawalRequest.id, mpTransferId],
|
|
83
|
-
})
|
|
84
|
-
.then((res) => {
|
|
85
|
-
assert(res.rowCount === 1, `Expected to update a hub.withdrawal_request.mp_transfer_id`);
|
|
86
|
-
});
|
|
87
|
-
if (mpStatus === TransferStatusKind.Pending ||
|
|
88
|
-
mpStatus === TransferStatusKind.Completed) {
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
logger.warn(`Expected status to be PENDING or COMPLETED, but got ${mpStatus}`);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
await pgClient.query({
|
|
95
|
-
text: `
|
|
96
|
-
insert into hub.withdrawal(
|
|
97
|
-
id,
|
|
98
|
-
casino_id,
|
|
99
|
-
mp_transfer_id,
|
|
100
|
-
user_id,
|
|
101
|
-
experience_id,
|
|
102
|
-
amount,
|
|
103
|
-
currency_key,
|
|
104
|
-
status,
|
|
105
|
-
withdrawal_request_id
|
|
106
|
-
)
|
|
107
|
-
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
108
|
-
`,
|
|
109
|
-
values: [
|
|
110
|
-
dbWithdrawalRequest.id,
|
|
111
|
-
dbWithdrawalRequest.casino_id,
|
|
112
|
-
mpTransferId,
|
|
113
|
-
dbWithdrawalRequest.user_id,
|
|
114
|
-
dbWithdrawalRequest.experience_id,
|
|
115
|
-
dbWithdrawalRequest.amount,
|
|
116
|
-
dbWithdrawalRequest.currency_key,
|
|
117
|
-
mpResult.transfer.status,
|
|
118
|
-
dbWithdrawalRequest.id,
|
|
119
|
-
],
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
export async function processWithdrawalRequests({ casinoId, graphqlClient, pool, }) {
|
|
124
|
-
const pendingRequests = await pool.query({
|
|
125
|
-
text: `
|
|
126
|
-
SELECT wr.*
|
|
127
|
-
FROM hub.withdrawal_request wr
|
|
128
|
-
WHERE wr.casino_id = $1
|
|
129
|
-
AND wr.mp_transfer_id IS NULL
|
|
130
|
-
LIMIT 10
|
|
131
|
-
`,
|
|
132
|
-
values: [casinoId],
|
|
133
|
-
});
|
|
134
|
-
for (const request of pendingRequests.rows) {
|
|
135
|
-
try {
|
|
136
|
-
await processWithdrawalRequest({
|
|
137
|
-
requestId: request.id,
|
|
138
|
-
graphqlClient,
|
|
139
|
-
pool,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
logger.error(error, `Failed to process withdrawal request ${request.id}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
1
|
+
export {};
|
package/dist/src/risk-policy.js
CHANGED
|
@@ -2,7 +2,6 @@ import { formatCurrency } from "./format-currency.js";
|
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
3
|
const RiskLimitsSchema = z
|
|
4
4
|
.object({
|
|
5
|
-
maxWager: z.number().positive().optional(),
|
|
6
5
|
maxPayout: z.number().positive(),
|
|
7
6
|
})
|
|
8
7
|
.strict();
|
|
@@ -21,20 +20,6 @@ export function validateRisk(options) {
|
|
|
21
20
|
error: { message, riskLimits: options.riskLimits },
|
|
22
21
|
};
|
|
23
22
|
}
|
|
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
|
-
})})`;
|
|
33
|
-
return {
|
|
34
|
-
ok: false,
|
|
35
|
-
error: { message, riskLimits: options.riskLimits },
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
23
|
if (maxPotentialPayout > options.riskLimits.maxPayout) {
|
|
39
24
|
const message = `Payout (${formatCurrency(maxPotentialPayout, {
|
|
40
25
|
displayUnitName: options.displayUnitName,
|
|
@@ -27,8 +27,10 @@ export type OurPgSettings = {
|
|
|
27
27
|
};
|
|
28
28
|
export declare const requiredPlugins: readonly GraphileConfig.Plugin[];
|
|
29
29
|
export declare const defaultPlugins: readonly GraphileConfig.Plugin[];
|
|
30
|
+
import { HubGameConfigPlugin } from "../plugins/hub-game-config-plugin.js";
|
|
31
|
+
export type HubPlugin = GraphileConfig.Plugin | HubGameConfigPlugin;
|
|
30
32
|
export declare function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, enableChat, enablePlayground, }: {
|
|
31
|
-
plugins: readonly
|
|
33
|
+
plugins: readonly HubPlugin[];
|
|
32
34
|
exportSchemaSDLPath?: string;
|
|
33
35
|
extraPgSchemas: string[];
|
|
34
36
|
abortSignal: AbortSignal;
|
|
@@ -56,6 +56,7 @@ export const defaultPlugins = [
|
|
|
56
56
|
HubPreimageHashFieldPlugin,
|
|
57
57
|
customPgOmitArchivedPlugin("deleted"),
|
|
58
58
|
];
|
|
59
|
+
import { HubGameConfigPlugin, isHubGameConfigPlugin, expandHubGameConfigPlugin, } from "../plugins/hub-game-config-plugin.js";
|
|
59
60
|
export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, enableChat, enablePlayground, }) {
|
|
60
61
|
if (exportSchemaSDLPath) {
|
|
61
62
|
if (!exportSchemaSDLPath.startsWith("/")) {
|
|
@@ -66,9 +67,20 @@ export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abo
|
|
|
66
67
|
}
|
|
67
68
|
logger.info(`Will save generated graphql schema to ${exportSchemaSDLPath}`);
|
|
68
69
|
}
|
|
69
|
-
const
|
|
70
|
+
const pluginsWithDefault = plugins.some(isHubGameConfigPlugin)
|
|
71
|
+
? plugins
|
|
72
|
+
: [...plugins, HubGameConfigPlugin({ outcomeBetConfigs: {} })];
|
|
73
|
+
const mutablePlugins = [];
|
|
74
|
+
for (const p of pluginsWithDefault) {
|
|
75
|
+
if (isHubGameConfigPlugin(p)) {
|
|
76
|
+
mutablePlugins.push(...expandHubGameConfigPlugin(p));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
mutablePlugins.push(p);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
70
82
|
for (const requiredPlugin of requiredPlugins) {
|
|
71
|
-
if (!
|
|
83
|
+
if (!mutablePlugins.some((plugin) => plugin === requiredPlugin)) {
|
|
72
84
|
logger.warn(`Adding required plugin "${requiredPlugin.name}"`);
|
|
73
85
|
mutablePlugins.unshift(requiredPlugin);
|
|
74
86
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { ServerOptions } from "../index.js";
|
|
2
2
|
import { ServerContext } from "../context.js";
|
|
3
3
|
export type HubServer = {
|
|
4
|
-
listen: () => Promise<
|
|
4
|
+
listen: () => Promise<{
|
|
5
|
+
port: number;
|
|
6
|
+
}>;
|
|
5
7
|
shutdown: () => Promise<void>;
|
|
6
8
|
};
|
|
7
|
-
export declare function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, enableChat, enablePlayground, }: Pick<ServerOptions, "plugins" | "exportSchemaSDLPath" | "extraPgSchemas" | "configureApp"> & {
|
|
9
|
+
export declare function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, enableChat, enablePlayground, port, }: Pick<ServerOptions, "plugins" | "exportSchemaSDLPath" | "extraPgSchemas" | "configureApp"> & {
|
|
8
10
|
abortSignal: AbortSignal;
|
|
9
11
|
context: ServerContext;
|
|
10
12
|
enableChat: boolean;
|
|
11
13
|
enablePlayground: boolean;
|
|
14
|
+
port: number;
|
|
12
15
|
}): HubServer;
|
package/dist/src/server/index.js
CHANGED
|
@@ -3,7 +3,6 @@ import { grafserv } from "postgraphile/grafserv/express/v4";
|
|
|
3
3
|
import postgraphile from "postgraphile";
|
|
4
4
|
import { createPreset, defaultPlugins } from "./graphile.config.js";
|
|
5
5
|
import express from "express";
|
|
6
|
-
import * as config from "../config.js";
|
|
7
6
|
import { logger } from "../logger.js";
|
|
8
7
|
import cors from "./middleware/cors.js";
|
|
9
8
|
import authentication from "./middleware/authentication.js";
|
|
@@ -45,7 +44,7 @@ function createExpressServer(context) {
|
|
|
45
44
|
});
|
|
46
45
|
return app;
|
|
47
46
|
}
|
|
48
|
-
export function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, enableChat, enablePlayground, }) {
|
|
47
|
+
export function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, enableChat, enablePlayground, port, }) {
|
|
49
48
|
const expressServer = createExpressServer(context);
|
|
50
49
|
const preset = createPreset({
|
|
51
50
|
plugins: plugins ?? defaultPlugins,
|
|
@@ -72,7 +71,11 @@ export function createHubServer({ configureApp, plugins, exportSchemaSDLPath, ex
|
|
|
72
71
|
return {
|
|
73
72
|
listen: () => {
|
|
74
73
|
return new Promise((resolve) => {
|
|
75
|
-
nodeServer.listen(
|
|
74
|
+
nodeServer.listen(port, () => {
|
|
75
|
+
const addr = nodeServer.address();
|
|
76
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
77
|
+
resolve({ port: actualPort });
|
|
78
|
+
});
|
|
76
79
|
});
|
|
77
80
|
},
|
|
78
81
|
shutdown: () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moneypot/hub",
|
|
3
|
-
"version": "1.18.0-dev.
|
|
3
|
+
"version": "1.18.0-dev.4",
|
|
4
4
|
"author": "moneypot.com",
|
|
5
5
|
"homepage": "https://moneypot.com/hub",
|
|
6
6
|
"keywords": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@graphile-contrib/pg-omit-archived": "^4.0.0-beta.4",
|
|
53
53
|
"@moneypot/hash-herald": "^1.0.0",
|
|
54
|
-
"@moneypot/pg-upgrade-schema": "^2.0
|
|
54
|
+
"@moneypot/pg-upgrade-schema": "^2.1.0",
|
|
55
55
|
"@noble/curves": "^1.5.0",
|
|
56
56
|
"dotenv": "^16.4.5",
|
|
57
57
|
"express": "^5.0.1",
|