@moneypot/hub 1.4.4 → 1.4.6

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.
@@ -119,7 +119,6 @@ export type DbHashChain = {
119
119
  user_id: string;
120
120
  experience_id: string;
121
121
  casino_id: string;
122
- client_seed: string;
123
122
  max_iterations: number;
124
123
  current_iteration: number;
125
124
  active: boolean;
@@ -136,9 +135,23 @@ export type DbHash = {
136
135
  hash_chain_id: string;
137
136
  iteration: number;
138
137
  digest: Uint8Array;
138
+ client_seed: string;
139
139
  metadata: Record<string, unknown>;
140
140
  };
141
- export type DbHubOutcome = {
141
+ export type DbOutcome = {
142
142
  weight: number;
143
143
  profit: number;
144
144
  };
145
+ export type DbOutcomeBet = {
146
+ id: string;
147
+ hash_id: DbHash["id"];
148
+ kind: string;
149
+ wager: number;
150
+ currency_key: string;
151
+ profit: number;
152
+ user_id: string;
153
+ casino_id: string;
154
+ experience_id: string;
155
+ outcomes: DbOutcome[];
156
+ outcome_idx: number;
157
+ };
@@ -6,10 +6,11 @@ export declare function dbLockHubHashChain(pgClient: PgClientInTransaction, { us
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 dbInsertHubHash(pgClient: PgClientInTransaction, { hashChainId, kind, digest, iteration, clientSeed, metadata, }: {
10
10
  hashChainId: DbHashChain["id"];
11
11
  kind: DbHash["kind"];
12
12
  digest: DbHash["digest"];
13
13
  iteration: number;
14
+ clientSeed?: string;
14
15
  metadata?: DbHash["metadata"];
15
16
  }): Promise<DbHash>;
@@ -1,3 +1,4 @@
1
+ import { DbHashKind, } from "../db/types.js";
1
2
  import { exactlyOneRow, maybeOneRow, } from "../db/index.js";
2
3
  import { assert } from "tsafe";
3
4
  export async function dbLockHubHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
@@ -17,18 +18,20 @@ export async function dbLockHubHashChain(pgClient, { userId, experienceId, casin
17
18
  .then(maybeOneRow)
18
19
  .then((row) => row ?? null);
19
20
  }
20
- export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, iteration, metadata = {}, }) {
21
+ export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, iteration, clientSeed, metadata = {}, }) {
21
22
  assert(pgClient._inTransaction, "dbInsertHash must be called in a transaction");
23
+ assert(kind === DbHashKind.INTERMEDIATE && typeof clientSeed === "string", "clientSeed must be provided for INTERMEDIATE hashes");
22
24
  return pgClient
23
25
  .query(`
24
- INSERT INTO hub.hash (hash_chain_id, kind, digest, iteration, metadata)
25
- VALUES ($1, $2, $3, $4, $5)
26
+ INSERT INTO hub.hash (hash_chain_id, kind, digest, iteration, client_seed, metadata)
27
+ VALUES ($1, $2, $3, $4, $5, $6)
26
28
  RETURNING *
27
29
  `, [
28
30
  hashChainId,
29
31
  kind,
30
32
  digest,
31
33
  iteration,
34
+ clientSeed,
32
35
  metadata,
33
36
  ])
34
37
  .then(exactlyOneRow);
@@ -1,47 +1,31 @@
1
1
  import { context, object, sideEffect } from "@moneypot/hub/grafast";
2
2
  import { gql, makeExtendSchemaPlugin } from "@moneypot/hub/graphile";
3
3
  import { GraphQLError } from "graphql";
4
- import { z } from "zod";
5
4
  import { PgAdvisoryLock } from "../../pg-advisory-lock.js";
6
5
  import { exactlyOneRow, superuserPool, withPgPoolTransaction, } from "@moneypot/hub/db";
7
6
  import * as HashCommon from "../get-hash.js";
8
7
  import { DbHashKind } from "../../db/types.js";
9
8
  import * as config from "../../config.js";
10
- const InputSchema = z.object({
11
- clientSeed: z.string(),
12
- });
13
9
  export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
14
10
  const hashChainTable = build.input.pgRegistry.pgResources.hub_hash_chain;
15
11
  return {
16
12
  typeDefs: gql `
17
- input HubCreateHashChainInput {
18
- clientSeed: String!
19
- }
20
-
21
13
  type HubCreateHashChainPayload {
22
14
  hashChain: HubHashChain!
23
15
  }
24
16
 
25
17
  extend type Mutation {
26
- hubCreateHashChain(
27
- input: HubCreateHashChainInput!
28
- ): HubCreateHashChainPayload!
18
+ hubCreateHashChain: HubCreateHashChainPayload!
29
19
  }
30
20
  `,
31
21
  plans: {
32
22
  Mutation: {
33
- hubCreateHashChain: (_, { $input }) => {
23
+ hubCreateHashChain: () => {
34
24
  const $identity = context().get("identity");
35
- const $hashChainId = sideEffect([$input, $identity], ([rawInput, identity]) => {
25
+ const $hashChainId = sideEffect([$identity], ([identity]) => {
36
26
  if (identity?.kind !== "user") {
37
27
  throw new GraphQLError("Unauthorized");
38
28
  }
39
- const result = InputSchema.safeParse(rawInput);
40
- if (!result.success) {
41
- const message = result.error.errors[0].message;
42
- throw new GraphQLError(message);
43
- }
44
- const { clientSeed } = result.data;
45
29
  return withPgPoolTransaction(superuserPool, async (pgClient) => {
46
30
  await PgAdvisoryLock.forNewHashChain(pgClient, {
47
31
  userId: identity.session.user_id,
@@ -66,18 +50,16 @@ export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
66
50
  user_id,
67
51
  experience_id,
68
52
  casino_id,
69
- client_seed,
70
53
  active,
71
54
  max_iteration,
72
55
  current_iteration
73
56
  )
74
- VALUES ($1, $2, $3, $4, true, $5, $5)
57
+ VALUES ($1, $2, $3, true, $4, $4)
75
58
  RETURNING *
76
59
  `, [
77
60
  identity.session.user_id,
78
61
  identity.session.experience_id,
79
62
  identity.session.casino_id,
80
- clientSeed,
81
63
  config.HASHCHAINSERVER_MAX_ITERATIONS,
82
64
  ])
83
65
  .then(exactlyOneRow);
@@ -0,0 +1,54 @@
1
+ -- Migration: Add hash_id to hub.outcome_bet table
2
+
3
+ -- Step 1: Add the hash_id column (nullable initially)
4
+ ALTER TABLE hub.outcome_bet
5
+ ADD COLUMN hash_id uuid REFERENCES hub.hash(id);
6
+
7
+ -- Step 2: Index
8
+ CREATE INDEX outcome_bet_hash_id_idx ON hub.outcome_bet(hash_id);
9
+
10
+ -- Step 3: Update existing rows to set hash_id based on hash_chain_id
11
+ -- This uses a window function to assign row numbers within each hash_chain_id group
12
+ -- ordered by the bet creation time (id)
13
+ WITH bet_iterations AS (
14
+ SELECT
15
+ ob.id,
16
+ ob.hash_chain_id,
17
+ hc.max_iteration,
18
+ -- Row number starting from 1 for each hash_chain_id group
19
+ -- Ordered by id (which is uuid v7, so it's time-ordered)
20
+ ROW_NUMBER() OVER (PARTITION BY ob.hash_chain_id ORDER BY ob.id) as bet_sequence
21
+ FROM hub.outcome_bet ob
22
+ JOIN hub.hash_chain hc ON hc.id = ob.hash_chain_id
23
+ ),
24
+ hash_mapping AS (
25
+ SELECT
26
+ bi.id as bet_id,
27
+ h.id as hash_id
28
+ FROM bet_iterations bi
29
+ JOIN hub.hash h ON
30
+ h.hash_chain_id = bi.hash_chain_id AND
31
+ -- Calculate the iteration: max_iteration - bet_sequence
32
+ h.iteration = bi.max_iteration - bi.bet_sequence
33
+ WHERE h.kind = 'INTERMEDIATE'
34
+ )
35
+ UPDATE hub.outcome_bet ob
36
+ SET hash_id = hm.hash_id
37
+ FROM hash_mapping hm
38
+ WHERE ob.id = hm.bet_id;
39
+
40
+ -- Step 4: Make hash_id NOT NULL after populating it
41
+ ALTER TABLE hub.outcome_bet ALTER COLUMN hash_id SET NOT NULL;
42
+
43
+ -- Step 5: Drop hash chain id
44
+ ALTER TABLE hub.outcome_bet DROP COLUMN hash_chain_id;
45
+
46
+ -- Step 5: Add client_seed column to hash table
47
+ ALTER TABLE hub.hash ADD COLUMN client_seed text null;
48
+
49
+ -- Step 6: Update hash table with client_seed from hash_chain for each outcome_bet
50
+ UPDATE hub.hash h
51
+ SET client_seed = hc.client_seed
52
+ FROM hub.outcome_bet ob
53
+ JOIN hub.hash_chain hc ON hc.id = ob.hash_chain_id
54
+ WHERE h.id = ob.hash_id;
@@ -1,17 +1,9 @@
1
1
  import { z } from "zod";
2
+ import { DbOutcome } from "../db/index.js";
2
3
  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
4
  declare const InputSchema: z.ZodObject<{
14
5
  kind: z.ZodString;
6
+ clientSeed: z.ZodString;
15
7
  wager: z.ZodNumber;
16
8
  currency: z.ZodString;
17
9
  outcomes: z.ZodEffects<z.ZodEffects<z.ZodArray<z.ZodObject<{
@@ -42,6 +34,7 @@ declare const InputSchema: z.ZodObject<{
42
34
  currency: string;
43
35
  hashChainId: string;
44
36
  kind: string;
37
+ clientSeed: string;
45
38
  wager: number;
46
39
  outcomes: {
47
40
  profit: number;
@@ -52,6 +45,7 @@ declare const InputSchema: z.ZodObject<{
52
45
  currency: string;
53
46
  hashChainId: string;
54
47
  kind: string;
48
+ clientSeed: string;
55
49
  wager: number;
56
50
  outcomes: {
57
51
  profit: number;
@@ -60,20 +54,20 @@ declare const InputSchema: z.ZodObject<{
60
54
  metadata?: Record<string, unknown> | undefined;
61
55
  }>;
62
56
  type Input = z.infer<typeof InputSchema>;
63
- type Outcome = z.infer<typeof OutcomeSchema>;
64
57
  type Metadata = NonNullable<Input["metadata"]>;
65
58
  type FinalizeMetadataData = {
66
59
  wager: number;
67
60
  currencyKey: string;
68
61
  clientSeed: string;
69
62
  hash: Uint8Array;
70
- outcomes: Outcome[];
63
+ outcomes: DbOutcome[];
71
64
  outcomeIdx: number;
65
+ roll: number;
72
66
  };
73
67
  export type OutcomeBetConfig = {
74
68
  houseEdge: number;
75
69
  saveOutcomes: boolean;
76
- allowLossBeyondWager: boolean;
70
+ allowLossBeyondWager?: boolean;
77
71
  initializeMetadataFromUntrustedUserInput?: (input: Input) => Result<Metadata, string>;
78
72
  finalizeMetadata?: (validatedMetadata: Metadata, data: FinalizeMetadataData) => Metadata;
79
73
  };
@@ -27,6 +27,7 @@ const OutcomeSchema = z
27
27
  const InputSchema = z
28
28
  .object({
29
29
  kind: z.string(),
30
+ clientSeed: z.string(),
30
31
  wager: z
31
32
  .number()
32
33
  .int("Wager must be an integer")
@@ -53,7 +54,7 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
53
54
  .gte(0, "House edge must be >= 0")
54
55
  .lte(1, "House edge must be <= 1"),
55
56
  saveOutcomes: z.boolean(),
56
- allowLossBeyondWager: z.boolean().optional().default(false),
57
+ allowLossBeyondWager: z.boolean().default(false),
57
58
  initializeMetadataFromUntrustedUserInput: z
58
59
  .function()
59
60
  .optional(),
@@ -77,6 +78,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
77
78
  currency: String!
78
79
  outcomes: [HubOutcomeInput!]!
79
80
  hashChainId: UUID!
81
+ clientSeed: String!
80
82
  metadata: JSON
81
83
  }
82
84
 
@@ -230,11 +232,12 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
230
232
  throw new Error(`Unknown bet hash result: ${_exhaustiveCheck}`);
231
233
  }
232
234
  }
233
- await dbInsertHubHash(pgClient, {
235
+ const dbHash = await dbInsertHubHash(pgClient, {
234
236
  hashChainId: dbHashChain.id,
235
237
  kind: DbHashKind.INTERMEDIATE,
236
238
  digest: betHashResult.hash,
237
239
  iteration: betHashIteration,
240
+ clientSeed: input.clientSeed,
238
241
  });
239
242
  const result = await pgClient.query(`
240
243
  UPDATE hub.hash_chain
@@ -246,13 +249,13 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
246
249
  }
247
250
  const finalHash = (() => {
248
251
  const serverHash = betHashResult.hash;
249
- const clientSeed = dbHashChain.client_seed;
252
+ const clientSeed = input.clientSeed;
250
253
  const finalHash = createHmac("sha256", serverHash)
251
254
  .update(clientSeed)
252
255
  .digest();
253
256
  return finalHash;
254
257
  })();
255
- const { outcome, outcomeIdx } = pickRandomOutcome({
258
+ const { outcome, outcomeIdx, roll } = pickRandomOutcome({
256
259
  outcomes: input.outcomes,
257
260
  hash: finalHash,
258
261
  });
@@ -293,10 +296,11 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
293
296
  const immutableData = structuredClone({
294
297
  wager: input.wager,
295
298
  currencyKey: dbCurrency.key,
296
- clientSeed: dbHashChain.client_seed,
299
+ clientSeed: input.clientSeed,
297
300
  hash: betHashResult.hash,
298
301
  outcomes: input.outcomes,
299
302
  outcomeIdx,
303
+ roll,
300
304
  });
301
305
  const finalizedMetadata = betConfig.finalizeMetadata
302
306
  ? betConfig.finalizeMetadata(initializedMetadata, immutableData)
@@ -306,7 +310,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
306
310
  wager: input.wager,
307
311
  profit: outcome.profit,
308
312
  currency_key: dbCurrency.key,
309
- hash_chain_id: input.hashChainId,
313
+ hash_id: dbHash.id,
310
314
  user_id: session.user_id,
311
315
  casino_id: session.casino_id,
312
316
  experience_id: session.experience_id,
@@ -328,7 +332,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
328
332
  user_id,
329
333
  casino_id,
330
334
  experience_id,
331
- hash_chain_id,
335
+ hash_id,
332
336
  kind,
333
337
  currency_key,
334
338
  wager,
@@ -344,7 +348,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
344
348
  newBet.user_id,
345
349
  newBet.casino_id,
346
350
  newBet.experience_id,
347
- newBet.hash_chain_id,
351
+ newBet.hash_id,
348
352
  newBet.kind,
349
353
  newBet.currency_key,
350
354
  newBet.wager,
@@ -402,18 +406,19 @@ function pickRandomOutcome({ outcomes, hash, }) {
402
406
  }));
403
407
  const totalProb = outcomesWithProbability.reduce((sum, outcome) => sum + outcome.probability, 0);
404
408
  assert(Math.abs(totalProb - 1.0) < FLOAT_EPSILON, "Probabilities must sum to ~1");
405
- const randomValue = normalizeHash(hash);
409
+ const roll = normalizeHash(hash);
406
410
  let cumulativeProb = 0;
407
411
  for (let i = 0; i < outcomesWithProbability.length; i++) {
408
412
  const outcome = outcomesWithProbability[i];
409
413
  cumulativeProb += outcome.probability;
410
- if (randomValue <= cumulativeProb) {
411
- return { outcome, outcomeIdx: i };
414
+ if (roll <= cumulativeProb) {
415
+ return { outcome, outcomeIdx: i, roll };
412
416
  }
413
417
  }
414
418
  return {
415
419
  outcome: outcomes[outcomes.length - 1],
416
420
  outcomeIdx: outcomes.length - 1,
421
+ roll,
417
422
  };
418
423
  }
419
424
  async function finishHashChainInBackground({ hashChainId, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [