@moneypot/hub 1.3.0-dev.9 → 1.3.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.
@@ -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";
@@ -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 dbLockHashChain(pgClient: PgClientInTransaction, { userId, experienceId, casinoId, hashChainId, }: {
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 dbInsertHash(pgClient: PgClientInTransaction, { hashChainId, kind, digest, iteration, metadata, }: {
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 dbLockHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
4
- assert(pgClient._inTransaction, "dbLockHashChain must be called in a transaction");
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 dbInsertHash(pgClient, { hashChainId, kind, digest, iteration, metadata = {}, }) {
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(`
@@ -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.ZodAny>>;
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, any> | undefined;
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, any> | undefined;
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
- validateUntrustedMetadata?: (input: Input) => Result<Record<string, any>, string>;
77
- finalizeMetadata?: (input: InputWithoutMetadata, validatedMetadata: Record<string, any>, data: FinalizeMetadataData) => Record<string, any>;
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 { dbInsertHash, dbLockHashChain } from "../hash-chain/db-hash-chain.js";
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.any()).optional(),
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
- validateUntrustedMetadata: z
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 HubMakeOutcomeBetOk {
82
+ type HubMakeOutcomeBetSuccess {
82
83
  bet: HubOutcomeBet!
83
84
  }
84
85
 
85
- union HubMakeOutcomeBetResult = HubMakeOutcomeBetOk | HubBadHashChainError
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
- if (!betKinds.includes(rawInput.kind)) {
122
- throw new GraphQLError(`Invalid bet kind`);
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
- validatedMetadata = result.value;
126
+ initializedMetadata = result.value;
129
127
  }
130
128
  else {
131
- throw new GraphQLError(`Invalid metadata: ${result.error}`);
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 dbLockHashChain(pgClient, {
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 dbInsertHash(pgClient, {
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 { outcome, outcomeIdx } = pickRandomOutcome(input.outcomes, betHashResult.hash);
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(withoutKey(input, "metadata"), validatedMetadata, immutableData)
285
- : validatedMetadata;
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: "HubMakeOutcomeBetOk",
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
- HubMakeOutcomeBetOk: {
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
- HubMakeOutcomeBetOk: {},
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 dbInsertHash(pgClient, {
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
+ };
@@ -1,55 +1,30 @@
1
- import { access, context, loadOne, object, } from "postgraphile/grafast";
1
+ import { access, constant, context } from "postgraphile/grafast";
2
2
  import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
- import { superuserPool } from "../db/index.js";
4
- import { pgSelectSingleFromRecord, } from "postgraphile/@dataplan/pg";
3
+ import { TYPES } from "postgraphile/@dataplan/pg";
4
+ import { sql } from "postgraphile/pg-sql2";
5
5
  export const HubUserBalanceByCurrencyPlugin = makeExtendSchemaPlugin((build) => {
6
- const balances = build.input.pgRegistry.pgResources.hub_balance;
6
+ const balanceTable = build.input.pgRegistry.pgResources.hub_balance;
7
7
  return {
8
8
  typeDefs: gql `
9
9
  extend type HubUser {
10
- balanceByCurrency(currency: String!): HubBalance
10
+ hubBalanceByCurrency(currency: String!): HubBalance
11
11
  }
12
12
  `,
13
13
  plans: {
14
14
  HubUser: {
15
- balanceByCurrency: ($record, { $currency }) => {
15
+ hubBalanceByCurrency: ($record, { $currency }) => {
16
16
  const $identity = context().get("identity");
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);
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
- }
@@ -23,6 +23,7 @@ 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";
26
27
  export const requiredPlugins = [
27
28
  SmartTagsPlugin,
28
29
  IdToNodeIdPlugin,
@@ -38,6 +39,7 @@ export const requiredPlugins = [
38
39
  export const defaultPlugins = [
39
40
  ...(config.NODE_ENV === "development" ? [DebugPlugin] : []),
40
41
  ...requiredPlugins,
42
+ HubOutcomeInputNonNullFieldsPlugin,
41
43
  HubBadHashChainErrorPlugin,
42
44
  HubCreateHashChainPlugin,
43
45
  HubUserActiveHashChainPlugin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.3.0-dev.9",
3
+ "version": "1.3.0",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [