@moneypot/hub 1.3.0-dev.9 → 1.4.0
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 +6 -0
- package/dist/src/db/index.d.ts +1 -0
- package/dist/src/db/index.js +1 -0
- package/dist/src/hash-chain/db-hash-chain.d.ts +2 -2
- package/dist/src/hash-chain/db-hash-chain.js +3 -3
- package/dist/src/pg-versions/007-notify-put.sql +22 -0
- package/dist/src/plugins/hub-make-outcome-bet.d.ts +20 -20
- package/dist/src/plugins/hub-make-outcome-bet.js +34 -27
- package/dist/src/plugins/hub-outcome-input-non-null-fields.d.ts +18 -0
- package/dist/src/plugins/hub-outcome-input-non-null-fields.js +20 -0
- package/dist/src/plugins/hub-put-alert.d.ts +1 -0
- package/dist/src/plugins/hub-put-alert.js +56 -0
- package/dist/src/plugins/hub-user-balance-by-currency.js +15 -40
- package/dist/src/plugins/listen-with-filter.d.ts +22 -0
- package/dist/src/plugins/listen-with-filter.js +61 -0
- package/dist/src/server/graphile.config.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/src/db/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as pg from "pg";
|
|
|
2
2
|
import stream from "node:stream";
|
|
3
3
|
import { DbCasino, DbExperience, DbSession, DbTransferStatusKind, DbUser, DbWithdrawal } from "./types.js";
|
|
4
4
|
import { TransferStatusKind } from "../__generated__/graphql.js";
|
|
5
|
+
export * from "../hash-chain/db-hash-chain.js";
|
|
5
6
|
export * from "./types.js";
|
|
6
7
|
export * from "./public.js";
|
|
7
8
|
export * from "./util.js";
|
package/dist/src/db/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { exactlyOneRow, maybeOneRow } from "./util.js";
|
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
6
|
import { setTimeout } from "node:timers/promises";
|
|
7
7
|
import { assert } from "tsafe";
|
|
8
|
+
export * from "../hash-chain/db-hash-chain.js";
|
|
8
9
|
export * from "./types.js";
|
|
9
10
|
export * from "./public.js";
|
|
10
11
|
export * from "./util.js";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { DbCasino, DbExperience, DbHash, DbHashChain, DbUser } from "../db/types.js";
|
|
2
2
|
import { PgClientInTransaction } from "../db/index.js";
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function dbLockHubHashChain(pgClient: PgClientInTransaction, { userId, experienceId, casinoId, hashChainId, }: {
|
|
4
4
|
userId: DbUser["id"];
|
|
5
5
|
experienceId: DbExperience["id"];
|
|
6
6
|
casinoId: DbCasino["id"];
|
|
7
7
|
hashChainId: DbHashChain["id"];
|
|
8
8
|
}): Promise<DbHashChain | null>;
|
|
9
|
-
export declare function
|
|
9
|
+
export declare function dbInsertHubHash(pgClient: PgClientInTransaction, { hashChainId, kind, digest, iteration, metadata, }: {
|
|
10
10
|
hashChainId: DbHashChain["id"];
|
|
11
11
|
kind: DbHash["kind"];
|
|
12
12
|
digest: DbHash["digest"];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exactlyOneRow, maybeOneRow, } from "../db/index.js";
|
|
2
2
|
import { assert } from "tsafe";
|
|
3
|
-
export async function
|
|
4
|
-
assert(pgClient._inTransaction, "
|
|
3
|
+
export async function dbLockHubHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
|
|
4
|
+
assert(pgClient._inTransaction, "dbLockHubHashChain must be called in a transaction");
|
|
5
5
|
return pgClient
|
|
6
6
|
.query(`
|
|
7
7
|
SELECT *
|
|
@@ -17,7 +17,7 @@ export async function dbLockHashChain(pgClient, { userId, experienceId, casinoId
|
|
|
17
17
|
.then(maybeOneRow)
|
|
18
18
|
.then((row) => row ?? null);
|
|
19
19
|
}
|
|
20
|
-
export async function
|
|
20
|
+
export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, iteration, metadata = {}, }) {
|
|
21
21
|
assert(pgClient._inTransaction, "dbInsertHash must be called in a transaction");
|
|
22
22
|
return pgClient
|
|
23
23
|
.query(`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Let experience know when user successfully puts money into their balance
|
|
2
|
+
-- A put is a successful deposit
|
|
3
|
+
|
|
4
|
+
CREATE OR REPLACE FUNCTION notify_put() RETURNS TRIGGER AS $$
|
|
5
|
+
BEGIN
|
|
6
|
+
PERFORM pg_notify('hub:user:' || NEW.user_id || ':put',
|
|
7
|
+
json_build_object(
|
|
8
|
+
'currency_key', NEW.currency_key,
|
|
9
|
+
'experience_id', NEW.experience_id,
|
|
10
|
+
'mp_transfer_id', NEW.mp_transfer_id
|
|
11
|
+
)::text);
|
|
12
|
+
RETURN NEW;
|
|
13
|
+
END;
|
|
14
|
+
$$ LANGUAGE plpgsql;
|
|
15
|
+
|
|
16
|
+
DROP TRIGGER IF EXISTS notify_put_trigger ON hub.deposit;
|
|
17
|
+
|
|
18
|
+
CREATE TRIGGER notify_put_trigger
|
|
19
|
+
AFTER INSERT ON hub.deposit
|
|
20
|
+
FOR EACH ROW
|
|
21
|
+
EXECUTE FUNCTION notify_put();
|
|
22
|
+
|
|
@@ -4,11 +4,11 @@ declare const OutcomeSchema: z.ZodObject<{
|
|
|
4
4
|
weight: z.ZodNumber;
|
|
5
5
|
profit: z.ZodNumber;
|
|
6
6
|
}, "strict", z.ZodTypeAny, {
|
|
7
|
-
weight: number;
|
|
8
7
|
profit: number;
|
|
9
|
-
}, {
|
|
10
8
|
weight: number;
|
|
9
|
+
}, {
|
|
11
10
|
profit: number;
|
|
11
|
+
weight: number;
|
|
12
12
|
}>;
|
|
13
13
|
declare const InputSchema: z.ZodObject<{
|
|
14
14
|
kind: z.ZodString;
|
|
@@ -18,53 +18,53 @@ declare const InputSchema: z.ZodObject<{
|
|
|
18
18
|
weight: z.ZodNumber;
|
|
19
19
|
profit: z.ZodNumber;
|
|
20
20
|
}, "strict", z.ZodTypeAny, {
|
|
21
|
-
weight: number;
|
|
22
21
|
profit: number;
|
|
23
|
-
}, {
|
|
24
22
|
weight: number;
|
|
23
|
+
}, {
|
|
25
24
|
profit: number;
|
|
26
|
-
}>, "many">, {
|
|
27
25
|
weight: number;
|
|
26
|
+
}>, "many">, {
|
|
28
27
|
profit: number;
|
|
29
|
-
}[], {
|
|
30
28
|
weight: number;
|
|
29
|
+
}[], {
|
|
31
30
|
profit: number;
|
|
32
|
-
}[]>, {
|
|
33
31
|
weight: number;
|
|
32
|
+
}[]>, {
|
|
34
33
|
profit: number;
|
|
35
|
-
}[], {
|
|
36
34
|
weight: number;
|
|
35
|
+
}[], {
|
|
37
36
|
profit: number;
|
|
37
|
+
weight: number;
|
|
38
38
|
}[]>;
|
|
39
39
|
hashChainId: z.ZodString;
|
|
40
|
-
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.
|
|
40
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
41
41
|
}, "strict", z.ZodTypeAny, {
|
|
42
42
|
currency: string;
|
|
43
|
-
kind: string;
|
|
44
43
|
hashChainId: string;
|
|
44
|
+
kind: string;
|
|
45
45
|
wager: number;
|
|
46
46
|
outcomes: {
|
|
47
|
-
weight: number;
|
|
48
47
|
profit: number;
|
|
48
|
+
weight: number;
|
|
49
49
|
}[];
|
|
50
|
-
metadata?: Record<string,
|
|
50
|
+
metadata?: Record<string, unknown> | undefined;
|
|
51
51
|
}, {
|
|
52
52
|
currency: string;
|
|
53
|
-
kind: string;
|
|
54
53
|
hashChainId: string;
|
|
54
|
+
kind: string;
|
|
55
55
|
wager: number;
|
|
56
56
|
outcomes: {
|
|
57
|
-
weight: number;
|
|
58
57
|
profit: number;
|
|
58
|
+
weight: number;
|
|
59
59
|
}[];
|
|
60
|
-
metadata?: Record<string,
|
|
60
|
+
metadata?: Record<string, unknown> | undefined;
|
|
61
61
|
}>;
|
|
62
62
|
type Input = z.infer<typeof InputSchema>;
|
|
63
|
-
type InputWithoutMetadata = Omit<Input, "metadata"> & {
|
|
64
|
-
metadata?: never;
|
|
65
|
-
};
|
|
66
63
|
type Outcome = z.infer<typeof OutcomeSchema>;
|
|
64
|
+
type Metadata = NonNullable<Input["metadata"]>;
|
|
67
65
|
type FinalizeMetadataData = {
|
|
66
|
+
wager: number;
|
|
67
|
+
currencyKey: string;
|
|
68
68
|
clientSeed: string;
|
|
69
69
|
hash: Uint8Array;
|
|
70
70
|
outcomes: Outcome[];
|
|
@@ -73,8 +73,8 @@ type FinalizeMetadataData = {
|
|
|
73
73
|
export type OutcomeBetConfig = {
|
|
74
74
|
houseEdge: number;
|
|
75
75
|
saveOutcomes: boolean;
|
|
76
|
-
|
|
77
|
-
finalizeMetadata?: (
|
|
76
|
+
initializeMetadataFromUntrustedUserInput?: (input: Input) => Result<Metadata, string>;
|
|
77
|
+
finalizeMetadata?: (validatedMetadata: Metadata, data: FinalizeMetadataData) => Metadata;
|
|
78
78
|
};
|
|
79
79
|
export type OutcomeBetConfigMap<BetKind extends string> = {
|
|
80
80
|
[betKind in BetKind]: OutcomeBetConfig;
|
|
@@ -4,8 +4,9 @@ import { z } from "zod";
|
|
|
4
4
|
import { GraphQLError } from "graphql";
|
|
5
5
|
import { DbHashKind, exactlyOneRow, maybeOneRow, superuserPool, withPgPoolTransaction, } from "../db/index.js";
|
|
6
6
|
import { assert } from "tsafe";
|
|
7
|
-
import {
|
|
7
|
+
import { dbInsertHubHash, dbLockHubHashChain, } from "../hash-chain/db-hash-chain.js";
|
|
8
8
|
import { getIntermediateHash, getPreimageHash, } from "../hash-chain/get-hash.js";
|
|
9
|
+
import { createHmac } from "node:crypto";
|
|
9
10
|
const FLOAT_EPSILON = 1e-10;
|
|
10
11
|
function sum(ns) {
|
|
11
12
|
return ns.reduce((a, b) => a + b, 0);
|
|
@@ -38,7 +39,7 @@ const InputSchema = z
|
|
|
38
39
|
.refine((data) => data.some((o) => o.profit < 0), "At least one outcome should have profit < 0")
|
|
39
40
|
.refine((data) => data.some((o) => o.profit > 0), "At least one outcome should have profit > 0"),
|
|
40
41
|
hashChainId: z.string().uuid("Invalid hash chain ID"),
|
|
41
|
-
metadata: z.record(z.string(), z.
|
|
42
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
42
43
|
})
|
|
43
44
|
.strict();
|
|
44
45
|
const BetKindSchema = z
|
|
@@ -52,7 +53,7 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
|
|
|
52
53
|
.gte(0, "House edge must be >= 0")
|
|
53
54
|
.lte(1, "House edge must be <= 1"),
|
|
54
55
|
saveOutcomes: z.boolean(),
|
|
55
|
-
|
|
56
|
+
initializeMetadataFromUntrustedUserInput: z
|
|
56
57
|
.function()
|
|
57
58
|
.optional(),
|
|
58
59
|
finalizeMetadata: z
|
|
@@ -78,11 +79,11 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
78
79
|
metadata: JSON
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
type
|
|
82
|
+
type HubMakeOutcomeBetSuccess {
|
|
82
83
|
bet: HubOutcomeBet!
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
union HubMakeOutcomeBetResult =
|
|
86
|
+
union HubMakeOutcomeBetResult = HubMakeOutcomeBetSuccess | HubBadHashChainError
|
|
86
87
|
|
|
87
88
|
type HubMakeOutcomeBetPayload {
|
|
88
89
|
result: HubMakeOutcomeBetResult!
|
|
@@ -118,17 +119,14 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
118
119
|
if (!betConfig) {
|
|
119
120
|
throw new GraphQLError(`Invalid bet kind`);
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
let validatedMetadata;
|
|
125
|
-
if (betConfig.validateUntrustedMetadata) {
|
|
126
|
-
const result = betConfig.validateUntrustedMetadata(input);
|
|
122
|
+
let initializedMetadata;
|
|
123
|
+
if (betConfig.initializeMetadataFromUntrustedUserInput) {
|
|
124
|
+
const result = betConfig.initializeMetadataFromUntrustedUserInput(input);
|
|
127
125
|
if (result.ok) {
|
|
128
|
-
|
|
126
|
+
initializedMetadata = result.value;
|
|
129
127
|
}
|
|
130
128
|
else {
|
|
131
|
-
throw new GraphQLError(`Invalid
|
|
129
|
+
throw new GraphQLError(`Invalid input: ${result.error}`);
|
|
132
130
|
}
|
|
133
131
|
}
|
|
134
132
|
const houseEV = calculateHouseEV(input.outcomes);
|
|
@@ -181,7 +179,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
181
179
|
if (maxPotentialPayout > maxAllowablePayout) {
|
|
182
180
|
throw new GraphQLError(`House risk limit exceeded. Max payout: ${maxPotentialPayout.toFixed(4)}`);
|
|
183
181
|
}
|
|
184
|
-
const dbHashChain = await
|
|
182
|
+
const dbHashChain = await dbLockHubHashChain(pgClient, {
|
|
185
183
|
userId: session.user_id,
|
|
186
184
|
experienceId: session.experience_id,
|
|
187
185
|
casinoId: session.casino_id,
|
|
@@ -225,7 +223,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
225
223
|
throw new Error(`Unknown bet hash result: ${_exhaustiveCheck}`);
|
|
226
224
|
}
|
|
227
225
|
}
|
|
228
|
-
await
|
|
226
|
+
await dbInsertHubHash(pgClient, {
|
|
229
227
|
hashChainId: dbHashChain.id,
|
|
230
228
|
kind: DbHashKind.INTERMEDIATE,
|
|
231
229
|
digest: betHashResult.hash,
|
|
@@ -239,7 +237,18 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
239
237
|
if (result.rowCount !== 1) {
|
|
240
238
|
throw new GraphQLError("Failed to update hash chain iteration");
|
|
241
239
|
}
|
|
242
|
-
const
|
|
240
|
+
const finalHash = (() => {
|
|
241
|
+
const serverHash = betHashResult.hash;
|
|
242
|
+
const clientSeed = dbHashChain.client_seed;
|
|
243
|
+
const finalHash = createHmac("sha256", serverHash)
|
|
244
|
+
.update(clientSeed)
|
|
245
|
+
.digest();
|
|
246
|
+
return finalHash;
|
|
247
|
+
})();
|
|
248
|
+
const { outcome, outcomeIdx } = pickRandomOutcome({
|
|
249
|
+
outcomes: input.outcomes,
|
|
250
|
+
hash: finalHash,
|
|
251
|
+
});
|
|
243
252
|
const netPlayerAmount = input.wager * outcome.profit;
|
|
244
253
|
await pgClient.query({
|
|
245
254
|
text: `
|
|
@@ -275,14 +284,16 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
275
284
|
],
|
|
276
285
|
});
|
|
277
286
|
const immutableData = structuredClone({
|
|
287
|
+
wager: input.wager,
|
|
288
|
+
currencyKey: dbCurrency.key,
|
|
278
289
|
clientSeed: dbHashChain.client_seed,
|
|
279
290
|
hash: betHashResult.hash,
|
|
280
291
|
outcomes: input.outcomes,
|
|
281
292
|
outcomeIdx,
|
|
282
293
|
});
|
|
283
294
|
const finalizedMetadata = betConfig.finalizeMetadata
|
|
284
|
-
? betConfig.finalizeMetadata(
|
|
285
|
-
:
|
|
295
|
+
? betConfig.finalizeMetadata(initializedMetadata, immutableData)
|
|
296
|
+
: initializedMetadata;
|
|
286
297
|
const newBet = {
|
|
287
298
|
kind: rawInput.kind,
|
|
288
299
|
wager: input.wager,
|
|
@@ -338,7 +349,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
338
349
|
})
|
|
339
350
|
.then(exactlyOneRow);
|
|
340
351
|
return {
|
|
341
|
-
__typename: "
|
|
352
|
+
__typename: "HubMakeOutcomeBetSuccess",
|
|
342
353
|
betId: bet.id,
|
|
343
354
|
};
|
|
344
355
|
});
|
|
@@ -348,7 +359,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
348
359
|
});
|
|
349
360
|
},
|
|
350
361
|
},
|
|
351
|
-
|
|
362
|
+
HubMakeOutcomeBetSuccess: {
|
|
352
363
|
__assertStep: ObjectStep,
|
|
353
364
|
bet($data) {
|
|
354
365
|
const $betId = access($data, "betId");
|
|
@@ -360,7 +371,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
|
|
|
360
371
|
result($data) {
|
|
361
372
|
const $result = $data.get("result");
|
|
362
373
|
return polymorphicBranch($result, {
|
|
363
|
-
|
|
374
|
+
HubMakeOutcomeBetSuccess: {},
|
|
364
375
|
HubBadHashChainError: {},
|
|
365
376
|
});
|
|
366
377
|
},
|
|
@@ -375,7 +386,7 @@ function normalizeHash(hash) {
|
|
|
375
386
|
const uint32Value = view.getUint32(0, false);
|
|
376
387
|
return uint32Value / Math.pow(2, 32);
|
|
377
388
|
}
|
|
378
|
-
function pickRandomOutcome(outcomes, hash) {
|
|
389
|
+
function pickRandomOutcome({ outcomes, hash, }) {
|
|
379
390
|
assert(outcomes.length >= 2, "Outcome count must be >= 2");
|
|
380
391
|
const totalWeight = sum(outcomes.map((o) => o.weight));
|
|
381
392
|
const outcomesWithProbability = outcomes.map((o) => ({
|
|
@@ -449,7 +460,7 @@ async function finishHashChainInBackground({ hashChainId, }) {
|
|
|
449
460
|
iteration: 0,
|
|
450
461
|
});
|
|
451
462
|
await withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
452
|
-
await
|
|
463
|
+
await dbInsertHubHash(pgClient, {
|
|
453
464
|
hashChainId,
|
|
454
465
|
kind: DbHashKind.PREIMAGE,
|
|
455
466
|
digest: preimageHashResult.hash,
|
|
@@ -473,7 +484,3 @@ async function finishHashChainInBackground({ hashChainId, }) {
|
|
|
473
484
|
});
|
|
474
485
|
}
|
|
475
486
|
}
|
|
476
|
-
function withoutKey(obj, key) {
|
|
477
|
-
const { [key]: _, ...rest } = obj;
|
|
478
|
-
return rest;
|
|
479
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GraphQLInputFieldConfig } from "graphql";
|
|
2
|
+
export declare const HubOutcomeInputNonNullFieldsPlugin: {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
description: string;
|
|
6
|
+
schema: {
|
|
7
|
+
hooks: {
|
|
8
|
+
GraphQLInputObjectType_fields_field: (field: GraphQLInputFieldConfig, build: any, context: any) => {
|
|
9
|
+
type: any;
|
|
10
|
+
description?: import("graphql/jsutils/Maybe.js").Maybe<string>;
|
|
11
|
+
defaultValue?: unknown;
|
|
12
|
+
deprecationReason?: import("graphql/jsutils/Maybe.js").Maybe<string>;
|
|
13
|
+
extensions?: import("graphql/jsutils/Maybe.js").Maybe<Readonly<import("graphql").GraphQLInputFieldExtensions>>;
|
|
14
|
+
astNode?: import("graphql/jsutils/Maybe.js").Maybe<import("graphql").InputValueDefinitionNode>;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const HubOutcomeInputNonNullFieldsPlugin = {
|
|
2
|
+
name: "HubOutcomeInputNonNullFieldsPlugin",
|
|
3
|
+
version: "0.0.0",
|
|
4
|
+
description: "Specifies that HubOutcomeInput fields are non-null",
|
|
5
|
+
schema: {
|
|
6
|
+
hooks: {
|
|
7
|
+
GraphQLInputObjectType_fields_field: (field, build, context) => {
|
|
8
|
+
const { scope: { fieldName }, } = context;
|
|
9
|
+
if (context.scope.pgCodec?.name === "hubOutcome" &&
|
|
10
|
+
["profit", "weight"].includes(fieldName)) {
|
|
11
|
+
return {
|
|
12
|
+
...field,
|
|
13
|
+
type: new build.graphql.GraphQLNonNull(field.type),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return field;
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HubPutAlertPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { context, lambda } from "postgraphile/grafast";
|
|
2
|
+
import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
|
|
3
|
+
import { jsonParse } from "postgraphile/@dataplan/json";
|
|
4
|
+
import { listenWithFilter } from "./listen-with-filter.js";
|
|
5
|
+
export const HubPutAlertPlugin = makeExtendSchemaPlugin(() => {
|
|
6
|
+
return {
|
|
7
|
+
typeDefs: gql `
|
|
8
|
+
extend type Subscription {
|
|
9
|
+
hubPutAlert: HubPutAlertPayload
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type HubPutAlertPayload {
|
|
13
|
+
currencyKey: String!
|
|
14
|
+
mpTransferId: UUID!
|
|
15
|
+
}
|
|
16
|
+
`,
|
|
17
|
+
plans: {
|
|
18
|
+
Subscription: {
|
|
19
|
+
hubPutAlert: {
|
|
20
|
+
subscribePlan(_$root) {
|
|
21
|
+
const $pgSubscriber = context().get("pgSubscriber");
|
|
22
|
+
const $identity = context().get("identity");
|
|
23
|
+
const $channelKey = lambda($identity, (identity) => {
|
|
24
|
+
if (identity?.kind === "user") {
|
|
25
|
+
return `hub:user:${identity.session.user_id}:put`;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return listenWithFilter($pgSubscriber, $channelKey, jsonParse, $identity, (identity, item) => {
|
|
32
|
+
if (identity?.kind !== "user") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return (identity.session.experience_id ===
|
|
37
|
+
JSON.parse(item).experience_id);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
plan($event) {
|
|
42
|
+
return $event;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
HubPutAlertPayload: {
|
|
47
|
+
currencyKey($event) {
|
|
48
|
+
return $event.get("currency_key");
|
|
49
|
+
},
|
|
50
|
+
mpTransferId($event) {
|
|
51
|
+
return $event.get("mp_transfer_id");
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
@@ -1,55 +1,30 @@
|
|
|
1
|
-
import { access,
|
|
1
|
+
import { access, constant, context } from "postgraphile/grafast";
|
|
2
2
|
import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { TYPES } from "postgraphile/@dataplan/pg";
|
|
4
|
+
import { sql } from "postgraphile/pg-sql2";
|
|
5
5
|
export const HubUserBalanceByCurrencyPlugin = makeExtendSchemaPlugin((build) => {
|
|
6
|
-
const
|
|
6
|
+
const balanceTable = build.input.pgRegistry.pgResources.hub_balance;
|
|
7
7
|
return {
|
|
8
8
|
typeDefs: gql `
|
|
9
9
|
extend type HubUser {
|
|
10
|
-
|
|
10
|
+
hubBalanceByCurrency(currency: String!): HubBalance
|
|
11
11
|
}
|
|
12
12
|
`,
|
|
13
13
|
plans: {
|
|
14
14
|
HubUser: {
|
|
15
|
-
|
|
15
|
+
hubBalanceByCurrency: ($record, { $currency }) => {
|
|
16
16
|
const $identity = context().get("identity");
|
|
17
|
-
const $
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
const $balances = balanceTable.find();
|
|
18
|
+
$balances.where(sql `
|
|
19
|
+
${$balances}.currency_key = ${$balances.placeholder($currency, TYPES.text)}
|
|
20
|
+
AND ${$balances}.user_id = ${$balances.placeholder($record.get("id"), TYPES.uuid)}
|
|
21
|
+
AND ${$balances}.casino_id = ${$balances.placeholder(access($identity, ["session", "casino_id"]), TYPES.uuid)}
|
|
22
|
+
AND ${$balances}.experience_id = ${$balances.placeholder(access($identity, ["session", "experience_id"]), TYPES.uuid)}
|
|
23
|
+
`);
|
|
24
|
+
$balances.setFirst(constant(1));
|
|
25
|
+
return $balances.single();
|
|
25
26
|
},
|
|
26
27
|
},
|
|
27
28
|
},
|
|
28
29
|
};
|
|
29
30
|
});
|
|
30
|
-
async function batchGetUserBalanceByCurrency(paramsArray) {
|
|
31
|
-
const values = [];
|
|
32
|
-
const valuePlaceholders = [];
|
|
33
|
-
paramsArray.forEach((p, index) => {
|
|
34
|
-
const baseIndex = index * 4 + 1;
|
|
35
|
-
valuePlaceholders.push(`($${baseIndex}, $${baseIndex + 1}::uuid, $${baseIndex + 2}::uuid, $${baseIndex + 3}::uuid)`);
|
|
36
|
-
values.push(p.currency, p.targetUserId, p.casino_id, p.experience_id);
|
|
37
|
-
});
|
|
38
|
-
const sql = `
|
|
39
|
-
SELECT b.*
|
|
40
|
-
FROM hub.balance b
|
|
41
|
-
JOIN (
|
|
42
|
-
VALUES
|
|
43
|
-
${valuePlaceholders.join(",\n ")}
|
|
44
|
-
) AS vals(currency_key, user_id, casino_id, experience_id)
|
|
45
|
-
ON b.currency_key = vals.currency_key
|
|
46
|
-
AND b.user_id = vals.user_id
|
|
47
|
-
AND b.casino_id = vals.casino_id
|
|
48
|
-
AND b.experience_id = vals.experience_id
|
|
49
|
-
`;
|
|
50
|
-
const { rows } = await superuserPool.query(sql, values);
|
|
51
|
-
return paramsArray.map((p) => rows.find((row) => row.currency_key === p.currency &&
|
|
52
|
-
row.user_id === p.targetUserId &&
|
|
53
|
-
row.casino_id === p.casino_id &&
|
|
54
|
-
row.experience_id === p.experience_id));
|
|
55
|
-
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { __ItemStep, ExecutionDetails, GrafastResultStreamList } from "postgraphile/grafast";
|
|
2
|
+
import { GrafastSubscriber } from "postgraphile/grafast";
|
|
3
|
+
import { Step } from "postgraphile/grafast";
|
|
4
|
+
export declare class ListenWithFilterStep<TTopics extends {
|
|
5
|
+
[topic: string]: any;
|
|
6
|
+
}, TTopic extends keyof TTopics, TPayloadStep extends Step, TFilterInput> extends Step<TTopics[TTopic][]> {
|
|
7
|
+
itemPlan: (itemPlan: __ItemStep<TTopics[TTopic]>) => TPayloadStep;
|
|
8
|
+
filterFn: (filterInput: TFilterInput, item: any) => boolean;
|
|
9
|
+
static $$export: {
|
|
10
|
+
moduleName: string;
|
|
11
|
+
exportName: string;
|
|
12
|
+
};
|
|
13
|
+
isSyncAndSafe: boolean;
|
|
14
|
+
private pubsubDep;
|
|
15
|
+
private topicDep;
|
|
16
|
+
private filterInputDep;
|
|
17
|
+
constructor(pubsubOrPlan: Step<GrafastSubscriber<TTopics> | null> | GrafastSubscriber<TTopics> | null, topicOrPlan: Step<TTopic> | string, itemPlan: ((itemPlan: __ItemStep<TTopics[TTopic]>) => TPayloadStep) | undefined, filterInputPlan: Step<TFilterInput>, filterFn: (filterInput: TFilterInput, item: any) => boolean);
|
|
18
|
+
execute({ indexMap, values, stream, }: ExecutionDetails<readonly [GrafastSubscriber<TTopics>, TTopic]>): GrafastResultStreamList<TTopics[TTopic]>;
|
|
19
|
+
}
|
|
20
|
+
export declare function listenWithFilter<TTopics extends {
|
|
21
|
+
[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: (filterInput: TFilterInput, item: any) => boolean): ListenWithFilterStep<TTopics, TTopic, TPayloadStep, TFilterInput>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { constant, isDev, isExecutableStep, SafeError, } from "postgraphile/grafast";
|
|
2
|
+
import { Step } from "postgraphile/grafast";
|
|
3
|
+
export class ListenWithFilterStep extends Step {
|
|
4
|
+
itemPlan;
|
|
5
|
+
filterFn;
|
|
6
|
+
static $$export = {
|
|
7
|
+
moduleName: "grafast",
|
|
8
|
+
exportName: "ListenWithFilterStep",
|
|
9
|
+
};
|
|
10
|
+
isSyncAndSafe = true;
|
|
11
|
+
pubsubDep;
|
|
12
|
+
topicDep;
|
|
13
|
+
filterInputDep;
|
|
14
|
+
constructor(pubsubOrPlan, topicOrPlan, itemPlan = ($item) => $item, filterInputPlan, filterFn) {
|
|
15
|
+
super();
|
|
16
|
+
this.itemPlan = itemPlan;
|
|
17
|
+
this.filterFn = filterFn;
|
|
18
|
+
const $topic = typeof topicOrPlan === "string" ? constant(topicOrPlan) : topicOrPlan;
|
|
19
|
+
const $pubsub = isExecutableStep(pubsubOrPlan)
|
|
20
|
+
? pubsubOrPlan
|
|
21
|
+
: constant(pubsubOrPlan, false);
|
|
22
|
+
this.pubsubDep = this.addDependency($pubsub);
|
|
23
|
+
this.topicDep = this.addDependency($topic);
|
|
24
|
+
this.filterInputDep = this.addDependency(filterInputPlan);
|
|
25
|
+
}
|
|
26
|
+
execute({ indexMap, values, stream, }) {
|
|
27
|
+
if (!stream) {
|
|
28
|
+
throw new Error("ListenWithFilterStep must be streamed, never merely executed");
|
|
29
|
+
}
|
|
30
|
+
const pubsubValue = values[this.pubsubDep];
|
|
31
|
+
const topicValue = values[this.topicDep];
|
|
32
|
+
const filterInputValue = values[this.filterInputDep];
|
|
33
|
+
return indexMap((i) => {
|
|
34
|
+
const pubsub = pubsubValue.at(i);
|
|
35
|
+
if (!pubsub) {
|
|
36
|
+
throw new SafeError("Subscription not supported", isDev
|
|
37
|
+
? {
|
|
38
|
+
hint: `${this.dependencies[this.pubsubDep]} did not provide a GrafastSubscriber; perhaps you forgot to add the relevant property to context?`,
|
|
39
|
+
}
|
|
40
|
+
: {});
|
|
41
|
+
}
|
|
42
|
+
const topic = topicValue.at(i);
|
|
43
|
+
const filterInput = filterInputValue.at(i);
|
|
44
|
+
const origStream = pubsub.subscribe(topic);
|
|
45
|
+
const filterFn = this.filterFn;
|
|
46
|
+
return {
|
|
47
|
+
[Symbol.asyncIterator]: async function* () {
|
|
48
|
+
const iterator = await origStream;
|
|
49
|
+
for await (const item of iterator) {
|
|
50
|
+
if (filterFn(filterInput, item)) {
|
|
51
|
+
yield item;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function listenWithFilter(pubsubOrPlan, topicOrPlan, itemPlan = ($item) => $item, filterInputPlan, filterFn) {
|
|
60
|
+
return new ListenWithFilterStep(pubsubOrPlan, topicOrPlan, itemPlan, filterInputPlan, filterFn);
|
|
61
|
+
}
|
|
@@ -23,6 +23,8 @@ import { HubCurrentXPlugin } from "../plugins/hub-current-x.js";
|
|
|
23
23
|
import { HubCreateHashChainPlugin } from "../hash-chain/plugins/hub-create-hash-chain.js";
|
|
24
24
|
import { HubBadHashChainErrorPlugin } from "../hash-chain/plugins/hub-bad-hash-chain-error.js";
|
|
25
25
|
import { HubUserActiveHashChainPlugin } from "../hash-chain/plugins/hub-user-active-hash-chain.js";
|
|
26
|
+
import { HubOutcomeInputNonNullFieldsPlugin } from "../plugins/hub-outcome-input-non-null-fields.js";
|
|
27
|
+
import { HubPutAlertPlugin } from "../plugins/hub-put-alert.js";
|
|
26
28
|
export const requiredPlugins = [
|
|
27
29
|
SmartTagsPlugin,
|
|
28
30
|
IdToNodeIdPlugin,
|
|
@@ -31,6 +33,7 @@ export const requiredPlugins = [
|
|
|
31
33
|
HubCurrentXPlugin,
|
|
32
34
|
HubWithdrawPlugin,
|
|
33
35
|
HubBalanceAlertPlugin,
|
|
36
|
+
HubPutAlertPlugin,
|
|
34
37
|
HubUserBalanceByCurrencyPlugin,
|
|
35
38
|
HubAddCasinoPlugin,
|
|
36
39
|
ValidateCasinoFieldsPlugin,
|
|
@@ -38,6 +41,7 @@ export const requiredPlugins = [
|
|
|
38
41
|
export const defaultPlugins = [
|
|
39
42
|
...(config.NODE_ENV === "development" ? [DebugPlugin] : []),
|
|
40
43
|
...requiredPlugins,
|
|
44
|
+
HubOutcomeInputNonNullFieldsPlugin,
|
|
41
45
|
HubBadHashChainErrorPlugin,
|
|
42
46
|
HubCreateHashChainPlugin,
|
|
43
47
|
HubUserActiveHashChainPlugin,
|