@moneypot/hub 1.4.3 → 1.4.5

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 CHANGED
@@ -76,6 +76,10 @@ insert into hub.api_key default values returning key;
76
76
 
77
77
  ## Changelog
78
78
 
79
+ ### 1.4.4
80
+
81
+ - Added `allowLossBeyondWager` option to `MakeOutcomeBetPlugin`. Must be set to `true` to allow outcome profits to be less than -1.
82
+
79
83
  ### 1.4.1
80
84
 
81
85
  - Added `hubPutAlert` subscription
@@ -1,8 +1,7 @@
1
- import * as pg from "pg";
2
1
  import { DbBalance, DbBankroll, DbCurrency, DbSession } from "./types.js";
3
- import { PgClientInTransaction } from "./index.js";
4
- export declare function dbGetActiveSessionById(pgClient: pg.PoolClient, sessionId: string): Promise<DbSession | undefined>;
5
- export declare function dbGetCasinoCurrencyByKey(pgClient: pg.PoolClient, { currencyKey, casinoId }: {
2
+ import { PgClientInTransaction, QueryExecutor } from "./index.js";
3
+ export declare function dbGetActiveSessionById(pgClient: QueryExecutor, sessionId: string): Promise<DbSession | undefined>;
4
+ export declare function dbGetCasinoCurrencyByKey(pgClient: QueryExecutor, { currencyKey, casinoId }: {
6
5
  currencyKey: string;
7
6
  casinoId: string;
8
7
  }): Promise<DbCurrency | undefined>;
@@ -2,21 +2,21 @@ import { maybeOneRow } from "./util.js";
2
2
  import { assert } from "tsafe";
3
3
  export async function dbGetActiveSessionById(pgClient, sessionId) {
4
4
  return pgClient
5
- .query("select * from hub.active_session where id = $1", [
6
- sessionId,
7
- ])
5
+ .query(`
6
+ SELECT *
7
+ FROM hub.active_session
8
+ WHERE id = $1
9
+ `, [sessionId])
8
10
  .then(maybeOneRow);
9
11
  }
10
12
  export async function dbGetCasinoCurrencyByKey(pgClient, { currencyKey, casinoId }) {
11
13
  return pgClient
12
- .query({
13
- text: `
14
+ .query(`
14
15
  SELECT *
15
16
  FROM hub.currency
16
- WHERE key = $1 AND casino_id = $2
17
- `,
18
- values: [currencyKey, casinoId],
19
- })
17
+ WHERE key = $1
18
+ AND casino_id = $2
19
+ `, [currencyKey, casinoId])
20
20
  .then(maybeOneRow);
21
21
  }
22
22
  export async function dbLockPlayerBalanceAndHouseBankroll(pgClient, { userId, casinoId, experienceId, currencyKey, }) {
@@ -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;
@@ -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,9 @@
1
+ alter table hub.outcome_bet add column client_seed text;
2
+
3
+ update hub.outcome_bet
4
+ set client_seed = hash_chain.client_seed
5
+ from hub.hash_chain where outcome_bet.hash_chain_id = hash_chain.id;
6
+
7
+ alter table hub.outcome_bet alter column client_seed set not null;
8
+
9
+ alter table hub.hash_chain drop column client_seed;
@@ -12,6 +12,7 @@ declare const OutcomeSchema: z.ZodObject<{
12
12
  }>;
13
13
  declare const InputSchema: z.ZodObject<{
14
14
  kind: z.ZodString;
15
+ clientSeed: z.ZodString;
15
16
  wager: z.ZodNumber;
16
17
  currency: z.ZodString;
17
18
  outcomes: z.ZodEffects<z.ZodEffects<z.ZodArray<z.ZodObject<{
@@ -42,6 +43,7 @@ declare const InputSchema: z.ZodObject<{
42
43
  currency: string;
43
44
  hashChainId: string;
44
45
  kind: string;
46
+ clientSeed: string;
45
47
  wager: number;
46
48
  outcomes: {
47
49
  profit: number;
@@ -52,6 +54,7 @@ declare const InputSchema: z.ZodObject<{
52
54
  currency: string;
53
55
  hashChainId: string;
54
56
  kind: string;
57
+ clientSeed: string;
55
58
  wager: number;
56
59
  outcomes: {
57
60
  profit: number;
@@ -69,10 +72,12 @@ type FinalizeMetadataData = {
69
72
  hash: Uint8Array;
70
73
  outcomes: Outcome[];
71
74
  outcomeIdx: number;
75
+ roll: number;
72
76
  };
73
77
  export type OutcomeBetConfig = {
74
78
  houseEdge: number;
75
79
  saveOutcomes: boolean;
80
+ allowLossBeyondWager?: boolean;
76
81
  initializeMetadataFromUntrustedUserInput?: (input: Input) => Result<Metadata, string>;
77
82
  finalizeMetadata?: (validatedMetadata: Metadata, data: FinalizeMetadataData) => Metadata;
78
83
  };
@@ -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,6 +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(),
57
+ allowLossBeyondWager: z.boolean().default(false),
56
58
  initializeMetadataFromUntrustedUserInput: z
57
59
  .function()
58
60
  .optional(),
@@ -76,6 +78,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
76
78
  currency: String!
77
79
  outcomes: [HubOutcomeInput!]!
78
80
  hashChainId: UUID!
81
+ clientSeed: String!
79
82
  metadata: JSON
80
83
  }
81
84
 
@@ -119,6 +122,12 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
119
122
  if (!betConfig) {
120
123
  throw new GraphQLError(`Invalid bet kind`);
121
124
  }
125
+ if (!betConfig.allowLossBeyondWager) {
126
+ const minProfit = Math.min(...input.outcomes.map((o) => o.profit));
127
+ if (minProfit < -1) {
128
+ throw new GraphQLError(`Loss beyond wager not allowed: outcome profit must be >= -1`);
129
+ }
130
+ }
122
131
  let initializedMetadata;
123
132
  if (betConfig.initializeMetadataFromUntrustedUserInput) {
124
133
  const result = betConfig.initializeMetadataFromUntrustedUserInput(input);
@@ -239,13 +248,13 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
239
248
  }
240
249
  const finalHash = (() => {
241
250
  const serverHash = betHashResult.hash;
242
- const clientSeed = dbHashChain.client_seed;
251
+ const clientSeed = input.clientSeed;
243
252
  const finalHash = createHmac("sha256", serverHash)
244
253
  .update(clientSeed)
245
254
  .digest();
246
255
  return finalHash;
247
256
  })();
248
- const { outcome, outcomeIdx } = pickRandomOutcome({
257
+ const { outcome, outcomeIdx, roll } = pickRandomOutcome({
249
258
  outcomes: input.outcomes,
250
259
  hash: finalHash,
251
260
  });
@@ -286,10 +295,11 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
286
295
  const immutableData = structuredClone({
287
296
  wager: input.wager,
288
297
  currencyKey: dbCurrency.key,
289
- clientSeed: dbHashChain.client_seed,
298
+ clientSeed: input.clientSeed,
290
299
  hash: betHashResult.hash,
291
300
  outcomes: input.outcomes,
292
301
  outcomeIdx,
302
+ roll,
293
303
  });
294
304
  const finalizedMetadata = betConfig.finalizeMetadata
295
305
  ? betConfig.finalizeMetadata(initializedMetadata, immutableData)
@@ -303,6 +313,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
303
313
  user_id: session.user_id,
304
314
  casino_id: session.casino_id,
305
315
  experience_id: session.experience_id,
316
+ client_seed: input.clientSeed,
306
317
  metadata: finalizedMetadata || {},
307
318
  ...(betConfig.saveOutcomes
308
319
  ? {
@@ -328,9 +339,10 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
328
339
  profit,
329
340
  outcomes,
330
341
  outcome_idx,
342
+ client_seed,
331
343
  metadata
332
344
  )
333
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
345
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
334
346
  RETURNING id
335
347
  `,
336
348
  values: [
@@ -344,6 +356,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
344
356
  newBet.profit,
345
357
  newBet.outcomes.map((o) => `(${o.weight},${o.profit})`),
346
358
  newBet.outcome_idx,
359
+ newBet.client_seed,
347
360
  newBet.metadata,
348
361
  ],
349
362
  })
@@ -395,18 +408,19 @@ function pickRandomOutcome({ outcomes, hash, }) {
395
408
  }));
396
409
  const totalProb = outcomesWithProbability.reduce((sum, outcome) => sum + outcome.probability, 0);
397
410
  assert(Math.abs(totalProb - 1.0) < FLOAT_EPSILON, "Probabilities must sum to ~1");
398
- const randomValue = normalizeHash(hash);
411
+ const roll = normalizeHash(hash);
399
412
  let cumulativeProb = 0;
400
413
  for (let i = 0; i < outcomesWithProbability.length; i++) {
401
414
  const outcome = outcomesWithProbability[i];
402
415
  cumulativeProb += outcome.probability;
403
- if (randomValue <= cumulativeProb) {
404
- return { outcome, outcomeIdx: i };
416
+ if (roll <= cumulativeProb) {
417
+ return { outcome, outcomeIdx: i, roll };
405
418
  }
406
419
  }
407
420
  return {
408
421
  outcome: outcomes[outcomes.length - 1],
409
422
  outcomeIdx: outcomes.length - 1,
423
+ roll,
410
424
  };
411
425
  }
412
426
  async function finishHashChainInBackground({ hashChainId, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [