@moneypot/hub 1.3.0-dev.15 → 1.3.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.
@@ -2,7 +2,6 @@ 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";
6
5
  export * from "./types.js";
7
6
  export * from "./public.js";
8
7
  export * from "./util.js";
@@ -5,7 +5,6 @@ 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";
9
8
  export * from "./types.js";
10
9
  export * from "./public.js";
11
10
  export * from "./util.js";
@@ -138,7 +138,3 @@ export type DbHash = {
138
138
  digest: Uint8Array;
139
139
  metadata: Record<string, unknown>;
140
140
  };
141
- export type DbHubOutcome = {
142
- weight: number;
143
- profit: number;
144
- };
@@ -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 dbLockHubHashChain(pgClient: PgClientInTransaction, { userId, experienceId, casinoId, hashChainId, }: {
3
+ export declare function dbLockHashChain(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 dbInsertHubHash(pgClient: PgClientInTransaction, { hashChainId, kind, digest, iteration, metadata, }: {
9
+ export declare function dbInsertHash(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 dbLockHubHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
4
- assert(pgClient._inTransaction, "dbLockHubHashChain must be called in a transaction");
3
+ export async function dbLockHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
4
+ assert(pgClient._inTransaction, "dbLockHashChain must be called in a transaction");
5
5
  return pgClient
6
6
  .query(`
7
7
  SELECT *
@@ -17,7 +17,7 @@ export async function dbLockHubHashChain(pgClient, { userId, experienceId, casin
17
17
  .then(maybeOneRow)
18
18
  .then((row) => row ?? null);
19
19
  }
20
- export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, iteration, metadata = {}, }) {
20
+ export async function dbInsertHash(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(`
@@ -1,15 +1,4 @@
1
1
  import { z } from "zod";
2
- import { Result } from "../util.js";
3
- declare const OutcomeSchema: z.ZodObject<{
4
- weight: z.ZodNumber;
5
- profit: z.ZodNumber;
6
- }, "strict", z.ZodTypeAny, {
7
- profit: number;
8
- weight: number;
9
- }, {
10
- profit: number;
11
- weight: number;
12
- }>;
13
2
  declare const InputSchema: z.ZodObject<{
14
3
  kind: z.ZodString;
15
4
  wager: z.ZodNumber;
@@ -18,62 +7,49 @@ declare const InputSchema: z.ZodObject<{
18
7
  weight: z.ZodNumber;
19
8
  profit: z.ZodNumber;
20
9
  }, "strict", z.ZodTypeAny, {
21
- profit: number;
22
10
  weight: number;
23
- }, {
24
11
  profit: number;
12
+ }, {
25
13
  weight: number;
26
- }>, "many">, {
27
14
  profit: number;
15
+ }>, "many">, {
28
16
  weight: number;
29
- }[], {
30
17
  profit: number;
18
+ }[], {
31
19
  weight: number;
32
- }[]>, {
33
20
  profit: number;
21
+ }[]>, {
34
22
  weight: number;
35
- }[], {
36
23
  profit: number;
24
+ }[], {
37
25
  weight: number;
26
+ profit: number;
38
27
  }[]>;
39
28
  hashChainId: z.ZodString;
40
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
41
29
  }, "strict", z.ZodTypeAny, {
42
30
  currency: string;
43
- hashChainId: string;
44
31
  kind: string;
32
+ hashChainId: string;
45
33
  wager: number;
46
34
  outcomes: {
47
- profit: number;
48
35
  weight: number;
36
+ profit: number;
49
37
  }[];
50
- metadata?: Record<string, any> | undefined;
51
38
  }, {
52
39
  currency: string;
53
- hashChainId: string;
54
40
  kind: string;
41
+ hashChainId: string;
55
42
  wager: number;
56
43
  outcomes: {
57
- profit: number;
58
44
  weight: number;
45
+ profit: number;
59
46
  }[];
60
- metadata?: Record<string, any> | undefined;
61
47
  }>;
62
48
  type Input = z.infer<typeof InputSchema>;
63
- type Outcome = z.infer<typeof OutcomeSchema>;
64
- type FinalizeMetadataData = {
65
- wager: number;
66
- currencyKey: string;
67
- clientSeed: string;
68
- hash: Uint8Array;
69
- outcomes: Outcome[];
70
- outcomeIdx: number;
71
- };
72
49
  export type OutcomeBetConfig = {
73
50
  houseEdge: number;
74
51
  saveOutcomes: boolean;
75
- initializeMetadataFromUntrustedUserInput?: (input: Input) => Result<Record<string, any>, string>;
76
- finalizeMetadata?: (validatedMetadata: Record<string, any>, data: FinalizeMetadataData) => Record<string, any>;
52
+ getMetadata?: (input: Input) => Promise<Record<string, any>>;
77
53
  };
78
54
  export type OutcomeBetConfigMap<BetKind extends string> = {
79
55
  [betKind in BetKind]: OutcomeBetConfig;
@@ -4,9 +4,8 @@ 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 { dbInsertHubHash, dbLockHubHashChain, } from "../hash-chain/db-hash-chain.js";
7
+ import { dbInsertHash, dbLockHashChain } from "../hash-chain/db-hash-chain.js";
8
8
  import { getIntermediateHash, getPreimageHash, } from "../hash-chain/get-hash.js";
9
- import { createHmac } from "node:crypto";
10
9
  const FLOAT_EPSILON = 1e-10;
11
10
  function sum(ns) {
12
11
  return ns.reduce((a, b) => a + b, 0);
@@ -39,7 +38,6 @@ const InputSchema = z
39
38
  .refine((data) => data.some((o) => o.profit < 0), "At least one outcome should have profit < 0")
40
39
  .refine((data) => data.some((o) => o.profit > 0), "At least one outcome should have profit > 0"),
41
40
  hashChainId: z.string().uuid("Invalid hash chain ID"),
42
- metadata: z.record(z.string(), z.any()).optional(),
43
41
  })
44
42
  .strict();
45
43
  const BetKindSchema = z
@@ -53,11 +51,10 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
53
51
  .gte(0, "House edge must be >= 0")
54
52
  .lte(1, "House edge must be <= 1"),
55
53
  saveOutcomes: z.boolean(),
56
- initializeMetadataFromUntrustedUserInput: z
57
- .function()
58
- .optional(),
59
- finalizeMetadata: z
54
+ getMetadata: z
60
55
  .function()
56
+ .args(InputSchema)
57
+ .returns(z.record(z.string(), z.any()))
61
58
  .optional(),
62
59
  }));
63
60
  export function MakeOutcomeBetPlugin({ betConfigs }) {
@@ -76,14 +73,13 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
76
73
  currency: String!
77
74
  outcomes: [HubOutcomeInput!]!
78
75
  hashChainId: UUID!
79
- metadata: JSON
80
76
  }
81
77
 
82
- type HubMakeOutcomeBetSuccess {
78
+ type HubMakeOutcomeBetOk {
83
79
  bet: HubOutcomeBet!
84
80
  }
85
81
 
86
- union HubMakeOutcomeBetResult = HubMakeOutcomeBetSuccess | HubBadHashChainError
82
+ union HubMakeOutcomeBetResult = HubMakeOutcomeBetOk | HubBadHashChainError
87
83
 
88
84
  type HubMakeOutcomeBetPayload {
89
85
  result: HubMakeOutcomeBetResult!
@@ -119,15 +115,8 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
119
115
  if (!betConfig) {
120
116
  throw new GraphQLError(`Invalid bet kind`);
121
117
  }
122
- let initializedMetadata;
123
- if (betConfig.initializeMetadataFromUntrustedUserInput) {
124
- const result = betConfig.initializeMetadataFromUntrustedUserInput(input);
125
- if (result.ok) {
126
- initializedMetadata = result.value;
127
- }
128
- else {
129
- throw new GraphQLError(`Invalid input: ${result.error}`);
130
- }
118
+ if (!betKinds.includes(rawInput.kind)) {
119
+ throw new GraphQLError(`Invalid bet kind`);
131
120
  }
132
121
  const houseEV = calculateHouseEV(input.outcomes);
133
122
  const minHouseEV = Math.max(0, betConfig.houseEdge - FLOAT_EPSILON);
@@ -179,7 +168,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
179
168
  if (maxPotentialPayout > maxAllowablePayout) {
180
169
  throw new GraphQLError(`House risk limit exceeded. Max payout: ${maxPotentialPayout.toFixed(4)}`);
181
170
  }
182
- const dbHashChain = await dbLockHubHashChain(pgClient, {
171
+ const dbHashChain = await dbLockHashChain(pgClient, {
183
172
  userId: session.user_id,
184
173
  experienceId: session.experience_id,
185
174
  casinoId: session.casino_id,
@@ -223,7 +212,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
223
212
  throw new Error(`Unknown bet hash result: ${_exhaustiveCheck}`);
224
213
  }
225
214
  }
226
- await dbInsertHubHash(pgClient, {
215
+ await dbInsertHash(pgClient, {
227
216
  hashChainId: dbHashChain.id,
228
217
  kind: DbHashKind.INTERMEDIATE,
229
218
  digest: betHashResult.hash,
@@ -237,18 +226,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
237
226
  if (result.rowCount !== 1) {
238
227
  throw new GraphQLError("Failed to update hash chain iteration");
239
228
  }
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
- });
229
+ const { outcome, outcomeIdx } = pickRandomOutcome(input.outcomes, betHashResult.hash);
252
230
  const netPlayerAmount = input.wager * outcome.profit;
253
231
  await pgClient.query({
254
232
  text: `
@@ -283,17 +261,6 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
283
261
  input.wager,
284
262
  ],
285
263
  });
286
- const immutableData = structuredClone({
287
- wager: input.wager,
288
- currencyKey: dbCurrency.key,
289
- clientSeed: dbHashChain.client_seed,
290
- hash: betHashResult.hash,
291
- outcomes: input.outcomes,
292
- outcomeIdx,
293
- });
294
- const finalizedMetadata = betConfig.finalizeMetadata
295
- ? betConfig.finalizeMetadata(initializedMetadata, immutableData)
296
- : initializedMetadata;
297
264
  const newBet = {
298
265
  kind: rawInput.kind,
299
266
  wager: input.wager,
@@ -303,7 +270,9 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
303
270
  user_id: session.user_id,
304
271
  casino_id: session.casino_id,
305
272
  experience_id: session.experience_id,
306
- metadata: finalizedMetadata || {},
273
+ metadata: betConfig.getMetadata
274
+ ? await betConfig.getMetadata(input)
275
+ : {},
307
276
  ...(betConfig.saveOutcomes
308
277
  ? {
309
278
  outcomes: input.outcomes,
@@ -349,7 +318,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
349
318
  })
350
319
  .then(exactlyOneRow);
351
320
  return {
352
- __typename: "HubMakeOutcomeBetSuccess",
321
+ __typename: "HubMakeOutcomeBetOk",
353
322
  betId: bet.id,
354
323
  };
355
324
  });
@@ -359,7 +328,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
359
328
  });
360
329
  },
361
330
  },
362
- HubMakeOutcomeBetSuccess: {
331
+ HubMakeOutcomeBetOk: {
363
332
  __assertStep: ObjectStep,
364
333
  bet($data) {
365
334
  const $betId = access($data, "betId");
@@ -371,7 +340,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
371
340
  result($data) {
372
341
  const $result = $data.get("result");
373
342
  return polymorphicBranch($result, {
374
- HubMakeOutcomeBetSuccess: {},
343
+ HubMakeOutcomeBetOk: {},
375
344
  HubBadHashChainError: {},
376
345
  });
377
346
  },
@@ -386,7 +355,7 @@ function normalizeHash(hash) {
386
355
  const uint32Value = view.getUint32(0, false);
387
356
  return uint32Value / Math.pow(2, 32);
388
357
  }
389
- function pickRandomOutcome({ outcomes, hash, }) {
358
+ function pickRandomOutcome(outcomes, hash) {
390
359
  assert(outcomes.length >= 2, "Outcome count must be >= 2");
391
360
  const totalWeight = sum(outcomes.map((o) => o.weight));
392
361
  const outcomesWithProbability = outcomes.map((o) => ({
@@ -460,7 +429,7 @@ async function finishHashChainInBackground({ hashChainId, }) {
460
429
  iteration: 0,
461
430
  });
462
431
  await withPgPoolTransaction(superuserPool, async (pgClient) => {
463
- await dbInsertHubHash(pgClient, {
432
+ await dbInsertHash(pgClient, {
464
433
  hashChainId,
465
434
  kind: DbHashKind.PREIMAGE,
466
435
  digest: preimageHashResult.hash,
@@ -1,30 +1,55 @@
1
- import { access, constant, context } from "postgraphile/grafast";
1
+ import { access, context, loadOne, object, } from "postgraphile/grafast";
2
2
  import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
- import { TYPES } from "postgraphile/@dataplan/pg";
4
- import { sql } from "postgraphile/pg-sql2";
3
+ import { superuserPool } from "../db/index.js";
4
+ import { pgSelectSingleFromRecord, } from "postgraphile/@dataplan/pg";
5
5
  export const HubUserBalanceByCurrencyPlugin = makeExtendSchemaPlugin((build) => {
6
- const balanceTable = build.input.pgRegistry.pgResources.hub_balance;
6
+ const balances = build.input.pgRegistry.pgResources.hub_balance;
7
7
  return {
8
8
  typeDefs: gql `
9
9
  extend type HubUser {
10
- hubBalanceByCurrency(currency: String!): HubBalance
10
+ balanceByCurrency(currency: String!): HubBalance
11
11
  }
12
12
  `,
13
13
  plans: {
14
14
  HubUser: {
15
- hubBalanceByCurrency: ($record, { $currency }) => {
15
+ balanceByCurrency: ($record, { $currency }) => {
16
16
  const $identity = context().get("identity");
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();
17
+ const $params = object({
18
+ currency: $currency,
19
+ targetUserId: $record.get("id"),
20
+ casino_id: access($identity, ["session", "casino_id"]),
21
+ experience_id: access($identity, ["session", "experience_id"]),
22
+ });
23
+ const $balance = loadOne($params, batchGetUserBalanceByCurrency);
24
+ return pgSelectSingleFromRecord(balances, $balance);
26
25
  },
27
26
  },
28
27
  },
29
28
  };
30
29
  });
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
+ }
@@ -23,7 +23,6 @@ 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
26
  export const requiredPlugins = [
28
27
  SmartTagsPlugin,
29
28
  IdToNodeIdPlugin,
@@ -39,7 +38,6 @@ export const requiredPlugins = [
39
38
  export const defaultPlugins = [
40
39
  ...(config.NODE_ENV === "development" ? [DebugPlugin] : []),
41
40
  ...requiredPlugins,
42
- HubOutcomeInputNonNullFieldsPlugin,
43
41
  HubBadHashChainErrorPlugin,
44
42
  HubCreateHashChainPlugin,
45
43
  HubUserActiveHashChainPlugin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.3.0-dev.15",
3
+ "version": "1.3.0-dev.4",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [
@@ -1,18 +0,0 @@
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
- };
@@ -1,20 +0,0 @@
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
- };