@moneypot/hub 1.16.0 → 1.16.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/cli/add-casino.js +6 -3
- package/dist/dashboard/assets/index-32DtKox_.css +5 -0
- package/dist/dashboard/assets/index-B4xPUnuF.js +412 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/src/__generated__/graphql.d.ts +74 -32
- package/dist/src/__generated__/graphql.js +23 -14
- package/dist/src/db/index.d.ts +2 -4
- package/dist/src/db/index.js +31 -64
- package/dist/src/db/transaction.d.ts +17 -0
- package/dist/src/db/transaction.js +93 -0
- package/dist/src/hash-chain/db-hash-chain.js +0 -2
- package/dist/src/hash-chain/reveal-hash-chain.js +0 -2
- package/dist/src/pg-advisory-lock.js +0 -4
- package/dist/src/plugins/chat/hub-chat-subscription.d.ts +1 -1
- package/dist/src/plugins/hub-authenticate.js +1 -1
- package/dist/src/plugins/hub-make-outcome-bet.d.ts +4 -44
- package/dist/src/plugins/hub-make-outcome-bet.js +1 -1
- package/dist/src/plugins/hub-put-alert.js +2 -2
- package/dist/src/plugins/listen-with-filter.d.ts +4 -3
- package/dist/src/plugins/listen-with-filter.js +65 -8
- package/dist/src/plugins/validate-fields.js +25 -33
- package/dist/src/process-transfers/index.js +1 -1
- package/dist/src/process-transfers/polling-processor.js +0 -6
- package/dist/src/process-transfers/process-transfer.js +15 -0
- package/dist/src/risk-policy.js +1 -1
- package/dist/src/services/jwt-service.js +1 -1
- package/dist/src/take-request/process-take-request.js +19 -19
- package/dist/src/validate-zod.d.ts +3 -0
- package/dist/src/validate-zod.js +19 -0
- package/package.json +3 -4
- package/dist/dashboard/assets/index-D7SlWXgD.js +0 -383
- package/dist/dashboard/assets/index-LZVcTrKv.css +0 -5
- package/dist/src/validate.d.ts +0 -9
- package/dist/src/validate.js +0 -91
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as pg from "pg";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
|
+
const PgClientInTransactionBrand = Symbol("PgClientInTransaction");
|
|
5
|
+
const PG_ERROR_CODE = {
|
|
6
|
+
deadlock: "40P01",
|
|
7
|
+
serializationFailure: "40001",
|
|
8
|
+
};
|
|
9
|
+
const SIDECAR = new WeakMap();
|
|
10
|
+
export function isInTransaction(pgClient) {
|
|
11
|
+
return SIDECAR.has(pgClient);
|
|
12
|
+
}
|
|
13
|
+
export function assertInTransaction(pgClient) {
|
|
14
|
+
if (!isInTransaction(pgClient)) {
|
|
15
|
+
throw new Error("Must be called inside a transaction");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function getIsolationLevel(pgClient) {
|
|
19
|
+
return SIDECAR.get(pgClient) ?? null;
|
|
20
|
+
}
|
|
21
|
+
export const IsolationLevel = {
|
|
22
|
+
READ_COMMITTED: "READ COMMITTED",
|
|
23
|
+
REPEATABLE_READ: "REPEATABLE READ",
|
|
24
|
+
SERIALIZABLE: "SERIALIZABLE",
|
|
25
|
+
};
|
|
26
|
+
export async function withPgPoolTransaction(pool, callbackOrIsolationLevel, callbackOrRetryCount, retryCountOrMaxRetries = 0, maxRetries = 3) {
|
|
27
|
+
let callback;
|
|
28
|
+
let isolationLevel = IsolationLevel.SERIALIZABLE;
|
|
29
|
+
let retryCount = 0;
|
|
30
|
+
if (typeof callbackOrIsolationLevel === "function") {
|
|
31
|
+
callback = callbackOrIsolationLevel;
|
|
32
|
+
if (typeof callbackOrRetryCount === "number") {
|
|
33
|
+
retryCount = callbackOrRetryCount;
|
|
34
|
+
maxRetries = retryCountOrMaxRetries;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
isolationLevel = callbackOrIsolationLevel;
|
|
39
|
+
callback = callbackOrRetryCount;
|
|
40
|
+
retryCount = retryCountOrMaxRetries;
|
|
41
|
+
}
|
|
42
|
+
let pgClient = null;
|
|
43
|
+
try {
|
|
44
|
+
pgClient = await pool.connect();
|
|
45
|
+
if (isolationLevel === IsolationLevel.READ_COMMITTED) {
|
|
46
|
+
await pgClient.query("BEGIN");
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
await pgClient.query(`BEGIN ISOLATION LEVEL ${isolationLevel}`);
|
|
50
|
+
}
|
|
51
|
+
SIDECAR.set(pgClient, isolationLevel);
|
|
52
|
+
assertInTransaction(pgClient);
|
|
53
|
+
const result = await callback(pgClient);
|
|
54
|
+
await pgClient.query("COMMIT");
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (pgClient) {
|
|
59
|
+
try {
|
|
60
|
+
await pgClient.query("ROLLBACK");
|
|
61
|
+
}
|
|
62
|
+
catch (rollbackError) {
|
|
63
|
+
logger.error(error, "Original error");
|
|
64
|
+
logger.error(rollbackError, "Rollback failed");
|
|
65
|
+
SIDECAR.delete(pgClient);
|
|
66
|
+
pgClient.release(true);
|
|
67
|
+
pgClient = null;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
if (retryCount < maxRetries &&
|
|
71
|
+
error instanceof pg.DatabaseError &&
|
|
72
|
+
(error.code === PG_ERROR_CODE.deadlock ||
|
|
73
|
+
error.code === PG_ERROR_CODE.serializationFailure)) {
|
|
74
|
+
const backoffMs = Math.min(100 * Math.pow(2, retryCount), 2000);
|
|
75
|
+
logger.warn(`Retrying transaction in ${Math.floor(backoffMs)}ms (attempt ${retryCount + 2}/${maxRetries + 1}) due to pg error code ${error.code}: ${error.message}`);
|
|
76
|
+
await setTimeout(backoffMs);
|
|
77
|
+
if (isolationLevel === IsolationLevel.READ_COMMITTED) {
|
|
78
|
+
return withPgPoolTransaction(pool, callback, retryCount + 1, maxRetries);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return withPgPoolTransaction(pool, isolationLevel, callback, retryCount + 1, maxRetries);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (pgClient) {
|
|
89
|
+
SIDECAR.delete(pgClient);
|
|
90
|
+
pgClient.release();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -3,7 +3,6 @@ import { exactlyOneRow, maybeOneRow, } from "../db/index.js";
|
|
|
3
3
|
import { assert } from "tsafe";
|
|
4
4
|
import { logger } from "../logger.js";
|
|
5
5
|
export async function dbLockHubHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
|
|
6
|
-
assert(pgClient._inTransaction, "dbLockHubHashChain must be called in a transaction");
|
|
7
6
|
const hashChain = await pgClient
|
|
8
7
|
.query(`
|
|
9
8
|
SELECT *
|
|
@@ -32,7 +31,6 @@ export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, ite
|
|
|
32
31
|
metadata,
|
|
33
32
|
digest: Buffer.from(digest).toString("base64"),
|
|
34
33
|
}, "[dbInsertHubHash] Inserting hash");
|
|
35
|
-
assert(pgClient._inTransaction, "dbInsertHash must be called in a transaction");
|
|
36
34
|
if (kind === DbHashKind.INTERMEDIATE) {
|
|
37
35
|
assert(typeof clientSeed === "string", "clientSeed must be provided for INTERMEDIATE hashes");
|
|
38
36
|
}
|
|
@@ -2,9 +2,7 @@ import { logger } from "../logger.js";
|
|
|
2
2
|
import { getPreimageHash } from "./get-hash.js";
|
|
3
3
|
import { DbHashKind } from "../db/types.js";
|
|
4
4
|
import { dbInsertHubHash } from "../db/index.js";
|
|
5
|
-
import { assert } from "tsafe";
|
|
6
5
|
export async function dbRevealHashChain(pgClient, { hashChainId, }) {
|
|
7
|
-
assert(pgClient._inTransaction, "dbRevealHashChain must be called in a transaction");
|
|
8
6
|
logger.debug({ hashChainId }, "Revealing hash chain");
|
|
9
7
|
const preimageHashResult = await getPreimageHash({
|
|
10
8
|
hashChainId,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { assert } from "tsafe";
|
|
2
1
|
var LockNamespace;
|
|
3
2
|
(function (LockNamespace) {
|
|
4
3
|
LockNamespace[LockNamespace["MP_TAKE_REQUEST"] = 1] = "MP_TAKE_REQUEST";
|
|
@@ -26,15 +25,12 @@ async function acquireAdvisoryLock(pgClient, namespace, hash) {
|
|
|
26
25
|
}
|
|
27
26
|
export const PgAdvisoryLock = {
|
|
28
27
|
forMpTakeRequestProcessing: async (pgClient, params) => {
|
|
29
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
30
28
|
await acquireAdvisoryLock(pgClient, LockNamespace.MP_TAKE_REQUEST, createHashKey(params.mpTakeRequestId, params.casinoId));
|
|
31
29
|
},
|
|
32
30
|
forNewHashChain: async (pgClient, params) => {
|
|
33
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
34
31
|
await acquireAdvisoryLock(pgClient, LockNamespace.NEW_HASH_CHAIN, createHashKey(params.userId, params.experienceId, params.casinoId));
|
|
35
32
|
},
|
|
36
33
|
forChatPlayerAction: async (pgClient, params) => {
|
|
37
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
38
34
|
await acquireAdvisoryLock(pgClient, LockNamespace.CHAT_PLAYER_ACTION, createHashKey(params.userId, params.experienceId, params.casinoId));
|
|
39
35
|
},
|
|
40
36
|
forChatModManagement: async (pgClient, params) => {
|
|
@@ -8,7 +8,7 @@ declare const ChatPgNotifyPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
8
8
|
}, z.core.$strip>, z.ZodObject<{
|
|
9
9
|
type: z.ZodLiteral<"unmute">;
|
|
10
10
|
user_id: z.ZodString;
|
|
11
|
-
}, z.core.$strip>]>;
|
|
11
|
+
}, z.core.$strip>], "type">;
|
|
12
12
|
export type ChatPgNotifyPayload = z.infer<typeof ChatPgNotifyPayloadSchema>;
|
|
13
13
|
export declare const HubChatSubscriptionPlugin: GraphileConfig.Plugin;
|
|
14
14
|
export {};
|
|
@@ -9,7 +9,7 @@ import { withPgPoolTransaction, } from "../db/index.js";
|
|
|
9
9
|
import { logger } from "../logger.js";
|
|
10
10
|
import * as jwtService from "../services/jwt-service.js";
|
|
11
11
|
import { extractGraphQLErrorInfo, isGraphQLError } from "../GraphQLError.js";
|
|
12
|
-
import { z } from "zod";
|
|
12
|
+
import { z } from "zod/v4";
|
|
13
13
|
import { prettifyError } from "zod/v4";
|
|
14
14
|
const BaseUrlSchema = z
|
|
15
15
|
.string()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
2
|
import { DbOutcome } from "../db/index.js";
|
|
3
3
|
import { Result } from "../util.js";
|
|
4
4
|
import { RiskPolicy } from "../risk-policy.js";
|
|
@@ -7,53 +7,13 @@ declare const InputSchema: z.ZodObject<{
|
|
|
7
7
|
clientSeed: z.ZodString;
|
|
8
8
|
wager: z.ZodNumber;
|
|
9
9
|
currency: z.ZodString;
|
|
10
|
-
outcomes: z.
|
|
10
|
+
outcomes: z.ZodArray<z.ZodObject<{
|
|
11
11
|
weight: z.ZodNumber;
|
|
12
12
|
profit: z.ZodNumber;
|
|
13
|
-
},
|
|
14
|
-
profit: number;
|
|
15
|
-
weight: number;
|
|
16
|
-
}, {
|
|
17
|
-
profit: number;
|
|
18
|
-
weight: number;
|
|
19
|
-
}>, "many">, {
|
|
20
|
-
profit: number;
|
|
21
|
-
weight: number;
|
|
22
|
-
}[], {
|
|
23
|
-
profit: number;
|
|
24
|
-
weight: number;
|
|
25
|
-
}[]>, {
|
|
26
|
-
profit: number;
|
|
27
|
-
weight: number;
|
|
28
|
-
}[], {
|
|
29
|
-
profit: number;
|
|
30
|
-
weight: number;
|
|
31
|
-
}[]>;
|
|
13
|
+
}, z.core.$strict>>;
|
|
32
14
|
hashChainId: z.ZodString;
|
|
33
15
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
34
|
-
},
|
|
35
|
-
currency: string;
|
|
36
|
-
hashChainId: string;
|
|
37
|
-
kind: string;
|
|
38
|
-
clientSeed: string;
|
|
39
|
-
outcomes: {
|
|
40
|
-
profit: number;
|
|
41
|
-
weight: number;
|
|
42
|
-
}[];
|
|
43
|
-
wager: number;
|
|
44
|
-
metadata?: Record<string, unknown> | undefined;
|
|
45
|
-
}, {
|
|
46
|
-
currency: string;
|
|
47
|
-
hashChainId: string;
|
|
48
|
-
kind: string;
|
|
49
|
-
clientSeed: string;
|
|
50
|
-
outcomes: {
|
|
51
|
-
profit: number;
|
|
52
|
-
weight: number;
|
|
53
|
-
}[];
|
|
54
|
-
wager: number;
|
|
55
|
-
metadata?: Record<string, unknown> | undefined;
|
|
56
|
-
}>;
|
|
16
|
+
}, z.core.$strict>;
|
|
57
17
|
type Input = z.infer<typeof InputSchema>;
|
|
58
18
|
type Metadata = NonNullable<Input["metadata"]>;
|
|
59
19
|
type FinalizeMetadataData = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { access, context, object, ObjectStep, sideEffect, } from "postgraphile/grafast";
|
|
2
2
|
import { gql, extendSchema } from "postgraphile/utils";
|
|
3
|
-
import * as z from "zod";
|
|
3
|
+
import * as z from "zod/v4";
|
|
4
4
|
import { GraphQLError } from "graphql";
|
|
5
5
|
import { DbHashKind, dbLockHouseBankroll, dbLockPlayerBalance, exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../db/index.js";
|
|
6
6
|
import { assert } from "tsafe";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { context, lambda } from "postgraphile/grafast";
|
|
2
|
-
import { gql,
|
|
2
|
+
import { gql, extendSchema } from "postgraphile/utils";
|
|
3
3
|
import { jsonParse } from "postgraphile/@dataplan/json";
|
|
4
4
|
import { listenWithFilter } from "./listen-with-filter.js";
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
|
-
export const HubPutAlertPlugin =
|
|
6
|
+
export const HubPutAlertPlugin = extendSchema((_build) => {
|
|
7
7
|
return {
|
|
8
8
|
typeDefs: gql `
|
|
9
9
|
extend type Subscription {
|
|
@@ -6,7 +6,7 @@ export declare class ListenWithFilterStep<TTopics extends {
|
|
|
6
6
|
}, TTopic extends keyof TTopics, TPayloadStep extends Step, TFilterInput> extends Step<TTopics[TTopic][]> {
|
|
7
7
|
itemPlan: (itemPlan: __ItemStep<TTopics[TTopic]>) => TPayloadStep;
|
|
8
8
|
filterFn: (item: unknown, filterInput: TFilterInput) => boolean;
|
|
9
|
-
static
|
|
9
|
+
static $export: {
|
|
10
10
|
moduleName: string;
|
|
11
11
|
exportName: string;
|
|
12
12
|
};
|
|
@@ -14,9 +14,10 @@ export declare class ListenWithFilterStep<TTopics extends {
|
|
|
14
14
|
private pubsubDep;
|
|
15
15
|
private topicDep;
|
|
16
16
|
private filterInputDep;
|
|
17
|
-
|
|
17
|
+
private initialEventDep;
|
|
18
|
+
constructor(pubsubOrPlan: Step<GrafastSubscriber<TTopics> | null> | GrafastSubscriber<TTopics> | null, topicOrPlan: Step<TTopic> | string, itemPlan: ((itemPlan: __ItemStep<TTopics[TTopic]>) => TPayloadStep) | undefined, filterInputPlan: Step<TFilterInput>, filterFn: (item: unknown, filterInput: TFilterInput) => boolean, $initialEvent?: Step<TTopics[TTopic]>);
|
|
18
19
|
execute({ indexMap, values, stream, }: ExecutionDetails<readonly [GrafastSubscriber<TTopics>, TTopic]>): GrafastResultStreamList<TTopics[TTopic]>;
|
|
19
20
|
}
|
|
20
21
|
export declare function listenWithFilter<TTopics extends {
|
|
21
22
|
[topic: string]: any;
|
|
22
|
-
}, TTopic extends keyof TTopics, TPayloadStep extends Step, TFilterInput>(pubsubOrPlan: Step<GrafastSubscriber<TTopics> | null> | GrafastSubscriber<TTopics> | null, topicOrPlan: Step<TTopic> | string, itemPlan: ((itemPlan: __ItemStep<TTopics[TTopic]>) => TPayloadStep) | undefined, filterInputPlan: Step<TFilterInput>, filterFn: (item: unknown, filterInput: TFilterInput) => boolean): ListenWithFilterStep<TTopics, TTopic, TPayloadStep, TFilterInput>;
|
|
23
|
+
}, TTopic extends keyof TTopics, TPayloadStep extends Step, TFilterInput>(pubsubOrPlan: Step<GrafastSubscriber<TTopics> | null> | GrafastSubscriber<TTopics> | null, topicOrPlan: Step<TTopic> | string, itemPlan: ((itemPlan: __ItemStep<TTopics[TTopic]>) => TPayloadStep) | undefined, filterInputPlan: Step<TFilterInput>, filterFn: (item: unknown, filterInput: TFilterInput) => boolean, $initialEvent?: Step<TTopics[TTopic]>): ListenWithFilterStep<TTopics, TTopic, TPayloadStep, TFilterInput>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { constant, isDev,
|
|
1
|
+
import { constant, isDev, isStep, SafeError, } from "postgraphile/grafast";
|
|
2
2
|
import { Step } from "postgraphile/grafast";
|
|
3
3
|
export class ListenWithFilterStep extends Step {
|
|
4
4
|
itemPlan;
|
|
5
5
|
filterFn;
|
|
6
|
-
static
|
|
6
|
+
static $export = {
|
|
7
7
|
moduleName: "grafast",
|
|
8
8
|
exportName: "ListenWithFilterStep",
|
|
9
9
|
};
|
|
@@ -11,17 +11,21 @@ export class ListenWithFilterStep extends Step {
|
|
|
11
11
|
pubsubDep;
|
|
12
12
|
topicDep;
|
|
13
13
|
filterInputDep;
|
|
14
|
-
|
|
14
|
+
initialEventDep = null;
|
|
15
|
+
constructor(pubsubOrPlan, topicOrPlan, itemPlan = ($item) => $item, filterInputPlan, filterFn, $initialEvent) {
|
|
15
16
|
super();
|
|
16
17
|
this.itemPlan = itemPlan;
|
|
17
18
|
this.filterFn = filterFn;
|
|
18
19
|
const $topic = typeof topicOrPlan === "string" ? constant(topicOrPlan) : topicOrPlan;
|
|
19
|
-
const $pubsub =
|
|
20
|
+
const $pubsub = isStep(pubsubOrPlan)
|
|
20
21
|
? pubsubOrPlan
|
|
21
22
|
: constant(pubsubOrPlan, false);
|
|
22
23
|
this.pubsubDep = this.addDependency($pubsub);
|
|
23
24
|
this.topicDep = this.addDependency($topic);
|
|
24
25
|
this.filterInputDep = this.addDependency(filterInputPlan);
|
|
26
|
+
if ($initialEvent) {
|
|
27
|
+
this.initialEventDep = this.addDependency($initialEvent);
|
|
28
|
+
}
|
|
25
29
|
}
|
|
26
30
|
execute({ indexMap, values, stream, }) {
|
|
27
31
|
if (!stream) {
|
|
@@ -30,20 +34,22 @@ export class ListenWithFilterStep extends Step {
|
|
|
30
34
|
const pubsubValue = values[this.pubsubDep];
|
|
31
35
|
const topicValue = values[this.topicDep];
|
|
32
36
|
const filterInputValue = values[this.filterInputDep];
|
|
37
|
+
const initialEventValue = this.initialEventDep !== null ? values[this.initialEventDep] : null;
|
|
33
38
|
return indexMap((i) => {
|
|
34
39
|
const pubsub = pubsubValue.at(i);
|
|
35
40
|
if (!pubsub) {
|
|
36
41
|
throw new SafeError("Subscription not supported", isDev
|
|
37
42
|
? {
|
|
38
|
-
hint:
|
|
43
|
+
hint: `Pubsub did not provide a GrafastSubscriber; perhaps you forgot to add the relevant property to context?`,
|
|
39
44
|
}
|
|
40
45
|
: {});
|
|
41
46
|
}
|
|
42
47
|
const topic = topicValue.at(i);
|
|
43
48
|
const filterInput = filterInputValue.at(i);
|
|
44
49
|
const origStream = pubsub.subscribe(topic);
|
|
50
|
+
const initialEvent = initialEventValue?.at(i);
|
|
45
51
|
const filterFn = this.filterFn;
|
|
46
|
-
|
|
52
|
+
const filteredStream = {
|
|
47
53
|
[Symbol.asyncIterator]: async function* () {
|
|
48
54
|
const iterator = await origStream;
|
|
49
55
|
for await (const item of iterator) {
|
|
@@ -53,9 +59,60 @@ export class ListenWithFilterStep extends Step {
|
|
|
53
59
|
}
|
|
54
60
|
},
|
|
55
61
|
};
|
|
62
|
+
if (initialEvent === undefined) {
|
|
63
|
+
return filteredStream;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (filterFn(initialEvent, filterInput)) {
|
|
67
|
+
return withInitialValue(initialEvent, filteredStream);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
return filteredStream;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
56
73
|
});
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
|
-
export function listenWithFilter(pubsubOrPlan, topicOrPlan, itemPlan = ($item) => $item, filterInputPlan, filterFn) {
|
|
60
|
-
return new ListenWithFilterStep(pubsubOrPlan, topicOrPlan, itemPlan, filterInputPlan, filterFn);
|
|
76
|
+
export function listenWithFilter(pubsubOrPlan, topicOrPlan, itemPlan = ($item) => $item, filterInputPlan, filterFn, $initialEvent) {
|
|
77
|
+
return new ListenWithFilterStep(pubsubOrPlan, topicOrPlan, itemPlan, filterInputPlan, filterFn, $initialEvent);
|
|
61
78
|
}
|
|
79
|
+
const DONE = Object.freeze({ value: undefined, done: true });
|
|
80
|
+
const withInitialValue = (initialVal, source) => ({
|
|
81
|
+
[Symbol.asyncIterator]() {
|
|
82
|
+
const sourceIterator = source[Symbol.asyncIterator]();
|
|
83
|
+
let first = true;
|
|
84
|
+
let done = null;
|
|
85
|
+
return {
|
|
86
|
+
async next() {
|
|
87
|
+
if (done)
|
|
88
|
+
return done;
|
|
89
|
+
if (first) {
|
|
90
|
+
first = false;
|
|
91
|
+
return { value: initialVal, done: false };
|
|
92
|
+
}
|
|
93
|
+
const res = await sourceIterator.next();
|
|
94
|
+
if (res.done)
|
|
95
|
+
done = res;
|
|
96
|
+
return res;
|
|
97
|
+
},
|
|
98
|
+
async return(value) {
|
|
99
|
+
done ??= { value: value, done: true };
|
|
100
|
+
if (typeof sourceIterator.return === "function") {
|
|
101
|
+
try {
|
|
102
|
+
await sourceIterator.return();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return done;
|
|
108
|
+
},
|
|
109
|
+
async throw(err) {
|
|
110
|
+
done ??= DONE;
|
|
111
|
+
if (typeof sourceIterator.throw === "function") {
|
|
112
|
+
return sourceIterator.throw(err);
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
});
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { sideEffect } from "postgraphile/grafast";
|
|
2
2
|
import { GraphQLError } from "postgraphile/graphql";
|
|
3
|
-
import {
|
|
4
|
-
import * as v from "../validate.js";
|
|
5
|
-
import
|
|
3
|
+
import { wrapPlans } from "postgraphile/utils";
|
|
4
|
+
import * as v from "../validate-zod.js";
|
|
5
|
+
import z, { prettifyError } from "zod/v4";
|
|
6
6
|
function validate(value, schema) {
|
|
7
7
|
try {
|
|
8
|
-
schema.
|
|
8
|
+
schema.parse(value);
|
|
9
9
|
}
|
|
10
10
|
catch (e) {
|
|
11
|
-
if (e instanceof
|
|
12
|
-
const
|
|
11
|
+
if (e instanceof z.ZodError) {
|
|
12
|
+
const fieldPath = e.issues.map((issue) => issue.path.join(".")).join(".");
|
|
13
|
+
const errorMessage = `Field "${fieldPath}": ${prettifyError(e)}`;
|
|
13
14
|
throw new GraphQLError(errorMessage);
|
|
14
15
|
}
|
|
15
16
|
else {
|
|
@@ -17,45 +18,36 @@ function validate(value, schema) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
const
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const CasinoPatchSchema = z.strictObject({
|
|
22
|
+
name: z.string().trim().min(1).optional(),
|
|
23
|
+
graphqlUrl: v.graphqlUrl().optional(),
|
|
24
|
+
baseUrl: v.baseUrl().optional(),
|
|
25
|
+
});
|
|
26
|
+
const AddCasinoSchema = z.strictObject({
|
|
27
|
+
name: z.string().trim().min(1),
|
|
24
28
|
baseUrl: v.baseUrl(),
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
graphqlUrl: v.graphqlUrl().required(),
|
|
33
|
-
apiKey: v.uuid().required(),
|
|
34
|
-
})
|
|
35
|
-
.strict()
|
|
36
|
-
.noUnknown();
|
|
37
|
-
const updateBankrollSchema = Yup.object()
|
|
38
|
-
.shape({
|
|
39
|
-
amount: Yup.number().integer().min(0).required(),
|
|
40
|
-
})
|
|
41
|
-
.strict()
|
|
42
|
-
.noUnknown();
|
|
43
|
-
export const ValidateCasinoFieldsPlugin = makeWrapPlansPlugin({
|
|
29
|
+
graphqlUrl: v.graphqlUrl(),
|
|
30
|
+
apiKey: z.uuid(),
|
|
31
|
+
});
|
|
32
|
+
const UpdateBankrollSchema = z.strictObject({
|
|
33
|
+
amount: z.number().int().min(0),
|
|
34
|
+
});
|
|
35
|
+
export const ValidateCasinoFieldsPlugin = wrapPlans(() => ({
|
|
44
36
|
Mutation: {
|
|
45
37
|
updateHubCasinoById: (plan, $source, fieldArgs) => {
|
|
46
38
|
const $patch = fieldArgs.getRaw(["input", "hubCasinoPatch"]);
|
|
47
|
-
sideEffect($patch, (patch) => validate(patch,
|
|
39
|
+
sideEffect($patch, (patch) => validate(patch, CasinoPatchSchema));
|
|
48
40
|
return plan();
|
|
49
41
|
},
|
|
50
42
|
hubAddCasino: (plan, $source, fieldArgs) => {
|
|
51
43
|
const $input = fieldArgs.getRaw(["input"]);
|
|
52
|
-
sideEffect($input, (input) => validate(input,
|
|
44
|
+
sideEffect($input, (input) => validate(input, AddCasinoSchema));
|
|
53
45
|
return plan();
|
|
54
46
|
},
|
|
55
47
|
updateHubBankrollById: (plan, $source, fieldArgs) => {
|
|
56
48
|
const $patch = fieldArgs.getRaw(["input", "hubBankrollPatch"]);
|
|
57
|
-
sideEffect($patch, (patch) => validate(patch,
|
|
49
|
+
sideEffect($patch, (patch) => validate(patch, UpdateBankrollSchema));
|
|
58
50
|
return plan();
|
|
59
51
|
},
|
|
60
52
|
},
|
|
61
|
-
});
|
|
53
|
+
}));
|
|
@@ -6,7 +6,7 @@ import { logger } from "../logger.js";
|
|
|
6
6
|
import * as config from "../config.js";
|
|
7
7
|
import * as db from "../db/index.js";
|
|
8
8
|
import * as pg from "pg";
|
|
9
|
-
import { z } from "zod";
|
|
9
|
+
import { z } from "zod/v4";
|
|
10
10
|
const activeCasinos = new Set();
|
|
11
11
|
export async function startCasinoTransferProcessor({ casinoId, signal, pool, }) {
|
|
12
12
|
if (activeCasinos.has(casinoId)) {
|
|
@@ -4,7 +4,6 @@ import { assert } from "tsafe";
|
|
|
4
4
|
import { isUuid } from "../util.js";
|
|
5
5
|
import { createGraphqlClient } from "../graphql-client.js";
|
|
6
6
|
import { GET_CURRENCIES } from "../graphql-queries.js";
|
|
7
|
-
import { processWithdrawalRequests } from "../process-withdrawal-request.js";
|
|
8
7
|
import { processTakeRequests } from "../take-request/process-take-request.js";
|
|
9
8
|
import * as jwtService from "../services/jwt-service.js";
|
|
10
9
|
import { dbGetCasinoById, dbGetCasinoSecretById } from "../db/internal.js";
|
|
@@ -230,11 +229,6 @@ async function processTransfersUntilEmpty({ afterCursor, graphqlClient, casinoIn
|
|
|
230
229
|
let hasNextPage = true;
|
|
231
230
|
const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
232
231
|
while (hasNextPage && !signal.aborted) {
|
|
233
|
-
await processWithdrawalRequests({
|
|
234
|
-
casinoId: casinoInfo.id,
|
|
235
|
-
graphqlClient,
|
|
236
|
-
pool,
|
|
237
|
-
});
|
|
238
232
|
if (signal.aborted) {
|
|
239
233
|
logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
|
|
240
234
|
break;
|
|
@@ -107,6 +107,21 @@ 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" &&
|
|
112
|
+
data.claimTransfer?.result.currentStatus ===
|
|
113
|
+
TransferStatusKind.Completed) {
|
|
114
|
+
logger.info(`Transfer ${transfer.id} already claimed (status: COMPLETED), skipping claim but attempting deposit insert`);
|
|
115
|
+
await db.insertDeposit(pool, {
|
|
116
|
+
casinoId,
|
|
117
|
+
mpTransferId: transfer.id,
|
|
118
|
+
userId: dbSender.id,
|
|
119
|
+
experienceId: dbExperience.id,
|
|
120
|
+
amount: transfer.amount,
|
|
121
|
+
currency: currency.id,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
110
125
|
throw new Error(`Failed to claim transfer: ${JSON.stringify(data.claimTransfer)}`);
|
|
111
126
|
}
|
|
112
127
|
await db.insertDeposit(pool, {
|
package/dist/src/risk-policy.js
CHANGED
|
@@ -243,7 +243,6 @@ export async function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest,
|
|
|
243
243
|
});
|
|
244
244
|
}
|
|
245
245
|
async function createAndProcessNewTakeRequest({ pgClient, mpTakeRequest, casinoId, graphqlClient, }) {
|
|
246
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
247
246
|
assert(mpTakeRequest.status === MpTakeRequestStatus.Pending);
|
|
248
247
|
const { dbUser, dbExperience, dbCurrency, dbBalance } = await loadRequiredEntities(pgClient, {
|
|
249
248
|
type: "mpId",
|
|
@@ -327,7 +326,6 @@ async function createAndProcessNewTakeRequest({ pgClient, mpTakeRequest, casinoI
|
|
|
327
326
|
});
|
|
328
327
|
}
|
|
329
328
|
async function processExistingTakeRequest({ pgClient, takeRequest, casinoId, graphqlClient, }) {
|
|
330
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
331
329
|
switch (takeRequest.status) {
|
|
332
330
|
case LocalTakeRequestStatus.PENDING:
|
|
333
331
|
logger.info(`[processExistingTakeRequest] Take request ${takeRequest.id} in PENDING state`);
|
|
@@ -371,7 +369,6 @@ async function processExistingTakeRequest({ pgClient, takeRequest, casinoId, gra
|
|
|
371
369
|
return null;
|
|
372
370
|
}
|
|
373
371
|
async function attemptTransfer({ pgClient, takeRequestId, mpTakeRequestId, mpExperienceId, mpUserId, amount, currencyKey, graphqlClient, }) {
|
|
374
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
375
372
|
try {
|
|
376
373
|
const transferResult = await graphqlClient.request(MP_TRANSFER_TAKE_REQUEST, {
|
|
377
374
|
mpTakeRequestId,
|
|
@@ -592,20 +589,18 @@ export async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTrans
|
|
|
592
589
|
logger.warn({ mpTransferId, takeRequestId }, `[completeTransfer] Transfer was already refunded. This should never happen.`);
|
|
593
590
|
break;
|
|
594
591
|
}
|
|
595
|
-
const
|
|
592
|
+
const dbLockedBalance = await pgClient
|
|
596
593
|
.query({
|
|
597
594
|
text: `
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
WHERE user_id = $
|
|
601
|
-
AND experience_id = $
|
|
602
|
-
AND casino_id = $
|
|
603
|
-
AND currency_key = $
|
|
604
|
-
|
|
605
|
-
RETURNING id, amount
|
|
595
|
+
SELECT id, amount
|
|
596
|
+
FROM hub.balance
|
|
597
|
+
WHERE user_id = $1
|
|
598
|
+
AND experience_id = $2
|
|
599
|
+
AND casino_id = $3
|
|
600
|
+
AND currency_key = $4
|
|
601
|
+
FOR UPDATE
|
|
606
602
|
`,
|
|
607
603
|
values: [
|
|
608
|
-
takeRequestData.reserved_amount,
|
|
609
604
|
takeRequestData.user_id,
|
|
610
605
|
takeRequestData.experience_id,
|
|
611
606
|
takeRequestData.casino_id,
|
|
@@ -613,10 +608,18 @@ export async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTrans
|
|
|
613
608
|
],
|
|
614
609
|
})
|
|
615
610
|
.then(exactlyOneRow);
|
|
611
|
+
await pgClient.query({
|
|
612
|
+
text: `
|
|
613
|
+
UPDATE hub.balance
|
|
614
|
+
SET amount = amount + $1
|
|
615
|
+
WHERE id = $2
|
|
616
|
+
`,
|
|
617
|
+
values: [takeRequestData.reserved_amount, dbLockedBalance.id],
|
|
618
|
+
});
|
|
616
619
|
await insertAuditLog(pgClient, "player-balance", {
|
|
617
|
-
balanceId:
|
|
618
|
-
balanceOld:
|
|
619
|
-
balanceNew:
|
|
620
|
+
balanceId: dbLockedBalance.id,
|
|
621
|
+
balanceOld: dbLockedBalance.amount,
|
|
622
|
+
balanceNew: dbLockedBalance.amount + takeRequestData.reserved_amount,
|
|
620
623
|
balanceDelta: takeRequestData.reserved_amount,
|
|
621
624
|
action: "hub:take_request:refund",
|
|
622
625
|
refType: "hub.take_request",
|
|
@@ -740,7 +743,6 @@ async function processStuckRequests({ casinoId, graphqlClient, abortSignal, pool
|
|
|
740
743
|
}
|
|
741
744
|
async function loadRequiredEntities(pgClient, params) {
|
|
742
745
|
const { type, currencyKey, casinoId } = params;
|
|
743
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
744
746
|
const dbCurrency = await pgClient
|
|
745
747
|
.query({
|
|
746
748
|
text: `
|
|
@@ -839,7 +841,6 @@ async function loadRequiredEntities(pgClient, params) {
|
|
|
839
841
|
return { dbCurrency, dbUser, dbExperience, dbBalance };
|
|
840
842
|
}
|
|
841
843
|
async function rejectMpTakeRequest(pgClient, graphqlClient, mpTakeRequestId, takeRequestId) {
|
|
842
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
843
844
|
try {
|
|
844
845
|
logger.info(`[rejectMpTakeRequest] Rejecting take request ${mpTakeRequestId}`);
|
|
845
846
|
const rejectResult = await graphqlClient.request(MP_REJECT_TAKE_REQUEST, {
|
|
@@ -916,7 +917,6 @@ async function rejectMpTakeRequest(pgClient, graphqlClient, mpTakeRequestId, tak
|
|
|
916
917
|
}
|
|
917
918
|
}
|
|
918
919
|
async function updateTakeRequestStatus(pgClient, takeRequestId, status) {
|
|
919
|
-
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
920
920
|
return pgClient.query({
|
|
921
921
|
text: `
|
|
922
922
|
UPDATE hub.take_request
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import z from "zod/v4";
|
|
2
|
+
export declare const baseUrl: () => z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<URL, string>>, z.ZodTransform<string, URL>>;
|
|
3
|
+
export declare const graphqlUrl: () => z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<URL, string>>, z.ZodTransform<string, URL>>;
|