@moneypot/hub 1.3.0-dev.1 → 1.3.0-dev.11

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.
Files changed (31) hide show
  1. package/dist/src/config.d.ts +8 -13
  2. package/dist/src/config.js +78 -52
  3. package/dist/src/db/index.d.ts +1 -0
  4. package/dist/src/db/index.js +2 -1
  5. package/dist/src/db/types.d.ts +28 -0
  6. package/dist/src/db/types.js +5 -1
  7. package/dist/src/hash-chain/db-hash-chain.d.ts +15 -0
  8. package/dist/src/hash-chain/db-hash-chain.js +35 -0
  9. package/dist/src/hash-chain/get-hash.d.ts +17 -0
  10. package/dist/src/hash-chain/get-hash.js +57 -0
  11. package/dist/src/hash-chain/plugins/hub-bad-hash-chain-error.d.ts +1 -0
  12. package/dist/src/hash-chain/plugins/hub-bad-hash-chain-error.js +20 -0
  13. package/dist/src/hash-chain/plugins/hub-create-hash-chain.d.ts +1 -0
  14. package/dist/src/hash-chain/plugins/hub-create-hash-chain.js +111 -0
  15. package/dist/src/hash-chain/plugins/hub-user-active-hash-chain.d.ts +1 -0
  16. package/dist/src/hash-chain/plugins/hub-user-active-hash-chain.js +46 -0
  17. package/dist/src/index.d.ts +1 -1
  18. package/dist/src/index.js +2 -2
  19. package/dist/src/pg-advisory-lock.d.ts +9 -4
  20. package/dist/src/pg-advisory-lock.js +8 -1
  21. package/dist/src/pg-versions/005-hash-chain.sql +84 -0
  22. package/dist/src/pg-versions/{005-outcome-bet.sql → 006-outcome-bet.sql} +8 -1
  23. package/dist/src/plugins/hub-authenticate.js +24 -3
  24. package/dist/src/plugins/hub-make-outcome-bet.d.ts +32 -5
  25. package/dist/src/plugins/hub-make-outcome-bet.js +175 -23
  26. package/dist/src/process-transfers.js +1 -1
  27. package/dist/src/server/graphile.config.js +7 -1
  28. package/dist/src/server/handle-errors.js +1 -1
  29. package/dist/src/server/index.js +1 -1
  30. package/dist/src/take-request/process-take-request.js +3 -3
  31. package/package.json +1 -1
@@ -1,14 +1,9 @@
1
1
  import "dotenv/config";
2
- export interface Config {
3
- NODE_ENV: string;
4
- PORT: number;
5
- DATABASE_URL: string;
6
- SUPERUSER_DATABASE_URL: string;
7
- MP_GRAPHQL_URL: string;
8
- HASHCHAINSERVER_OPTS: {
9
- baseUrl: string;
10
- applicationSecret: string;
11
- };
12
- }
13
- declare const config: Config;
14
- export default config;
2
+ export declare const NODE_ENV: string;
3
+ export declare const PORT: number;
4
+ export declare const MP_GRAPHQL_URL: string;
5
+ export declare const DATABASE_URL: string;
6
+ export declare const SUPERUSER_DATABASE_URL: string;
7
+ export declare const HASHCHAINSERVER_URL: string;
8
+ export declare const HASHCHAINSERVER_MAX_ITERATIONS: number;
9
+ export declare const HASHCHAINSERVER_APPLICATION_SECRET: string;
@@ -1,57 +1,83 @@
1
1
  import "dotenv/config";
2
2
  import pgConnectionString from "pg-connection-string";
3
3
  import { logger } from "./logger.js";
