@moneypot/hub 1.12.0 → 1.13.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 CHANGED
@@ -76,6 +76,10 @@ insert into hub.api_key default values returning key;
76
76
 
77
77
  ## Changelog
78
78
 
79
+ ### 1.13.x
80
+
81
+ - Added `hubRevealHashChain` mutation to generate a preimage hash for the chain and mark it as inactive.
82
+
79
83
  ### 1.12.x
80
84
 
81
85
  - Added `hub.audit_log` table to track balance and bankroll changes.
@@ -136,17 +136,21 @@ export async function setTransferCursor(pgClient, { cursor, casinoId, }) {
136
136
  export async function upsertUser(pgClient, { uname, casinoId, mpUserId, }) {
137
137
  const user = await pgClient
138
138
  .query(`
139
- WITH ins AS (
140
- INSERT INTO hub.user (casino_id, mp_user_id, uname)
141
- VALUES ($1, $2, $3)
142
- ON CONFLICT (casino_id, mp_user_id) DO NOTHING
143
- RETURNING *
144
- )
145
- SELECT * FROM ins
146
- UNION ALL
147
- SELECT * FROM hub.user
148
- WHERE casino_id = $1 AND mp_user_id = $2
149
- LIMIT 1;
139
+ WITH upsert AS (
140
+ INSERT INTO hub.user (casino_id, mp_user_id, uname)
141
+ VALUES ($1, $2, $3)
142
+ ON CONFLICT (casino_id, mp_user_id) DO UPDATE
143
+ SET uname = EXCLUDED.uname
144
+ WHERE hub.user.uname IS DISTINCT FROM EXCLUDED.uname
145
+ RETURNING *
146
+ )
147
+ SELECT * FROM upsert
148
+ UNION ALL
149
+ SELECT * FROM hub.user
150
+ WHERE casino_id = $1 AND mp_user_id = $2
151
+ AND NOT EXISTS (SELECT 1 FROM upsert)
152
+ LIMIT 1;
153
+
150
154
  `, [casinoId, mpUserId, uname])
151
155
  .then(exactlyOneRow);
152
156
  return user;
@@ -1,9 +1,10 @@
1
1
  import { DbHashKind, } from "../db/types.js";
2
2
  import { exactlyOneRow, maybeOneRow, } from "../db/index.js";
3
3
  import { assert } from "tsafe";
4
+ import { logger } from "../logger.js";
4
5
  export async function dbLockHubHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
5
6
  assert(pgClient._inTransaction, "dbLockHubHashChain must be called in a transaction");
6
- return pgClient
7
+ const hashChain = await pgClient
7
8
  .query(`
8
9
  SELECT *
9
10
  FROM hub.hash_chain
@@ -17,10 +18,24 @@ export async function dbLockHubHashChain(pgClient, { userId, experienceId, casin
17
18
  `, [hashChainId, userId, experienceId, casinoId])
18
19
  .then(maybeOneRow)
19
20
  .then((row) => row ?? null);
21
+ if (hashChain) {
22
+ assert(hashChain.current_iteration >= 1, "Hash chain iteration must be >= 1");
23
+ }
24
+ return hashChain;
20
25
  }
21
26
  export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, iteration, clientSeed, metadata = {}, }) {
27
+ logger.debug({
28
+ hashChainId,
29
+ kind,
30
+ iteration,
31
+ clientSeed,
32
+ metadata,
33
+ digest: Buffer.from(digest).toString("base64"),
34
+ }, "[dbInsertHubHash] Inserting hash");
22
35
  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");
36
+ if (kind === DbHashKind.INTERMEDIATE) {
37
+ assert(typeof clientSeed === "string", "clientSeed must be provided for INTERMEDIATE hashes");
38
+ }
24
39
  return pgClient
25
40
  .query(`
26
41
  INSERT INTO hub.hash (hash_chain_id, kind, digest, iteration, client_seed, metadata)
@@ -65,9 +65,12 @@ export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
65
65
  config.HASHCHAINSERVER_MAX_ITERATIONS,
66
66
  ])
67
67
  .then(exactlyOneRow);
68
- const terminalHash = await HashCommon.getTerminalHash({
68
+ const terminalHashResult = await HashCommon.getTerminalHash({
69
69
  hashChainId: dbHashChain.id,
70
70
  });
71
+ if (terminalHashResult.type !== "success") {
72
+ throw new Error("Failed to get terminal hash");
73
+ }
71
74
  await pgClient.query(`
72
75
  INSERT INTO hub.hash (
73
76
  hash_chain_id,
@@ -79,7 +82,7 @@ export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
79
82
  `, [
80
83
  dbHashChain.id,
81
84
  DbHashKind.TERMINAL,
82
- terminalHash,
85
+ terminalHashResult.hash,
83
86
  config.HASHCHAINSERVER_MAX_ITERATIONS,
84
87
  ]);
85
88
  return dbHashChain.id;
@@ -0,0 +1 @@
1
+ export declare const HubRevealHashChainPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,74 @@
1
+ import { context, object, sideEffect } from "postgraphile/grafast";
2
+ import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
+ import { dbLockHubHashChain, withPgPoolTransaction } from "../../db/index.js";
4
+ import z, { prettifyError } from "zod/v4";
5
+ import { GraphQLError } from "graphql";
6
+ import { assert } from "tsafe";
7
+ import { dbRevealHashChain } from "../reveal-hash-chain.js";
8
+ const InputSchema = z.object({
9
+ hashChainId: z.uuidv7("Invalid hash chain ID"),
10
+ });
11
+ export const HubRevealHashChainPlugin = makeExtendSchemaPlugin((build) => {
12
+ const hashTable = build.input.pgRegistry.pgResources.hub_hash;
13
+ return {
14
+ typeDefs: gql `
15
+ input HubRevealHashChainInput {
16
+ hashChainId: UUID!
17
+ }
18
+
19
+ type HubRevealHashChainPayload {
20
+ preimageHash: HubHash!
21
+ }
22
+
23
+ extend type Mutation {
24
+ hubRevealHashChain(
25
+ input: HubRevealHashChainInput!
26
+ ): HubRevealHashChainPayload!
27
+ }
28
+ `,
29
+ objects: {
30
+ Mutation: {
31
+ plans: {
32
+ hubRevealHashChain: (_, { $input }) => {
33
+ const $identity = context().get("identity");
34
+ const $superuserPool = context().get("superuserPool");
35
+ const $preimageHashId = sideEffect([$input, $identity, $superuserPool], ([rawInput, identity, superuserPool]) => {
36
+ if (identity?.kind !== "user") {
37
+ throw new GraphQLError("Unauthorized");
38
+ }
39
+ let input;
40
+ try {
41
+ input = InputSchema.parse(rawInput);
42
+ }
43
+ catch (e) {
44
+ if (e instanceof z.ZodError) {
45
+ throw new GraphQLError(prettifyError(e));
46
+ }
47
+ throw e;
48
+ }
49
+ return withPgPoolTransaction(superuserPool, async (pgClient) => {
50
+ const hashChain = await dbLockHubHashChain(pgClient, {
51
+ userId: identity.session.user_id,
52
+ experienceId: identity.session.experience_id,
53
+ casinoId: identity.session.casino_id,
54
+ hashChainId: input.hashChainId,
55
+ });
56
+ if (!hashChain || !hashChain.active) {
57
+ throw new GraphQLError("Active hash chain not found");
58
+ }
59
+ assert(hashChain.current_iteration >= 1, "Invalid hash chain iteration");
60
+ const dbPreimageHash = await dbRevealHashChain(pgClient, {
61
+ hashChainId: input.hashChainId,
62
+ });
63
+ return dbPreimageHash.id;
64
+ });
65
+ });
66
+ return object({
67
+ preimageHash: hashTable.get({ id: $preimageHashId }),
68
+ });
69
+ },
70
+ },
71
+ },
72
+ },
73
+ };
74
+ });
@@ -0,0 +1,5 @@
1
+ import { DbHash } from "../db/types.js";
2
+ import { PgClientInTransaction } from "../db/index.js";
3
+ export declare function dbRevealHashChain(pgClient: PgClientInTransaction, { hashChainId, }: {
4
+ hashChainId: string;
5
+ }): Promise<DbHash>;
@@ -0,0 +1,35 @@
1
+ import { logger } from "../logger.js";
2
+ import { getPreimageHash } from "./get-hash.js";
3
+ import { DbHashKind } from "../db/types.js";
4
+ import { dbInsertHubHash } from "../db/index.js";
5
+ import { assert } from "tsafe";
6
+ export async function dbRevealHashChain(pgClient, { hashChainId, }) {
7
+ assert(pgClient._inTransaction, "dbRevealHashChain must be called in a transaction");
8
+ logger.debug({ hashChainId }, "Revealing hash chain");
9
+ const preimageHashResult = await getPreimageHash({
10
+ hashChainId,
11
+ });
12
+ logger.debug({ preimageHashResult }, "Preimage hash result");
13
+ if (preimageHashResult.type === "success") {
14
+ const dbPreimageHash = await dbInsertHubHash(pgClient, {
15
+ hashChainId,
16
+ kind: DbHashKind.PREIMAGE,
17
+ digest: preimageHashResult.hash,
18
+ iteration: 0,
19
+ });
20
+ const result = await pgClient.query(`
21
+ UPDATE hub.hash_chain
22
+ SET current_iteration = 0,
23
+ active = false
24
+ WHERE id = $1
25
+ `, [hashChainId]);
26
+ if (result.rowCount !== 1) {
27
+ throw new Error("Failed to update hash chain iteration");
28
+ }
29
+ return dbPreimageHash;
30
+ }
31
+ else {
32
+ logger.warn({ hashChainId, error: preimageHashResult }, "Failed to reveal hash chain");
33
+ throw new Error("Failed to reveal hash chain");
34
+ }
35
+ }
@@ -69,6 +69,7 @@ CREATE POLICY select_hash_chain ON hub.hash_chain FOR SELECT USING (
69
69
  )
70
70
  );
71
71
 
72
+ -- TODO: Since we have RLS we could just make hashes public for simplicity/perf
72
73
  CREATE POLICY select_hash ON hub.hash FOR SELECT USING (
73
74
  -- Operator can see all rows
74
75
  hub_hidden.is_operator() OR
@@ -5,11 +5,12 @@ import { GraphQLError } from "graphql";
5
5
  import { DbHashKind, dbLockPlayerBalanceAndHouseBankroll, exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../db/index.js";
6
6
  import { assert } from "tsafe";
7
7
  import { dbInsertHubHash, dbLockHubHashChain, } from "../hash-chain/db-hash-chain.js";
8
- import { getIntermediateHash, getPreimageHash, } from "../hash-chain/get-hash.js";
8
+ import { getIntermediateHash } from "../hash-chain/get-hash.js";
9
9
  import { makeFinalHash, pickRandomOutcome } from "../hash-chain/util.js";
10
10
  import { logger } from "../logger.js";
11
11
  import { validateRisk } from "../risk-policy.js";
12
12
  import { insertAuditLog } from "../audit-log.js";
13
+ import { dbRevealHashChain } from "../hash-chain/reveal-hash-chain.js";
13
14
  const FLOAT_EPSILON = 1e-10;
14
15
  function sum(ns) {
15
16
  return ns.reduce((a, b) => a + b, 0);
@@ -215,15 +216,24 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
215
216
  message: "Active hash chain not found",
216
217
  };
217
218
  }
218
- if (dbHashChain.current_iteration <= 1) {
219
- if (dbHashChain.current_iteration === 1) {
220
- finishHashChainInBackground({
221
- hashChainId: input.hashChainId,
222
- pool: superuserPool,
223
- }).catch((e) => {
224
- logger.error({ hashChainId: input.hashChainId, error: e }, "Error finishing hash chain in background");
225
- });
226
- }
219
+ if (dbHashChain.current_iteration < 1) {
220
+ logger.error({
221
+ hashChainId: input.hashChainId,
222
+ iteration: dbHashChain.current_iteration,
223
+ }, "Hash chain has invalid iteration < 1");
224
+ return {
225
+ __typename: "HubBadHashChainError",
226
+ message: "Hash chain is in an invalid state",
227
+ };
228
+ }
229
+ if (dbHashChain.current_iteration === 1) {
230
+ await pgClient.query(`UPDATE hub.hash_chain SET active = false WHERE id = $1`, [input.hashChainId]);
231
+ revealHashChainInBackground({
232
+ hashChainId: input.hashChainId,
233
+ pool: superuserPool,
234
+ }).catch((e) => {
235
+ logger.error({ hashChainId: input.hashChainId, error: e }, "Error finishing hash chain in background");
236
+ });
227
237
  return {
228
238
  __typename: "HubBadHashChainError",
229
239
  message: "Hash chain drained. Create a new one.",
@@ -413,38 +423,8 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
413
423
  };
414
424
  }, "HubMakeOutcomeBetPlugin");
415
425
  }
416
- async function finishHashChainInBackground({ hashChainId, pool, }) {
417
- logger.debug({ hashChainId }, "Finishing hash chain in background");
418
- const preimageHashResult = await getPreimageHash({
419
- hashChainId,
426
+ async function revealHashChainInBackground({ hashChainId, pool, }) {
427
+ return await withPgPoolTransaction(pool, async (pgClient) => {
428
+ await dbRevealHashChain(pgClient, { hashChainId });
420
429
  });
421
- logger.debug({ preimageHashResult }, "Preimage hash result");
422
- if (preimageHashResult.type === "success") {
423
- logger.debug({
424
- hashChainId,
425
- kind: DbHashKind.PREIMAGE,
426
- digest: preimageHashResult.hash,
427
- iteration: 0,
428
- }, "Inserting preimage hash");
429
- await withPgPoolTransaction(pool, async (pgClient) => {
430
- await dbInsertHubHash(pgClient, {
431
- hashChainId,
432
- kind: DbHashKind.PREIMAGE,
433
- digest: preimageHashResult.hash,
434
- iteration: 0,
435
- });
436
- const result = await pgClient.query(`
437
- UPDATE hub.hash_chain
438
- SET current_iteration = 0,
439
- active = false
440
- WHERE id = $1
441
- `, [hashChainId]);
442
- if (result.rowCount !== 1) {
443
- throw new Error("Failed to update hash chain iteration");
444
- }
445
- });
446
- }
447
- else {
448
- logger.warn({ hashChainId, error: preimageHashResult }, "Failed to insert preimage hash in background");
449
- }
450
430
  }
@@ -23,6 +23,7 @@ import { HubBadHashChainErrorPlugin } from "../hash-chain/plugins/hub-bad-hash-c
23
23
  import { HubUserActiveHashChainPlugin } from "../hash-chain/plugins/hub-user-active-hash-chain.js";
24
24
  import { HubOutcomeInputNonNullFieldsPlugin } from "../plugins/hub-outcome-input-non-null-fields.js";
25
25
  import { HubPutAlertPlugin } from "../plugins/hub-put-alert.js";
26
+ import { HubRevealHashChainPlugin } from "../hash-chain/plugins/hub-reveal-hash-chain.js";
26
27
  export const requiredPlugins = [
27
28
  SmartTagsPlugin,
28
29
  IdToNodeIdPlugin,
@@ -42,6 +43,7 @@ export const defaultPlugins = [
42
43
  HubBadHashChainErrorPlugin,
43
44
  HubCreateHashChainPlugin,
44
45
  HubUserActiveHashChainPlugin,
46
+ HubRevealHashChainPlugin,
45
47
  customPgOmitArchivedPlugin("deleted"),
46
48
  ];
47
49
  export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [