@moneypot/hub 1.17.1 → 1.18.0-dev.2
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 +3 -1
- package/dist/src/db/public.d.ts +4 -1
- package/dist/src/db/public.js +20 -11
- package/dist/src/hash-chain/plugins/hub-bad-hash-chain-error.js +1 -1
- package/dist/src/plugins/chat/hub-chat-create-user-message.js +1 -1
- package/dist/src/plugins/chat/hub-chat-subscription.js +1 -1
- package/dist/src/plugins/hub-make-outcome-bet.js +107 -10
- package/dist/src/process-transfers/process-transfer.js +1 -2
- package/dist/src/risk-policy.d.ts +12 -3
- package/dist/src/risk-policy.js +28 -37
- package/package.json +3 -1
package/dist/src/db/index.js
CHANGED
|
@@ -82,7 +82,9 @@ export class DatabaseNotifier extends stream.EventEmitter {
|
|
|
82
82
|
}
|
|
83
83
|
export async function getTransferCursor(pgClient, { casinoId, }) {
|
|
84
84
|
const row = await pgClient
|
|
85
|
-
.query("select cursor from hub_hidden.transfer_cursor where casino_id = $1", [
|
|
85
|
+
.query("select cursor from hub_hidden.transfer_cursor where casino_id = $1", [
|
|
86
|
+
casinoId,
|
|
87
|
+
])
|
|
86
88
|
.then(maybeOneRow);
|
|
87
89
|
return row?.cursor;
|
|
88
90
|
}
|
package/dist/src/db/public.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { DbBalance, DbBankroll, DbCurrency, DbSession } from "./types.js";
|
|
1
|
+
import { DbBalance, DbBankroll, DbCasino, DbCurrency, DbSession } from "./types.js";
|
|
2
2
|
import { PgClientInTransaction, QueryExecutor } from "./index.js";
|
|
3
3
|
export declare function dbGetActiveSessionById(pgClient: QueryExecutor, sessionId: string): Promise<DbSession | undefined>;
|
|
4
4
|
export declare function dbGetCasinoCurrencyByKey(pgClient: QueryExecutor, { currencyKey, casinoId }: {
|
|
5
5
|
currencyKey: string;
|
|
6
6
|
casinoId: string;
|
|
7
7
|
}): Promise<DbCurrency | undefined>;
|
|
8
|
+
export declare function dbGetHouseBankrolls(pgClient: QueryExecutor, { casinoId }: {
|
|
9
|
+
casinoId: DbCasino["id"];
|
|
10
|
+
}): Promise<Pick<DbBankroll, "id" | "currency_key" | "amount">[]>;
|
|
8
11
|
export declare function dbLockPlayerBalance(pgClient: PgClientInTransaction, { userId, casinoId, experienceId, currencyKey, }: {
|
|
9
12
|
userId: string;
|
|
10
13
|
casinoId: string;
|
package/dist/src/db/public.js
CHANGED
|
@@ -13,20 +13,29 @@ export async function dbGetCasinoCurrencyByKey(pgClient, { currencyKey, casinoId
|
|
|
13
13
|
.query(`
|
|
14
14
|
SELECT *
|
|
15
15
|
FROM hub.currency
|
|
16
|
-
WHERE key = $1
|
|
16
|
+
WHERE key = $1
|
|
17
17
|
AND casino_id = $2
|
|
18
18
|
`, [currencyKey, casinoId])
|
|
19
19
|
.then(maybeOneRow);
|
|
20
20
|
}
|
|
21
|
+
export async function dbGetHouseBankrolls(pgClient, { casinoId }) {
|
|
22
|
+
return pgClient
|
|
23
|
+
.query(`
|
|
24
|
+
SELECT id, currency_key, amount
|
|
25
|
+
FROM hub.bankroll
|
|
26
|
+
WHERE casino_id = $1
|
|
27
|
+
`, [casinoId])
|
|
28
|
+
.then((res) => res.rows);
|
|
29
|
+
}
|
|
21
30
|
export async function dbLockPlayerBalance(pgClient, { userId, casinoId, experienceId, currencyKey, }) {
|
|
22
31
|
return pgClient
|
|
23
32
|
.query(`
|
|
24
|
-
SELECT *
|
|
25
|
-
FROM hub.balance
|
|
26
|
-
WHERE user_id = $1
|
|
27
|
-
AND casino_id = $2
|
|
28
|
-
AND experience_id = $3
|
|
29
|
-
AND currency_key = $4
|
|
33
|
+
SELECT *
|
|
34
|
+
FROM hub.balance
|
|
35
|
+
WHERE user_id = $1
|
|
36
|
+
AND casino_id = $2
|
|
37
|
+
AND experience_id = $3
|
|
38
|
+
AND currency_key = $4
|
|
30
39
|
FOR UPDATE
|
|
31
40
|
`, [userId, casinoId, experienceId, currencyKey])
|
|
32
41
|
.then(maybeOneRow)
|
|
@@ -35,10 +44,10 @@ export async function dbLockPlayerBalance(pgClient, { userId, casinoId, experien
|
|
|
35
44
|
export async function dbLockHouseBankroll(pgClient, { casinoId, currencyKey }) {
|
|
36
45
|
return pgClient
|
|
37
46
|
.query(`
|
|
38
|
-
SELECT *
|
|
39
|
-
FROM hub.bankroll
|
|
40
|
-
WHERE casino_id = $1
|
|
41
|
-
AND currency_key = $2
|
|
47
|
+
SELECT *
|
|
48
|
+
FROM hub.bankroll
|
|
49
|
+
WHERE casino_id = $1
|
|
50
|
+
AND currency_key = $2
|
|
42
51
|
FOR UPDATE
|
|
43
52
|
`, [casinoId, currencyKey])
|
|
44
53
|
.then(maybeOneRow)
|
|
@@ -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, dbLockHouseBankroll, dbLockPlayerBalance, exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../db/index.js";
|
|
5
|
+
import { DbHashKind, dbGetHouseBankrolls, 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";
|
|
@@ -92,7 +92,12 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
92
92
|
bet: HubOutcomeBet!
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
type HubRiskError {
|
|
96
|
+
message: String!
|
|
97
|
+
riskLimits: HubRiskLimit!
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
union HubMakeOutcomeBetResult = HubMakeOutcomeBetSuccess | HubBadHashChainError | HubRiskError
|
|
96
101
|
|
|
97
102
|
type HubMakeOutcomeBetPayload {
|
|
98
103
|
result: HubMakeOutcomeBetResult!
|
|
@@ -101,10 +106,70 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
101
106
|
extend type Mutation {
|
|
102
107
|
hubMakeOutcomeBet(input: HubMakeOutcomeBetInput!): HubMakeOutcomeBetPayload
|
|
103
108
|
}
|
|
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
|
+
}
|
|
104
125
|
`;
|
|
105
126
|
return {
|
|
106
127
|
typeDefs,
|
|
107
128
|
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
|
+
},
|
|
108
173
|
Mutation: {
|
|
109
174
|
plans: {
|
|
110
175
|
hubMakeOutcomeBet: (_, { $input }) => {
|
|
@@ -284,18 +349,28 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
284
349
|
}
|
|
285
350
|
const maxProfitMultiplier = Math.max(...input.outcomes.map((o) => o.profit));
|
|
286
351
|
const maxPotentialPayout = input.wager * maxProfitMultiplier;
|
|
352
|
+
const riskLimits = betConfig.riskPolicy({
|
|
353
|
+
type: "get-limits",
|
|
354
|
+
currency: input.currency,
|
|
355
|
+
bankroll: dbHouseBankroll.amount,
|
|
356
|
+
});
|
|
287
357
|
const riskResult = validateRisk({
|
|
358
|
+
type: "validate-bet",
|
|
288
359
|
currency: input.currency,
|
|
289
360
|
wager: input.wager,
|
|
290
361
|
bankroll: dbHouseBankroll.amount,
|
|
291
362
|
maxPotentialPayout,
|
|
292
|
-
|
|
363
|
+
riskLimits,
|
|
293
364
|
displayUnitName: dbCurrency.display_unit_name,
|
|
294
365
|
displayUnitScale: dbCurrency.display_unit_scale,
|
|
295
366
|
outcomes: input.outcomes,
|
|
296
367
|
});
|
|
297
368
|
if (!riskResult.ok) {
|
|
298
|
-
|
|
369
|
+
return {
|
|
370
|
+
__typename: "HubRiskError",
|
|
371
|
+
message: riskResult.error.message,
|
|
372
|
+
riskLimits: riskResult.error.riskLimits,
|
|
373
|
+
};
|
|
299
374
|
}
|
|
300
375
|
await pgClient.query({
|
|
301
376
|
text: `
|
|
@@ -354,14 +429,14 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
354
429
|
.query({
|
|
355
430
|
text: `
|
|
356
431
|
INSERT INTO hub.outcome_bet (
|
|
357
|
-
user_id,
|
|
358
|
-
casino_id,
|
|
359
|
-
experience_id,
|
|
432
|
+
user_id,
|
|
433
|
+
casino_id,
|
|
434
|
+
experience_id,
|
|
360
435
|
hash_id,
|
|
361
436
|
kind,
|
|
362
|
-
currency_key,
|
|
363
|
-
wager,
|
|
364
|
-
profit,
|
|
437
|
+
currency_key,
|
|
438
|
+
wager,
|
|
439
|
+
profit,
|
|
365
440
|
outcomes,
|
|
366
441
|
outcome_idx,
|
|
367
442
|
metadata
|
|
@@ -422,6 +497,28 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
422
497
|
},
|
|
423
498
|
},
|
|
424
499
|
},
|
|
500
|
+
HubRiskError: {
|
|
501
|
+
assertStep: ObjectStep,
|
|
502
|
+
plans: {
|
|
503
|
+
message($data) {
|
|
504
|
+
return access($data, "message");
|
|
505
|
+
},
|
|
506
|
+
riskLimits($data) {
|
|
507
|
+
return access($data, "riskLimits");
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
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
|
+
},
|
|
425
522
|
HubMakeOutcomeBetPayload: {
|
|
426
523
|
assertStep: ObjectStep,
|
|
427
524
|
plans: {
|
|
@@ -107,8 +107,7 @@ export async function processTransfer({ casinoId, controllerId, transfer, graphq
|
|
|
107
107
|
}
|
|
108
108
|
logger.debug(data, "MP_CLAIM_TRANSFER response");
|
|
109
109
|
if (data.claimTransfer?.result.__typename !== "ClaimTransferSuccess") {
|
|
110
|
-
if (data.claimTransfer?.result.__typename ===
|
|
111
|
-
"InvalidTransferStatus" &&
|
|
110
|
+
if (data.claimTransfer?.result.__typename === "InvalidTransferStatus" &&
|
|
112
111
|
data.claimTransfer?.result.currentStatus ===
|
|
113
112
|
TransferStatusKind.Completed) {
|
|
114
113
|
logger.info(`Transfer ${transfer.id} already claimed (status: COMPLETED), skipping claim but attempting deposit insert`);
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { DbOutcome } from "./db/types.js";
|
|
2
2
|
import { Result } from "./util.js";
|
|
3
3
|
export type RiskPolicyArgs = {
|
|
4
|
+
type: "validate-bet";
|
|
4
5
|
currency: string;
|
|
5
6
|
wager: number;
|
|
6
7
|
bankroll: number;
|
|
7
8
|
maxPotentialPayout: number;
|
|
8
9
|
outcomes: DbOutcome[];
|
|
9
10
|
};
|
|
11
|
+
export type BetLimitPolicyArgs = {
|
|
12
|
+
type: "get-limits";
|
|
13
|
+
currency: string;
|
|
14
|
+
bankroll: number;
|
|
15
|
+
};
|
|
10
16
|
export type RiskLimits = {
|
|
11
17
|
maxWager?: number;
|
|
12
18
|
maxPayout: number;
|
|
13
19
|
};
|
|
14
|
-
export type RiskPolicy = (args: RiskPolicyArgs) => RiskLimits;
|
|
20
|
+
export type RiskPolicy = (args: RiskPolicyArgs | BetLimitPolicyArgs) => RiskLimits;
|
|
15
21
|
export declare function validateRisk(options: RiskPolicyArgs & {
|
|
16
|
-
|
|
22
|
+
riskLimits: RiskLimits;
|
|
17
23
|
} & {
|
|
18
24
|
displayUnitName: string;
|
|
19
25
|
displayUnitScale: number;
|
|
20
|
-
}): Result<void,
|
|
26
|
+
}): Result<void, {
|
|
27
|
+
message: string;
|
|
28
|
+
riskLimits: RiskLimits;
|
|
29
|
+
}>;
|
package/dist/src/risk-policy.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { formatCurrency } from "./format-currency.js";
|
|
2
|
-
import { logger } from "./logger.js";
|
|
3
2
|
import { z } from "zod/v4";
|
|
4
3
|
const RiskLimitsSchema = z
|
|
5
4
|
.object({
|
|
@@ -8,53 +7,45 @@ const RiskLimitsSchema = z
|
|
|
8
7
|
})
|
|
9
8
|
.strict();
|
|
10
9
|
export function validateRisk(options) {
|
|
11
|
-
const { wager, bankroll, maxPotentialPayout
|
|
10
|
+
const { wager, bankroll, maxPotentialPayout } = options;
|
|
12
11
|
if (maxPotentialPayout > bankroll) {
|
|
12
|
+
const message = `House cannot cover potential payout (${formatCurrency(maxPotentialPayout, {
|
|
13
|
+
displayUnitName: options.displayUnitName,
|
|
14
|
+
displayUnitScale: options.displayUnitScale,
|
|
15
|
+
})}). Bankroll: ${formatCurrency(bankroll, {
|
|
16
|
+
displayUnitName: options.displayUnitName,
|
|
17
|
+
displayUnitScale: options.displayUnitScale,
|
|
18
|
+
})}`;
|
|
13
19
|
return {
|
|
14
20
|
ok: false,
|
|
15
|
-
error:
|
|
16
|
-
displayUnitName: options.displayUnitName,
|
|
17
|
-
displayUnitScale: options.displayUnitScale,
|
|
18
|
-
})}). Bankroll: ${formatCurrency(bankroll, {
|
|
19
|
-
displayUnitName: options.displayUnitName,
|
|
20
|
-
displayUnitScale: options.displayUnitScale,
|
|
21
|
-
})}`,
|
|
21
|
+
error: { message, riskLimits: options.riskLimits },
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const limits = limitsResult.data;
|
|
36
|
-
if (limits.maxWager !== undefined && wager > limits.maxWager) {
|
|
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
|
+
})})`;
|
|
37
33
|
return {
|
|
38
34
|
ok: false,
|
|
39
|
-
error:
|
|
40
|
-
displayUnitName: options.displayUnitName,
|
|
41
|
-
displayUnitScale: options.displayUnitScale,
|
|
42
|
-
})}) exceeds limit (${formatCurrency(limits.maxWager, {
|
|
43
|
-
displayUnitName: options.displayUnitName,
|
|
44
|
-
displayUnitScale: options.displayUnitScale,
|
|
45
|
-
})})`,
|
|
35
|
+
error: { message, riskLimits: options.riskLimits },
|
|
46
36
|
};
|
|
47
37
|
}
|
|
48
|
-
if (maxPotentialPayout >
|
|
38
|
+
if (maxPotentialPayout > options.riskLimits.maxPayout) {
|
|
39
|
+
const message = `Payout (${formatCurrency(maxPotentialPayout, {
|
|
40
|
+
displayUnitName: options.displayUnitName,
|
|
41
|
+
displayUnitScale: options.displayUnitScale,
|
|
42
|
+
})}) exceeds limit (${formatCurrency(options.riskLimits.maxPayout, {
|
|
43
|
+
displayUnitName: options.displayUnitName,
|
|
44
|
+
displayUnitScale: options.displayUnitScale,
|
|
45
|
+
})})`;
|
|
49
46
|
return {
|
|
50
47
|
ok: false,
|
|
51
|
-
error:
|
|
52
|
-
displayUnitName: options.displayUnitName,
|
|
53
|
-
displayUnitScale: options.displayUnitScale,
|
|
54
|
-
})}) exceeds limit (${formatCurrency(limits.maxPayout, {
|
|
55
|
-
displayUnitName: options.displayUnitName,
|
|
56
|
-
displayUnitScale: options.displayUnitScale,
|
|
57
|
-
})})`,
|
|
48
|
+
error: { message, riskLimits: options.riskLimits },
|
|
58
49
|
};
|
|
59
50
|
}
|
|
60
51
|
return { ok: true, value: undefined };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moneypot/hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0-dev.2",
|
|
4
4
|
"author": "moneypot.com",
|
|
5
5
|
"homepage": "https://moneypot.com/hub",
|
|
6
6
|
"keywords": [
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"check": "tsc --noEmit && npm run check-dashboard",
|
|
38
38
|
"check-tests": "tsc -p tests/tsconfig.json --noEmit",
|
|
39
39
|
"check-dashboard": "cd ./dashboard && npm run check",
|
|
40
|
+
"format": "prettier --write \"**/*.ts\"",
|
|
40
41
|
"lint": "eslint --fix . && npm run lint-dashboard",
|
|
41
42
|
"lint-dashboard": "cd ./dashboard && npm run lint",
|
|
42
43
|
"prepublishOnly": "npm run build",
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"eslint": "^9.8.0",
|
|
78
79
|
"globals": "^16.0.0",
|
|
79
80
|
"pino-pretty": "^13.0.0",
|
|
81
|
+
"prettier": "^3.6.2",
|
|
80
82
|
"supertest": "^7.0.0",
|
|
81
83
|
"tsx": "^4.20.3",
|
|
82
84
|
"typescript": "^5.4.5",
|