4
- let NODE_ENV = process.env.NODE_ENV || "";
5
- if (!NODE_ENV) {
6
- NODE_ENV = "development";
7
- logger.warn("Missing NODE_ENV env var. Defaulting to 'development'");
4
+ import { assert } from "tsafe";
5
+ function getEnvVariable(key, transform = (value) => value) {
6
+ return transform(process.env[key] || "");
8
7
  }
9
- let PORT = Number.parseInt(process.env.PORT || "", 10);
10
- if (!PORT || PORT <= 0 || !Number.isSafeInteger(PORT)) {
11
- PORT = 4000;
12
- logger.warn("Warning: PORT missing or invalid, defaulting to ", PORT);
13
- }
14
- let MP_GRAPHQL_URL = process.env.MP_GRAPHQL_URL || "";
15
- if (!MP_GRAPHQL_URL) {
16
- MP_GRAPHQL_URL = "http://localhost:3000/graphql";
17
- logger.warn(`Missing MP_GRAPHQL_URL env var. Defaulting to ${MP_GRAPHQL_URL}`);
18
- }
19
- if (!MP_GRAPHQL_URL.includes("/graphql")) {
20
- logger.warn(`MP_GRAPHQL_URL didn't include '/graphql'. Are you sure it points to a graphql endpoint?`);
21
- }
22
- const DATABASE_URL = process.env.DATABASE_URL || "";
23
- if (!DATABASE_URL) {
24
- throw new Error(`Missing DATABASE_URL env var.`);
25
- }
26
- const databaseUrlUsername = pgConnectionString.parse(DATABASE_URL).user;
27
- if (databaseUrlUsername !== "app_postgraphile") {
28
- logger.warn(`DATABASE_URL username is ${databaseUrlUsername}, expected app_postgraphile`);
29
- }
30
- const SUPERUSER_DATABASE_URL = process.env.SUPERUSER_DATABASE_URL || "";
31
- if (!SUPERUSER_DATABASE_URL) {
32
- throw new Error("SUPERUSER_DATABASE_URL env var is required");
33
- }
34
- const HASHCHAINSERVER_URL = process.env.HASHCHAINSERVER_URL || "mock-server";
35
- if (HASHCHAINSERVER_URL == "mock-server") {
36
- logger.warn(`Missing HASHCHAINSERVER_URL env var, defaulting to mock-server.`);
37
- if (NODE_ENV !== "development") {
38
- logger.warn("Missing HASHCHAINSERVER_URL and NODE_ENV != 'development': You're using the hashchain mock-server outside of development. (If you aren't using the hashchain server, you can ignore this.)");
8
+ export const NODE_ENV = getEnvVariable("NODE_ENV", (value) => {
9
+ if (!value) {
10
+ logger.warn("Missing NODE_ENV env var. Defaulting to 'development'");
11
+ return "development";
39
12
  }
40
- }
41
- const HASHCHAINSERVER_APPLICATION_SECRET = process.env.HASHCHAINSERVER_APPLICATION_SECRET || "";
42
- if (!HASHCHAINSERVER_APPLICATION_SECRET && NODE_ENV !== "development") {
43
- logger.warn("Missing HASHCHAINSERVER_APPLICATION_SECRET and NODE_ENV != 'development': To use the hashchain server you must pick a random (but stable) HASHCHAINSERVER_APPLICATION_SECRET for secure communciation with it. (If you aren't using the hashchain server, you can ignore this.)");
44
- }
45
- const HASHCHAINSERVER_OPTS = {
46
- baseUrl: HASHCHAINSERVER_URL,
47
- applicationSecret: HASHCHAINSERVER_APPLICATION_SECRET,
48
- };
49
- const config = {
50
- NODE_ENV,
51
- PORT,
52
- DATABASE_URL,
53
- SUPERUSER_DATABASE_URL,
54
- MP_GRAPHQL_URL,
55
- HASHCHAINSERVER_OPTS,
56
- };
57
- export default config;
13
+ return value;
14
+ });
15
+ export const PORT = getEnvVariable("PORT", (value) => {
16
+ const parsed = Number.parseInt(value, 10);
17
+ if (!parsed || parsed <= 0 || !Number.isSafeInteger(parsed)) {
18
+ logger.warn("Warning: PORT missing or invalid, defaulting to ", parsed);
19
+ return 4000;
20
+ }
21
+ return parsed;
22
+ });
23
+ export const MP_GRAPHQL_URL = getEnvVariable("MP_GRAPHQL_URL", (value) => {
24
+ if (!value) {
25
+ logger.warn("Missing MP_GRAPHQL_URL env var. Defaulting to http://localhost:3000/graphql");
26
+ return "http://localhost:3000/graphql";
27
+ }
28
+ if (!URL.parse(value)) {
29
+ logger.warn("MP_GRAPHQL_URL is not a valid URL. Defaulting to http://localhost:3000/graphql");
30
+ }
31
+ const url = new URL(value);
32
+ if (url.pathname !== "/graphql") {
33
+ logger.warn("MP_GRAPHQL_URL pathname is not '/graphql'. Are you sure it points to a graphql endpoint?");
34
+ }
35
+ return value;
36
+ });
37
+ export const DATABASE_URL = getEnvVariable("DATABASE_URL", (value) => {
38
+ if (!value) {
39
+ throw new Error(`Missing DATABASE_URL env var.`);
40
+ }
41
+ if (!URL.parse(value)) {
42
+ logger.warn("DATABASE_URL is not a valid URL.");
43
+ }
44
+ const databaseUrlUsername = pgConnectionString.parse(value).user;
45
+ if (databaseUrlUsername !== "app_postgraphile") {
46
+ logger.warn(`DATABASE_URL username is ${databaseUrlUsername}, expected app_postgraphile`);
47
+ }
48
+ return value;
49
+ });
50
+ export const SUPERUSER_DATABASE_URL = getEnvVariable("SUPERUSER_DATABASE_URL", (value) => {
51
+ if (!value) {
52
+ throw new Error("SUPERUSER_DATABASE_URL env var is required");
53
+ }
54
+ if (!URL.parse(value)) {
55
+ logger.warn("SUPERUSER_DATABASE_URL is not a valid URL.");
56
+ }
57
+ return value;
58
+ });
59
+ export const HASHCHAINSERVER_URL = getEnvVariable("HASHCHAINSERVER_URL", (value) => {
60
+ value = value || "mock-server";
61
+ if (value === "mock-server") {
62
+ logger.warn("Using mock-server for HASHCHAINSERVER_URL. This is only allowed in development.");
63
+ }
64
+ else if (!URL.parse(value)) {
65
+ logger.warn("HASHCHAINSERVER_URL is not a valid URL. It can either be empty, 'mock-server', or URL but it was: " +
66
+ value);
67
+ }
68
+ if (NODE_ENV !== "development" && value === "mock-server") {
69
+ logger.warn("Using mock-server for HASHCHAINSERVER_URL. This is only allowed in development.");
70
+ }
71
+ return value;
72
+ });
73
+ export const HASHCHAINSERVER_MAX_ITERATIONS = getEnvVariable("HASHCHAINSERVER_MAX_ITERATIONS", (value) => {
74
+ const iterations = Number.parseInt(value, 10) || 1_000;
75
+ assert(iterations >= 10, "HASHCHAINSERVER_MAX_ITERATIONS must be >= 10");
76
+ return iterations;
77
+ });
78
+ export const HASHCHAINSERVER_APPLICATION_SECRET = getEnvVariable("HASHCHAINSERVER_APPLICATION_SECRET", (value) => {
79
+ if (!value && NODE_ENV !== "development") {
80
+ logger.warn("Missing HASHCHAINSERVER_APPLICATION_SECRET and NODE_ENV != 'development': To use the hashchain server you must pick a random (but stable) HASHCHAINSERVER_APPLICATION_SECRET for secure communciation with it. (If you aren't using the hashchain server, you can ignore this.)");
81
+ }
82
+ return value || "";
83
+ });
@@ -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";
@@ -1,10 +1,11 @@
1
1
  import * as pg from "pg";
2
- import config from "../config.js";
2
+ import * as config from "../config.js";
3
3
  import stream from "node:stream";
4
4
  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";
@@ -114,3 +114,31 @@ export type DbTakeRequest = {
114
114
  transfer_completion_attempted_at: Date | null;
115
115
  updated_at: Date;
116
116
  };
117
+ export type DbHashChain = {
118
+ id: string;
119
+ user_id: string;
120
+ experience_id: string;
121
+ casino_id: string;
122
+ client_seed: string;
123
+ max_iterations: number;
124
+ current_iteration: number;
125
+ active: boolean;
126
+ };
127
+ export declare const DbHashKind: {
128
+ readonly TERMINAL: "TERMINAL";
129
+ readonly INTERMEDIATE: "INTERMEDIATE";
130
+ readonly PREIMAGE: "PREIMAGE";
131
+ };
132
+ export type DbHashKind = (typeof DbHashKind)[keyof typeof DbHashKind];
133
+ export type DbHash = {
134
+ id: string;
135
+ kind: DbHashKind;
136
+ hash_chain_id: string;
137
+ iteration: number;
138
+ digest: Uint8Array;
139
+ metadata: Record<string, unknown>;
140
+ };
141
+ export type DbHubOutcome = {
142
+ weight: number;
143
+ profit: number;
144
+ };
@@ -1 +1,5 @@
1
- export {};
1
+ export const DbHashKind = {
2
+ TERMINAL: "TERMINAL",
3
+ INTERMEDIATE: "INTERMEDIATE",
4
+ PREIMAGE: "PREIMAGE",
5
+ };
@@ -0,0 +1,15 @@
1
+ import { DbCasino, DbExperience, DbHash, DbHashChain, DbUser } from "../db/types.js";
2
+ import { PgClientInTransaction } from "../db/index.js";
3
+ export declare function dbLockHubHashChain(pgClient: PgClientInTransaction, { userId, experienceId, casinoId, hashChainId, }: {
4
+ userId: DbUser["id"];
5
+ experienceId: DbExperience["id"];
6
+ casinoId: DbCasino["id"];
7
+ hashChainId: DbHashChain["id"];
8
+ }): Promise<DbHashChain | null>;
9
+ export declare function dbInsertHubHash(pgClient: PgClientInTransaction, { hashChainId, kind, digest, iteration, metadata, }: {
10
+ hashChainId: DbHashChain["id"];
11
+ kind: DbHash["kind"];
12
+ digest: DbHash["digest"];
13
+ iteration: number;
14
+ metadata?: DbHash["metadata"];
15
+ }): Promise<DbHash>;
@@ -0,0 +1,35 @@
1
+ import { exactlyOneRow, maybeOneRow, } from "../db/index.js";
2
+ import { assert } from "tsafe";
3
+ export async function dbLockHubHashChain(pgClient, { userId, experienceId, casinoId, hashChainId, }) {
4
+ assert(pgClient._inTransaction, "dbLockHashChain must be called in a transaction");
5
+ return pgClient
6
+ .query(`
7
+ SELECT *
8
+ FROM hub.hash_chain
9
+ WHERE id = $1
10
+ AND user_id = $2
11
+ AND experience_id = $3
12
+ AND casino_id = $4
13
+ AND active = TRUE
14
+
15
+ FOR UPDATE
16
+ `, [hashChainId, userId, experienceId, casinoId])
17
+ .then(maybeOneRow)
18
+ .then((row) => row ?? null);
19
+ }
20
+ export async function dbInsertHubHash(pgClient, { hashChainId, kind, digest, iteration, metadata = {}, }) {
21
+ assert(pgClient._inTransaction, "dbInsertHash must be called in a transaction");
22
+ return pgClient
23
+ .query(`
24
+ INSERT INTO hub.hash (hash_chain_id, kind, digest, iteration, metadata)
25
+ VALUES ($1, $2, $3, $4, $5)
26
+ RETURNING *
27
+ `, [
28
+ hashChainId,
29
+ kind,
30
+ digest,
31
+ iteration,
32
+ metadata,
33
+ ])
34
+ .then(exactlyOneRow);
35
+ }
@@ -0,0 +1,17 @@
1
+ export type HashResult = {
2
+ type: "bad_hash_chain";
3
+ reason: "hashchain_too_old" | "empty_response";
4
+ } | {
5
+ type: "success";
6
+ hash: Uint8Array;
7
+ };
8
+ export declare function getIntermediateHash({ hashChainId, iteration, }: {
9
+ hashChainId: string;
10
+ iteration: number;
11
+ }): Promise<HashResult>;
12
+ export declare function getPreimageHash({ hashChainId, }: {
13
+ hashChainId: string;
14
+ }): Promise<HashResult>;
15
+ export declare function getTerminalHash({ hashChainId, }: {
16
+ hashChainId: string;
17
+ }): Promise<HashResult>;
@@ -0,0 +1,57 @@
1
+ import { getHash } from "@moneypot/hash-herald";
2
+ import * as config from "../config.js";
3
+ const HASH_HERALD_OPTIONS = {
4
+ baseUrl: config.HASHCHAINSERVER_URL,
5
+ applicationSecret: config.HASHCHAINSERVER_APPLICATION_SECRET,
6
+ };
7
+ function resultFromGetHashResponse(response) {
8
+ if (!response.resp) {
9
+ return { type: "bad_hash_chain", reason: "empty_response" };
10
+ }
11
+ const $case = response.resp.$case;
12
+ switch ($case) {
13
+ case "hash":
14
+ return { type: "success", hash: response.resp.value };
15
+ case "hashchainTooOldError":
16
+ return { type: "bad_hash_chain", reason: "hashchain_too_old" };
17
+ default: {
18
+ const _exhaustiveCheck = $case;
19
+ throw new Error(`Unknown hash response: ${_exhaustiveCheck}`);
20
+ }
21
+ }
22
+ }
23
+ export async function getIntermediateHash({ hashChainId, iteration, }) {
24
+ const req = {
25
+ hashchainId: hashChainId,
26
+ iterations: iteration,
27
+ context: undefined,
28
+ };
29
+ return getHash(HASH_HERALD_OPTIONS, req).then(resultFromGetHashResponse);
30
+ }
31
+ export async function getPreimageHash({ hashChainId, }) {
32
+ const req = {
33
+ hashchainId: hashChainId,
34
+ iterations: 0,
35
+ context: {
36
+ event: {
37
+ $case: "fetchingPreimage",
38
+ value: {},
39
+ },
40
+ },
41
+ };
42
+ return getHash(HASH_HERALD_OPTIONS, req).then(resultFromGetHashResponse);
43
+ }
44
+ export async function getTerminalHash({ hashChainId, }) {
45
+ const iterations = 1_000;
46
+ const req = {
47
+ hashchainId: hashChainId,
48
+ iterations,
49
+ context: {
50
+ event: {
51
+ $case: "fetchingTerminalHash",
52
+ value: {},
53
+ },
54
+ },
55
+ };
56
+ return getHash(HASH_HERALD_OPTIONS, req).then(resultFromGetHashResponse);
57
+ }
@@ -0,0 +1 @@
1
+ export declare const HubBadHashChainErrorPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,20 @@
1
+ import { ObjectStep, access } from "postgraphile/grafast";
2
+ import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
+ export const HubBadHashChainErrorPlugin = makeExtendSchemaPlugin(() => {
4
+ return {
5
+ typeDefs: gql `
6
+ type HubBadHashChainError {
7
+ message: String
8
+ }
9
+ `,
10
+ plans: {
11
+ HubBadHashChainError: {
12
+ __assertStep: ObjectStep,
13
+ message($data) {
14
+ const $message = access($data, "message");
15
+ return $message;
16
+ },
17
+ },
18
+ },
19
+ };
20
+ }, "HubBadHashChainErrorPlugin");
@@ -0,0 +1 @@
1
+ export declare const HubCreateHashChainPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,111 @@
1
+ import { context, object, sideEffect } from "@moneypot/hub/grafast";
2
+ import { gql, makeExtendSchemaPlugin } from "@moneypot/hub/graphile";
3
+ import { GraphQLError } from "graphql";
4
+ import { z } from "zod";
5
+ import { PgAdvisoryLock } from "../../pg-advisory-lock.js";
6
+ import { exactlyOneRow, superuserPool, withPgPoolTransaction, } from "@moneypot/hub/db";
7
+ import * as HashCommon from "../get-hash.js";
8
+ import { DbHashKind } from "../../db/types.js";
9
+ import * as config from "../../config.js";
10
+ const InputSchema = z.object({
11
+ clientSeed: z.string(),
12
+ });
13
+ export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
14
+ const hashChainTable = build.input.pgRegistry.pgResources.hub_hash_chain;
15
+ return {
16
+ typeDefs: gql `
17
+ input HubCreateHashChainInput {
18
+ clientSeed: String!
19
+ }
20
+
21
+ type HubCreateHashChainPayload {
22
+ hashChain: HubHashChain!
23
+ }
24
+
25
+ extend type Mutation {
26
+ hubCreateHashChain(
27
+ input: HubCreateHashChainInput!
28
+ ): HubCreateHashChainPayload!
29
+ }
30
+ `,
31
+ plans: {
32
+ Mutation: {
33
+ hubCreateHashChain: (_, { $input }) => {
34
+ const $identity = context().get("identity");
35
+ const $hashChainId = sideEffect([$input, $identity], ([rawInput, identity]) => {
36
+ if (identity?.kind !== "user") {
37
+ throw new GraphQLError("Unauthorized");
38
+ }
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
+ return withPgPoolTransaction(superuserPool, async (pgClient) => {
46
+ await PgAdvisoryLock.forNewHashChain(pgClient, {
47
+ userId: identity.session.user_id,
48
+ experienceId: identity.session.experience_id,
49
+ casinoId: identity.session.casino_id,
50
+ });
51
+ await pgClient.query(`
52
+ UPDATE hub.hash_chain
53
+ SET active = false
54
+ WHERE user_id = $1
55
+ AND experience_id = $2
56
+ AND casino_id = $3
57
+ AND active = true
58
+ `, [
59
+ identity.session.user_id,
60
+ identity.session.experience_id,
61
+ identity.session.casino_id,
62
+ ]);
63
+ const dbHashChain = await pgClient
64
+ .query(`
65
+ INSERT INTO hub.hash_chain (
66
+ user_id,
67
+ experience_id,
68
+ casino_id,
69
+ client_seed,
70
+ active,
71
+ max_iteration,
72
+ current_iteration
73
+ )
74
+ VALUES ($1, $2, $3, $4, true, $5, $5)
75
+ RETURNING *
76
+ `, [
77
+ identity.session.user_id,
78
+ identity.session.experience_id,
79
+ identity.session.casino_id,
80
+ clientSeed,
81
+ config.HASHCHAINSERVER_MAX_ITERATIONS,
82
+ ])
83
+ .then(exactlyOneRow);
84
+ const terminalHash = await HashCommon.getTerminalHash({
85
+ hashChainId: dbHashChain.id,
86
+ });
87
+ await pgClient.query(`
88
+ INSERT INTO hub.hash (
89
+ hash_chain_id,
90
+ kind,
91
+ digest,
92
+ iteration
93
+ )
94
+ VALUES ($1, $2, $3, $4)
95
+ `, [
96
+ dbHashChain.id,
97
+ DbHashKind.TERMINAL,
98
+ terminalHash,
99
+ config.HASHCHAINSERVER_MAX_ITERATIONS,
100
+ ]);
101
+ return dbHashChain.id;
102
+ });
103
+ });
104
+ return object({
105
+ hashChain: hashChainTable.get({ id: $hashChainId }),
106
+ });
107
+ },
108
+ },
109
+ },
110
+ };
111
+ }, "CreateHashChainPlugin");
@@ -0,0 +1 @@
1
+ export declare const HubUserActiveHashChainPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,46 @@
1
+ import { maybeOneRow } from "@moneypot/hub/db";
2
+ import { inhibitOnNull } from "@moneypot/hub/grafast";
3
+ import { context } from "@moneypot/hub/grafast";
4
+ import { gql, makeExtendSchemaPlugin } from "@moneypot/hub/graphile";
5
+ import { withPgClient } from "postgraphile/@dataplan/pg";
6
+ import { object } from "postgraphile/grafast";
7
+ export const HubUserActiveHashChainPlugin = makeExtendSchemaPlugin((build) => {
8
+ const hashChainTable = build.input.pgRegistry.pgResources.hub_hash_chain;
9
+ return {
10
+ typeDefs: gql `
11
+ extend type HubUser {
12
+ activeHashChain: HubHashChain
13
+ }
14
+ `,
15
+ plans: {
16
+ HubUser: {
17
+ activeHashChain: ($record) => {
18
+ const $identity = context().get("identity");
19
+ const $hashChainId = withPgClient(hashChainTable.executor, object({ userId: $record.get("id"), identity: $identity }), async (pgClient, { userId, identity }) => {
20
+ if (identity?.kind !== "user") {
21
+ return null;
22
+ }
23
+ const { session } = identity;
24
+ const activeHashChain = await pgClient
25
+ .query({
26
+ text: `
27
+ select id
28
+ from hub.hash_chain
29
+ where user_id = $1
30
+ and experience_id = $2
31
+ and casino_id = $3
32
+ and active = TRUE
33
+ order by id desc
34
+ limit 1
35
+ `,
36
+ values: [userId, session.experience_id, session.casino_id],
37
+ })
38
+ .then(maybeOneRow);
39
+ return activeHashChain?.id ?? null;
40
+ });
41
+ return hashChainTable.get({ id: inhibitOnNull($hashChainId) });
42
+ },
43
+ },
44
+ },
45
+ };
46
+ }, "HubActiveHashChainPlugin");
@@ -1,6 +1,6 @@
1
1
  import { Express } from "express";
2
2
  import { Logger } from "./logger.js";
3
- export { MakeOutcomeBetPlugin } from "./plugins/hub-make-outcome-bet.js";
3
+ export { MakeOutcomeBetPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, } from "./plugins/hub-make-outcome-bet.js";
4
4
  export { defaultPlugins, type PluginContext, type PluginIdentity, type UserSessionContext, } from "./server/graphile.config.js";
5
5
  export type ServerOptions = {
6
6
  configureApp?: (app: Express) => void;
package/dist/src/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import PgUpgradeSchema, { DatabaseAheadError, } from "@moneypot/pg-upgrade-schema";
2
2
  import * as db from "./db/index.js";
3
- import config from "./config.js";
3
+ import * as config from "./config.js";
4
4
  import { createHubServer } from "./server/index.js";
5
5
  import { initializeTransferProcessors } from "./process-transfers.js";
6
6
  import { join } from "path";
7
7
  import { logger, setLogger } from "./logger.js";
8
- export { MakeOutcomeBetPlugin } from "./plugins/hub-make-outcome-bet.js";
8
+ export { MakeOutcomeBetPlugin, } from "./plugins/hub-make-outcome-bet.js";
9
9
  export { defaultPlugins, } from "./server/graphile.config.js";
10
10
  async function initialize(options) {
11
11
  if (options.logger) {
@@ -1,8 +1,13 @@
1
- import { PoolClient } from "pg";
2
- import { DbCasino, DbProcessedTakeRequest } from "./db/types.js";
3
- export declare const pgAdvisoryLock: {
4
- forMpTakeRequestProcessing: (pgClient: PoolClient, params: {
1
+ import { DbCasino, DbExperience, DbProcessedTakeRequest, DbUser } from "./db/types.js";
2
+ import { PgClientInTransaction } from "./db/index.js";
3
+ export declare const PgAdvisoryLock: {
4
+ forMpTakeRequestProcessing: (pgClient: PgClientInTransaction, params: {
5
5
  mpTakeRequestId: DbProcessedTakeRequest["mp_take_request_id"];
6
6
  casinoId: DbCasino["id"];
7
7
  }) => Promise<void>;
8
+ forNewHashChain: (pgClient: PgClientInTransaction, params: {
9
+ userId: DbUser["id"];
10
+ experienceId: DbExperience["id"];
11
+ casinoId: DbCasino["id"];
12
+ }) => Promise<void>;
8
13
  };
@@ -1,6 +1,8 @@
1
+ import { assert } from "tsafe";
1
2
  var LockNamespace;
2
3
  (function (LockNamespace) {
3
4
  LockNamespace[LockNamespace["MP_TAKE_REQUEST"] = 1] = "MP_TAKE_REQUEST";
5
+ LockNamespace[LockNamespace["NEW_HASH_CHAIN"] = 2] = "NEW_HASH_CHAIN";
4
6
  })(LockNamespace || (LockNamespace = {}));
5
7
  function simpleHash32(text) {
6
8
  let hash = 0;
@@ -20,8 +22,13 @@ async function acquireAdvisoryLock(pgClient, namespace, hash) {
20
22
  values: [namespace, hash],
21
23
  });
22
24
  }
23
- export const pgAdvisoryLock = {
25
+ export const PgAdvisoryLock = {
24
26
  forMpTakeRequestProcessing: async (pgClient, params) => {
27
+ assert(pgClient._inTransaction, "pgClient must be in a transaction");
25
28
  await acquireAdvisoryLock(pgClient, LockNamespace.MP_TAKE_REQUEST, createHashKey(params.mpTakeRequestId, params.casinoId));
26
29
  },
30
+ forNewHashChain: async (pgClient, params) => {
31
+ assert(pgClient._inTransaction, "pgClient must be in a transaction");
32
+ await acquireAdvisoryLock(pgClient, LockNamespace.NEW_HASH_CHAIN, createHashKey(params.userId, params.experienceId, params.casinoId));
33
+ },
27
34
  };
@@ -0,0 +1,84 @@
1
+ CREATE TABLE hub.hash_chain (
2
+ id uuid PRIMARY KEY DEFAULT hub_hidden.uuid_generate_v7(),
3
+ user_id uuid NOT NULL REFERENCES hub.user(id),
4
+ experience_id uuid NOT NULL REFERENCES hub.experience(id),
5
+ casino_id uuid NOT NULL REFERENCES hub.casino(id),
6
+ client_seed text NOT NULL,
7
+ active boolean NOT NULL,
8
+
9
+ max_iteration int NOT NULL check (max_iteration > 0),
10
+ current_iteration int NOT NULL check (current_iteration between 0 and max_iteration)
11
+ );
12
+
13
+ -- TODO: Should probably index current_iteration
14
+ -- CREATE INDEX hash_chain_current_iteration_idx ON hub.hash_chain(current_iteration);
15
+
16
+ CREATE INDEX hash_chain_user_id_idx ON hub.hash_chain(user_id);
17
+ CREATE INDEX hash_chain_experience_id_idx ON hub.hash_chain(experience_id);
18
+ CREATE INDEX hash_chain_casino_id_idx ON hub.hash_chain(casino_id);
19
+
20
+ -- Ensure only one active hash_chain per user per experience per casino
21
+ CREATE UNIQUE INDEX active_hash_chain_idx
22
+ ON hub.hash_chain (user_id, experience_id, casino_id)
23
+ WHERE active = true;
24
+
25
+ CREATE TYPE hub.hash_kind AS ENUM (
26
+ 'TERMINAL', -- max iteration hash (e.g. iteration 1000)
27
+ 'INTERMEDIATE', -- intermediate hash (e.g. iteration 1-999)
28
+ 'PREIMAGE' -- preimage hash (always iteration 0)
29
+ );
30
+
31
+ -- this is the base table for all bets events
32
+ CREATE TABLE hub.hash (
33
+ id uuid PRIMARY KEY DEFAULT hub_hidden.uuid_generate_v7(),
34
+ kind hub.hash_kind NOT NULL,
35
+ hash_chain_id uuid NOT NULL REFERENCES hub.hash_chain(id),
36
+ iteration int NOT NULL check (iteration >= 0), -- which Nth value from the hash chain it is
37
+ digest bytea NOT NULL, -- the actual hash we got from hash chain server
38
+ metadata jsonb NOT NULL DEFAULT '{}' -- operator can store game-specific tags
39
+ );
40
+
41
+ CREATE INDEX hash_hash_chain_id_idx ON hub.hash(hash_chain_id);
42
+
43
+ -- Ensure iterations are unique per hash_chain to avoid dupe mistakes
44
+ CREATE UNIQUE INDEX hash_hash_chain_id_iteration_idx ON hub.hash(hash_chain_id, iteration);
45
+
46
+ -- Ensure a hash_chain only has of each end-type hash
47
+ CREATE UNIQUE INDEX hash_chain_terminal_hash_idx ON hub.hash (hash_chain_id)
48
+ WHERE kind = 'TERMINAL';
49
+ CREATE UNIQUE INDEX hash_chain_preimage_hash_idx ON hub.hash (hash_chain_id)
50
+ WHERE kind = 'PREIMAGE';
51
+
52
+ -- GRANTS
53
+
54
+ GRANT SELECT ON TABLE hub.hash_chain TO app_postgraphile;
55
+ GRANT SELECT ON TABLE hub.hash TO app_postgraphile;
56
+
57
+ -- RLS
58
+ ALTER TABLE hub.hash_chain ENABLE ROW LEVEL SECURITY;
59
+ ALTER TABLE hub.hash ENABLE ROW LEVEL SECURITY;
60
+
61
+ CREATE POLICY select_hash_chain ON hub.hash_chain FOR SELECT USING (
62
+ -- Operator can see all rows
63
+ hub_hidden.is_operator() OR
64
+ -- User can see their own rows
65
+ (
66
+ user_id = hub_hidden.current_user_id() AND
67
+ experience_id = hub_hidden.current_experience_id() AND
68
+ casino_id = hub_hidden.current_casino_id()
69
+ )
70
+ );
71
+
72
+ CREATE POLICY select_hash ON hub.hash FOR SELECT USING (
73
+ -- Operator can see all rows
74
+ hub_hidden.is_operator() OR
75
+ -- User can see their own rows by checking the associated hash_chain
76
+ EXISTS (
77
+ SELECT 1
78
+ FROM hub.hash_chain
79
+ WHERE hub.hash_chain.id = hub.hash.hash_chain_id
80
+ AND hub.hash_chain.user_id = hub_hidden.current_user_id()
81
+ AND hub.hash_chain.experience_id = hub_hidden.current_experience_id()
82
+ AND hub.hash_chain.casino_id = hub_hidden.current_casino_id()
83
+ )
84
+ );
@@ -1,3 +1,6 @@
1
+ drop table if exists hub.outcome_bet cascade;
2
+ drop type if exists hub.outcome cascade;
3
+
1
4
  create type hub.outcome as (
2
5
  weight float,
3
6
  profit float
@@ -14,6 +17,9 @@ create table hub.outcome_bet (
14
17
  experience_id uuid not null references hub.experience(id),
15
18
  casino_id uuid not null references hub.casino(id),
16
19
 
20
+ -- provably fair hash chain
21
+ hash_chain_id uuid not null references hub.hash_chain(id),
22
+
17
23
  currency_key text not null,
18
24
  wager float not null,
19
25
 
@@ -24,7 +30,7 @@ create table hub.outcome_bet (
24
30
  profit float not null,
25
31
 
26
32
  -- null when no outcomes saved
27
- outcome_idx smallint null,
33
+ outcome_idx smallint null check (outcome_idx between 0 and array_length(outcomes,1)-1),
28
34
  outcomes hub.outcome[] not null default '{}',
29
35
 
30
36
  -- Operator-provided data per bet
@@ -37,6 +43,7 @@ create index outcome_bet_user_id_idx on hub.outcome_bet(user_id);
37
43
  create index outcome_bet_experience_id_idx on hub.outcome_bet(experience_id);
38
44
  create index outcome_bet_casino_id_idx on hub.outcome_bet(casino_id);
39
45
  create index outcome_bet_kind_idx on hub.outcome_bet(kind);
46
+ create index outcome_bet_hash_chain_id_idx on hub.outcome_bet(hash_chain_id);
40
47
 
41
48
  -- GRANT
42
49
 
@@ -1,6 +1,6 @@
1
1
  import { GraphQLError } from "graphql";
2
2
  import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
- import { assert, is } from "tsafe";
3
+ import { assert } from "tsafe";
4
4
  import { GET_USER_FROM_USER_TOKEN } from "../graphql-queries.js";
5
5
  import { exactlyOneRow, maybeOneRow } from "../db/util.js";
6
6
  import { createGraphqlClient } from "../graphql-client.js";
@@ -9,6 +9,18 @@ import { superuserPool, withPgPoolTransaction, } from "../db/index.js";
9
9
  import { logger } from "../logger.js";
10
10
  import * as jwtService from "../services/jwt-service.js";
11
11
  import { extractGraphQLErrorInfo, isGraphQLError } from "../GraphQLError.js";
12
+ import { z } from "zod";
13
+ const BaseUrlSchema = z
14
+ .string()
15
+ .refine((val) => URL.canParse(val), "Base URL must be a valid url")
16
+ .transform((val) => new URL(val))
17
+ .refine((val) => val.protocol === "http:" || val.protocol === "https:", "Base URL must use http or https protocol")
18
+ .refine((val) => val.pathname === "/" && val.search === "" && val.hash === "", "Base URL must have no path, query, nor hash")
19
+ .transform((val) => val.toString().replace(/\/$/, ""));
20
+ const InputSchema = z.object({
21
+ casinoBaseUrl: BaseUrlSchema,
22
+ userToken: z.string().min(1, "User token required"),
23
+ });
12
24
  export const HubAuthenticatePlugin = makeExtendSchemaPlugin(() => {
13
25
  return {
14
26
  typeDefs: gql `
@@ -38,9 +50,18 @@ export const HubAuthenticatePlugin = makeExtendSchemaPlugin(() => {
38
50
  hubAuthenticate(_, { $input }) {
39
51
  try {
40
52
  const $context = context();
41
- const $success = sideEffect([$input, $context], ([input, context]) => {
53
+ const $success = sideEffect([$input, $context], ([rawInput, context]) => {
42
54
  return withPgPoolTransaction(superuserPool, async (pgClient) => {
43
- assert(is(input));
55
+ let input;
56
+ try {
57
+ input = InputSchema.parse(rawInput);
58
+ }
59
+ catch (e) {
60
+ if (e instanceof z.ZodError) {
61
+ throw new GraphQLError(e.errors[0].message);
62
+ }
63
+ throw e;
64
+ }
44
65
  const { userToken: jwt, casinoBaseUrl } = input;
45
66
  const casino = await pgClient
46
67
  .query({
@@ -1,4 +1,15 @@
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
+ weight: number;
8
+ profit: number;
9
+ }, {
10
+ weight: number;
11
+ profit: number;
12
+ }>;
2
13
  declare const InputSchema: z.ZodObject<{
3
14
  kind: z.ZodString;
4
15
  wager: z.ZodNumber;
@@ -25,33 +36,49 @@ declare const InputSchema: z.ZodObject<{
25
36
  weight: number;
26
37
  profit: number;
27
38
  }[]>;
39
+ hashChainId: z.ZodString;
40
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
28
41
  }, "strict", z.ZodTypeAny, {
29
42
  currency: string;
43
+ hashChainId: string;
30
44
  kind: string;
31
45
  wager: number;
32
46
  outcomes: {
33
47
  weight: number;
34
48
  profit: number;
35
49
  }[];
50
+ metadata?: Record<string, any> | undefined;
36
51
  }, {
37
52
  currency: string;
53
+ hashChainId: string;
38
54
  kind: string;
39
55
  wager: number;
40
56
  outcomes: {
41
57
  weight: number;
42
58
  profit: number;
43
59
  }[];
60
+ metadata?: Record<string, any> | undefined;
44
61
  }>;
45
62
  type Input = z.infer<typeof InputSchema>;
46
- type BetConfig = {
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
+ export type OutcomeBetConfig = {
47
73
  houseEdge: number;
48
74
  saveOutcomes: boolean;
49
- getMetadata?: (input: Input) => Promise<Record<string, any>>;
75
+ initializeMetadataFromUntrustedUserInput?: (input: Input) => Result<Record<string, any>, string>;
76
+ finalizeMetadata?: (validatedMetadata: Record<string, any>, data: FinalizeMetadataData) => Record<string, any>;
50
77
  };
51
- export type BetConfigMap<BetKind extends string> = {
52
- [betKind in BetKind]: BetConfig;
78
+ export type OutcomeBetConfigMap<BetKind extends string> = {
79
+ [betKind in BetKind]: OutcomeBetConfig;
53
80
  };
54
81
  export declare function MakeOutcomeBetPlugin<BetKind extends string>({ betConfigs }: {
55
- betConfigs: BetConfigMap<BetKind>;
82
+ betConfigs: OutcomeBetConfigMap<BetKind>;
56
83
  }): GraphileConfig.Plugin;
57
84
  export {};
@@ -1,9 +1,11 @@
1
- import { context, object, sideEffect } from "postgraphile/grafast";
1
+ import { access, context, object, ObjectStep, polymorphicBranch, sideEffect, } from "postgraphile/grafast";
2
2
  import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
3
  import { z } from "zod";
4
4
  import { GraphQLError } from "graphql";
5
- import { exactlyOneRow, maybeOneRow, superuserPool, withPgPoolTransaction, } from "../db/index.js";
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";
8
+ import { getIntermediateHash, getPreimageHash, } from "../hash-chain/get-hash.js";
7
9
  const FLOAT_EPSILON = 1e-10;
8
10
  function sum(ns) {
9
11
  return ns.reduce((a, b) => a + b, 0);
@@ -35,6 +37,8 @@ const InputSchema = z
35
37
  .max(50, "Outcome count must be <= 50")
36
38
  .refine((data) => data.some((o) => o.profit < 0), "At least one outcome should have profit < 0")
37
39
  .refine((data) => data.some((o) => o.profit > 0), "At least one outcome should have profit > 0"),
40
+ hashChainId: z.string().uuid("Invalid hash chain ID"),
41
+ metadata: z.record(z.string(), z.any()).optional(),
38
42
  })
39
43
  .strict();
40
44
  const BetKindSchema = z
@@ -48,10 +52,11 @@ const BetConfigsSchema = z.record(BetKindSchema, z.object({
48
52
  .gte(0, "House edge must be >= 0")
49
53
  .lte(1, "House edge must be <= 1"),
50
54
  saveOutcomes: z.boolean(),
51
- getMetadata: z
55
+ initializeMetadataFromUntrustedUserInput: z
56
+ .function()
57
+ .optional(),
58
+ finalizeMetadata: z
52
59
  .function()
53
- .args(InputSchema)
54
- .returns(z.record(z.string(), z.any()))
55
60
  .optional(),
56
61
  }));
57
62
  export function MakeOutcomeBetPlugin({ betConfigs }) {
@@ -69,12 +74,20 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
69
74
  wager: Int!
70
75
  currency: String!
71
76
  outcomes: [HubOutcomeInput!]!
77
+ hashChainId: UUID!
78
+ metadata: JSON
72
79
  }
73
80
 
74
- type HubMakeOutcomeBetPayload {
81
+ type HubMakeOutcomeBetOk {
75
82
  bet: HubOutcomeBet!
76
83
  }
77
84
 
85
+ union HubMakeOutcomeBetResult = HubMakeOutcomeBetOk | HubBadHashChainError
86
+
87
+ type HubMakeOutcomeBetPayload {
88
+ result: HubMakeOutcomeBetResult!
89
+ }
90
+
78
91
  extend type Mutation {
79
92
  hubMakeOutcomeBet(input: HubMakeOutcomeBetInput!): HubMakeOutcomeBetPayload
80
93
  }
@@ -85,7 +98,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
85
98
  Mutation: {
86
99
  hubMakeOutcomeBet: (_, { $input }) => {
87
100
  const $identity = context().get("identity");
88
- const $betId = sideEffect([$identity, $input], async ([identity, rawInput]) => {
101
+ const $result = sideEffect([$identity, $input], async ([identity, rawInput]) => {
89
102
  if (identity?.kind !== "user") {
90
103
  throw new GraphQLError("Unauthorized");
91
104
  }
@@ -108,6 +121,16 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
108
121
  if (!betKinds.includes(rawInput.kind)) {
109
122
  throw new GraphQLError(`Invalid bet kind`);
110
123
  }
124
+ let initializedMetadata;
125
+ if (betConfig.initializeMetadataFromUntrustedUserInput) {
126
+ const result = betConfig.initializeMetadataFromUntrustedUserInput(input);
127
+ if (result.ok) {
128
+ initializedMetadata = result.value;
129
+ }
130
+ else {
131
+ throw new GraphQLError(`Invalid input: ${result.error}`);
132
+ }
133
+ }
111
134
  const houseEV = calculateHouseEV(input.outcomes);
112
135
  const minHouseEV = Math.max(0, betConfig.houseEdge - FLOAT_EPSILON);
113
136
  if (houseEV < minHouseEV) {
@@ -158,7 +181,65 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
158
181
  if (maxPotentialPayout > maxAllowablePayout) {
159
182
  throw new GraphQLError(`House risk limit exceeded. Max payout: ${maxPotentialPayout.toFixed(4)}`);
160
183
  }
161
- const { outcome, outcomeIdx } = pickRandomOutcome(input.outcomes);
184
+ const dbHashChain = await dbLockHubHashChain(pgClient, {
185
+ userId: session.user_id,
186
+ experienceId: session.experience_id,
187
+ casinoId: session.casino_id,
188
+ hashChainId: input.hashChainId,
189
+ });
190
+ if (!dbHashChain || !dbHashChain.active) {
191
+ return {
192
+ __typename: "HubBadHashChainError",
193
+ message: "Active hash chain not found",
194
+ };
195
+ }
196
+ if (dbHashChain.current_iteration <= 1) {
197
+ if (dbHashChain.current_iteration === 1) {
198
+ finishHashChainInBackground({
199
+ hashChainId: input.hashChainId,
200
+ }).catch((e) => {
201
+ console.error("Error finishing hash chain in background", { hashChainId: input.hashChainId, error: e });
202
+ });
203
+ }
204
+ return {
205
+ __typename: "HubBadHashChainError",
206
+ message: "Hash chain drained. Create a new one.",
207
+ };
208
+ }
209
+ const betHashIteration = dbHashChain.current_iteration - 1;
210
+ assert(betHashIteration > 0, "Bet hash iteration must be > 0");
211
+ const betHashResult = await getIntermediateHash({
212
+ hashChainId: input.hashChainId,
213
+ iteration: betHashIteration,
214
+ });
215
+ switch (betHashResult.type) {
216
+ case "success":
217
+ break;
218
+ case "bad_hash_chain":
219
+ return {
220
+ __typename: "HubBadHashChainError",
221
+ message: "Hash chain not found",
222
+ };
223
+ default: {
224
+ const _exhaustiveCheck = betHashResult;
225
+ throw new Error(`Unknown bet hash result: ${_exhaustiveCheck}`);
226
+ }
227
+ }
228
+ await dbInsertHubHash(pgClient, {
229
+ hashChainId: dbHashChain.id,
230
+ kind: DbHashKind.INTERMEDIATE,
231
+ digest: betHashResult.hash,
232
+ iteration: betHashIteration,
233
+ });
234
+ const result = await pgClient.query(`
235
+ UPDATE hub.hash_chain
236
+ SET current_iteration = $2
237
+ WHERE id = $1
238
+ `, [dbHashChain.id, betHashIteration]);
239
+ if (result.rowCount !== 1) {
240
+ throw new GraphQLError("Failed to update hash chain iteration");
241
+ }
242
+ const { outcome, outcomeIdx } = pickRandomOutcome(input.outcomes, betHashResult.hash);
162
243
  const netPlayerAmount = input.wager * outcome.profit;
163
244
  await pgClient.query({
164
245
  text: `
@@ -193,17 +274,27 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
193
274
  input.wager,
194
275
  ],
195
276
  });
277
+ const immutableData = structuredClone({
278
+ wager: input.wager,
279
+ currencyKey: dbCurrency.key,
280
+ clientSeed: dbHashChain.client_seed,
281
+ hash: betHashResult.hash,
282
+ outcomes: input.outcomes,
283
+ outcomeIdx,
284
+ });
285
+ const finalizedMetadata = betConfig.finalizeMetadata
286
+ ? betConfig.finalizeMetadata(initializedMetadata, immutableData)
287
+ : initializedMetadata;
196
288
  const newBet = {
197
289
  kind: rawInput.kind,
198
290
  wager: input.wager,
199
291
  profit: outcome.profit,
200
292
  currency_key: dbCurrency.key,
293
+ hash_chain_id: input.hashChainId,
201
294
  user_id: session.user_id,
202
295
  casino_id: session.casino_id,
203
296
  experience_id: session.experience_id,
204
- metadata: betConfig.getMetadata
205
- ? await betConfig.getMetadata(input)
206
- : {},
297
+ metadata: finalizedMetadata || {},
207
298
  ...(betConfig.saveOutcomes
208
299
  ? {
209
300
  outcomes: input.outcomes,
@@ -221,6 +312,7 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
221
312
  user_id,
222
313
  casino_id,
223
314
  experience_id,
315
+ hash_chain_id,
224
316
  kind,
225
317
  currency_key,
226
318
  wager,
@@ -229,13 +321,14 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
229
321
  outcome_idx,
230
322
  metadata
231
323
  )
232
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
324
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
233
325
  RETURNING id
234
326
  `,
235
327
  values: [
236
328
  newBet.user_id,
237
329
  newBet.casino_id,
238
330
  newBet.experience_id,
331
+ newBet.hash_chain_id,
239
332
  newBet.kind,
240
333
  newBet.currency_key,
241
334
  newBet.wager,
@@ -246,11 +339,31 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
246
339
  ],
247
340
  })
248
341
  .then(exactlyOneRow);
249
- return bet.id;
342
+ return {
343
+ __typename: "HubMakeOutcomeBetOk",
344
+ betId: bet.id,
345
+ };
250
346
  });
251
347
  });
252
348
  return object({
253
- bet: outcomeBetTable.get({ id: $betId }),
349
+ result: $result,
350
+ });
351
+ },
352
+ },
353
+ HubMakeOutcomeBetOk: {
354
+ __assertStep: ObjectStep,
355
+ bet($data) {
356
+ const $betId = access($data, "betId");
357
+ return outcomeBetTable.get({ id: $betId });
358
+ },
359
+ },
360
+ HubMakeOutcomeBetPayload: {
361
+ __assertStep: ObjectStep,
362
+ result($data) {
363
+ const $result = $data.get("result");
364
+ return polymorphicBranch($result, {
365
+ HubMakeOutcomeBetOk: {},
366
+ HubBadHashChainError: {},
254
367
  });
255
368
  },
256
369
  },
@@ -258,12 +371,13 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
258
371
  };
259
372
  }, "HubMakeOutcomeBetPlugin");
260
373
  }
261
- function generateRandomNumber() {
262
- const array = new Uint32Array(1);
263
- crypto.getRandomValues(array);
264
- return array[0] / 2 ** 32;
374
+ function normalizeHash(hash) {
375
+ assert(hash.length >= 4, "Hash must be at least 4 bytes");
376
+ const view = new DataView(hash.buffer, hash.byteOffset, Math.min(hash.byteLength, 4));
377
+ const uint32Value = view.getUint32(0, false);
378
+ return uint32Value / Math.pow(2, 32);
265
379
  }
266
- function pickRandomOutcome(outcomes) {
380
+ function pickRandomOutcome(outcomes, hash) {
267
381
  assert(outcomes.length >= 2, "Outcome count must be >= 2");
268
382
  const totalWeight = sum(outcomes.map((o) => o.weight));
269
383
  const outcomesWithProbability = outcomes.map((o) => ({
@@ -272,12 +386,12 @@ function pickRandomOutcome(outcomes) {
272
386
  }));
273
387
  const totalProb = outcomesWithProbability.reduce((sum, outcome) => sum + outcome.probability, 0);
274
388
  assert(Math.abs(totalProb - 1.0) < FLOAT_EPSILON, "Probabilities must sum to ~1");
275
- const randomValue = generateRandomNumber();
276
- let cumProb = 0;
389
+ const randomValue = normalizeHash(hash);
390
+ let cumulativeProb = 0;
277
391
  for (let i = 0; i < outcomesWithProbability.length; i++) {
278
392
  const outcome = outcomesWithProbability[i];
279
- cumProb += outcome.probability;
280
- if (randomValue <= cumProb) {
393
+ cumulativeProb += outcome.probability;
394
+ if (randomValue <= cumulativeProb) {
281
395
  return { outcome, outcomeIdx: i };
282
396
  }
283
397
  }
@@ -323,3 +437,41 @@ async function dbLockBalanceAndBankroll(pgClient, { userId, casinoId, experience
323
437
  dbHouseBankroll: null,
324
438
  };
325
439
  }
440
+ async function finishHashChainInBackground({ hashChainId, }) {
441
+ console.log("Finishing hash chain in background", { hashChainId });
442
+ const preimageHashResult = await getPreimageHash({
443
+ hashChainId,
444
+ });
445
+ console.log("Preimage hash result", { preimageHashResult });
446
+ if (preimageHashResult.type === "success") {
447
+ console.log("Inserting preimage hash", {
448
+ hashChainId,
449
+ kind: DbHashKind.PREIMAGE,
450
+ digest: preimageHashResult.hash,
451
+ iteration: 0,
452
+ });
453
+ await withPgPoolTransaction(superuserPool, async (pgClient) => {
454
+ await dbInsertHubHash(pgClient, {
455
+ hashChainId,
456
+ kind: DbHashKind.PREIMAGE,
457
+ digest: preimageHashResult.hash,
458
+ iteration: 0,
459
+ });
460
+ const result = await pgClient.query(`
461
+ UPDATE hub.hash_chain
462
+ SET current_iteration = 0,
463
+ active = false
464
+ WHERE id = $1
465
+ `, [hashChainId]);
466
+ if (result.rowCount !== 1) {
467
+ throw new Error("Failed to update hash chain iteration");
468
+ }
469
+ });
470
+ }
471
+ else {
472
+ console.warn("Failed to insert preimage hash in background", {
473
+ hashChainId,
474
+ error: preimageHashResult,
475
+ });
476
+ }
477
+ }
@@ -6,7 +6,7 @@ import EventEmitter from "events";
6
6
  import { superuserPool } from "./db/index.js";
7
7
  import { dbGetCasinoById, dbGetCasinoSecretById } from "./db/internal.js";
8
8
  import pg from "pg";
9
- import config from "./config.js";
9
+ import * as config from "./config.js";
10
10
  import { z } from "zod";
11
11
  import { gql } from "./__generated__/gql.js";
12
12
  import { logger } from "./logger.js";
@@ -2,7 +2,7 @@ import "graphile-config";
2
2
  import "postgraphile";
3
3
  import { makePgService } from "postgraphile/adaptors/pg";
4
4
  import { PostGraphileAmberPreset } from "postgraphile/presets/amber";
5
- import config from "../config.js";
5
+ import * as config from "../config.js";
6
6
  import { maskError } from "./handle-errors.js";
7
7
  import * as db from "../db/index.js";
8
8
  import { SmartTagsPlugin } from "../smart-tags.js";
@@ -20,6 +20,9 @@ import { HubAddCasinoPlugin } from "../plugins/hub-add-casino.js";
20
20
  import { HubBalanceAlertPlugin } from "../plugins/hub-balance-alert.js";
21
21
  import { custom as customPgOmitArchivedPlugin } from "@graphile-contrib/pg-omit-archived";
22
22
  import { HubCurrentXPlugin } from "../plugins/hub-current-x.js";
23
+ import { HubCreateHashChainPlugin } from "../hash-chain/plugins/hub-create-hash-chain.js";
24
+ import { HubBadHashChainErrorPlugin } from "../hash-chain/plugins/hub-bad-hash-chain-error.js";
25
+ import { HubUserActiveHashChainPlugin } from "../hash-chain/plugins/hub-user-active-hash-chain.js";
23
26
  export const requiredPlugins = [
24
27
  SmartTagsPlugin,
25
28
  IdToNodeIdPlugin,
@@ -35,6 +38,9 @@ export const requiredPlugins = [
35
38
  export const defaultPlugins = [
36
39
  ...(config.NODE_ENV === "development" ? [DebugPlugin] : []),
37
40
  ...requiredPlugins,
41
+ HubBadHashChainErrorPlugin,
42
+ HubCreateHashChainPlugin,
43
+ HubUserActiveHashChainPlugin,
38
44
  HubClaimFaucetPlugin,
39
45
  customPgOmitArchivedPlugin("deleted"),
40
46
  ];
@@ -1,5 +1,5 @@
1
1
  import { GraphQLError } from "postgraphile/graphql";
2
- import config from "../config.js";
2
+ import * as config from "../config.js";
3
3
  const isDev = config.NODE_ENV === "development";
4
4
  const isTest = config.NODE_ENV === "test";
5
5
  const camelCase = (s) => s.toLowerCase().replace(/(_\\w)/g, (m) => m[1].toUpperCase());
@@ -3,7 +3,7 @@ import { grafserv } from "grafserv/express/v4";
3
3
  import postgraphile from "postgraphile";
4
4
  import { createPreset, defaultPlugins } from "./graphile.config.js";
5
5
  import express from "express";
6
- import config from "../config.js";
6
+ import * as config from "../config.js";
7
7
  import { logger } from "../logger.js";
8
8
  import cors from "./middleware/cors.js";
9
9
  import authentication from "./middleware/authentication.js";
@@ -2,7 +2,7 @@ import { gql } from "../__generated__/gql.js";
2
2
  import { TakeRequestStatus as MpTakeRequestStatus, TransferStatusKind as MpTransferStatus, } from "../__generated__/graphql.js";
3
3
  import { exactlyOneRow, maybeOneRow, superuserPool, withPgPoolTransaction, } from "../db/index.js";
4
4
  import { assert } from "tsafe";
5
- import { pgAdvisoryLock } from "../pg-advisory-lock.js";
5
+ import { PgAdvisoryLock } from "../pg-advisory-lock.js";
6
6
  const MP_PAGINATE_PENDING_TAKE_REQUESTS = gql(`
7
7
  query MpPaginatedPendingTakeRequests($controllerId: UUID!, $after: Cursor) {
8
8
  allTakeRequests(
@@ -178,7 +178,7 @@ async function fetchPendingTakeRequests(graphqlClient, controllerId) {
178
178
  }
179
179
  async function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, }) {
180
180
  return withPgPoolTransaction(superuserPool, async (pgClient) => {
181
- await pgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
181
+ await PgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
182
182
  mpTakeRequestId,
183
183
  casinoId,
184
184
  });
@@ -457,7 +457,7 @@ async function processPendingTransferCompletions({ casinoId, graphqlClient, abor
457
457
  }
458
458
  async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTransferId, graphqlClient, casinoId, }) {
459
459
  return withPgPoolTransaction(superuserPool, async (pgClient) => {
460
- await pgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
460
+ await PgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
461
461
  mpTakeRequestId,
462
462
  casinoId,
463
463
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.3.0-dev.1",
3
+ "version": "1.3.0-dev.11",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [