@moneypot/hub 1.14.7 → 1.15.0-dev.1

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Dashboard</title>
7
- <script type="module" crossorigin src="/dashboard/assets/index-BXG4SGJn.js"></script>
7
+ <script type="module" crossorigin src="/dashboard/assets/index-CrY0xaa9.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/dashboard/assets/index-LZVcTrKv.css">
9
9
  </head>
10
10
  <body>
@@ -20,6 +20,7 @@ export declare function withPgPoolTransaction<T>(pool: pg.Pool, callback: (_pgCl
20
20
  export declare function userFromActiveSessionKey(pgClient: QueryExecutor, sessionKey: string): Promise<{
21
21
  user: DbUser;
22
22
  sessionId: DbSession["id"];
23
+ isPlayground: boolean;
23
24
  } | null>;
24
25
  export declare class DatabaseNotifier extends stream.EventEmitter {
25
26
  private pgClient;
@@ -79,7 +80,9 @@ export declare function settleWithdrawal(pool: pg.Pool, { withdrawalId, newStatu
79
80
  withdrawalId: string;
80
81
  newStatus: Extract<DbTransferStatusKind, "COMPLETED" | "CANCELED">;
81
82
  }): Promise<void>;
82
- export declare function listCasinos(pgClient: QueryExecutor): Promise<DbCasino[]>;
83
+ export declare function listCasinos(pgClient: QueryExecutor, { isPlayground }: {
84
+ isPlayground: "include" | "exclude";
85
+ }): Promise<DbCasino[]>;
83
86
  export declare function upsertCurrencies(pgClient: QueryExecutor, { casinoId, currencies, }: {
84
87
  casinoId: string;
85
88
  currencies: {
@@ -71,20 +71,22 @@ export async function withPgPoolTransaction(pool, callback, retryCount = 0, maxR
71
71
  export async function userFromActiveSessionKey(pgClient, sessionKey) {
72
72
  const result = await pgClient
73
73
  .query(`
74
- select u.id, u.uname, u.casino_id, s.experience_id, u.mp_user_id, s.id as session_id
74
+ select u.id, u.uname, u.casino_id, s.experience_id, u.mp_user_id, s.id as session_id, c.is_playground
75
75
  from hub.active_session s
76
76
  join hub.user u on s.user_id = u.id
77
77
  join hub.experience e on e.id = s.experience_id
78
+ join hub.casino c on c.id = u.casino_id
78
79
  where s.key = $1
79
80
  `, [sessionKey])
80
81
  .then(maybeOneRow);
81
82
  if (!result) {
82
83
  return null;
83
84
  }
84
- const { session_id, ...user } = result;
85
+ const { session_id, is_playground, ...user } = result;
85
86
  return {
86
87
  user,
87
88
  sessionId: session_id,
89
+ isPlayground: is_playground,
88
90
  };
89
91
  }
90
92
  export class DatabaseNotifier extends stream.EventEmitter {
@@ -339,8 +341,8 @@ export async function settleWithdrawal(pool, { withdrawalId, newStatus, }) {
339
341
  }
340
342
  });
341
343
  }
342
- export async function listCasinos(pgClient) {
343
- const result = await pgClient.query("select * from hub.casino");
344
+ export async function listCasinos(pgClient, { isPlayground }) {
345
+ const result = await pgClient.query(`select * from hub.casino ${isPlayground === "include" ? "" : "where not is_playground"}`);
344
346
  return result.rows;
345
347
  }
346
348
  export async function upsertCurrencies(pgClient, { casinoId, currencies, }) {
@@ -55,6 +55,7 @@ export type DbCasino = {
55
55
  name: string;
56
56
  base_url: string;
57
57
  graphql_url: string;
58
+ is_playground: boolean;
58
59
  };
59
60
  export type DbCasinoSecret = {
60
61
  id: string;
@@ -6,6 +6,7 @@ export interface HubRequest extends Request {
6
6
  kind: "user";
7
7
  user: DbUser;
8
8
  sessionId: DbSession["id"];
9
+ isPlayground: boolean;
9
10
  } | {
10
11
  kind: "operator";
11
12
  apiKey: string;
@@ -182,6 +182,7 @@ create table hub.session (
182
182
  CREATE INDEX session_casino_id_idx ON hub.session(casino_id);
183
183
  CREATE INDEX session_user_id_idx ON hub.session(user_id);
184
184
  CREATE INDEX session_experience_id_idx ON hub.session(experience_id);
185
+ -- TODO: Shouldn't these only be unique per casino/exp?
185
186
  CREATE UNIQUE INDEX session_user_token_idx ON hub.session(user_token);
186
187
  CREATE UNIQUE INDEX session_key_idx ON hub.session(key);
187
188
 
@@ -0,0 +1,40 @@
1
+ -- insert a global playground app.casino record that all playground users and experiences are associated with
2
+
3
+ -- add is_playground boolean to the casino table with default=false for existing casinos
4
+ alter table hub.casino add column is_playground boolean not null default false;
5
+
6
+ insert into hub.casino (name, base_url, graphql_url, is_playground)
7
+ values (
8
+ 'Playground',
9
+ 'https://not-found.moneypot.com',
10
+ 'https://not-found.moneypot.com/graphql',
11
+ true
12
+ );
13
+
14
+ -- make it so that only one row can have is_playground=true and the rest are false
15
+ CREATE UNIQUE INDEX single_playground_casino_idx
16
+ ON hub.casino ((true))
17
+ WHERE is_playground;
18
+
19
+ -- insert HOUSE hub.currency for casino
20
+ insert into hub.currency (key, casino_id, display_unit_name, display_unit_scale)
21
+ select 'HOUSE', (select id from hub.casino where is_playground = true limit 1), 'token', 1;
22
+
23
+ -- insert 10_000_000 HOUSE bankroll for casino
24
+ insert into hub.bankroll (casino_id, currency_key, amount)
25
+ select (select id from hub.casino where is_playground = true limit 1), 'HOUSE', 10_000_000;
26
+
27
+ -- Update the notify_new_casino trigger function to exclude playground casinos
28
+ -- TODO: Seems weird that this even is a trigger... oh well, just porting it for now
29
+ CREATE OR REPLACE FUNCTION notify_new_casino()
30
+ RETURNS TRIGGER AS $$
31
+ BEGIN
32
+ -- Only notify for non-playground casinos
33
+ IF NEW.is_playground = false THEN
34
+ PERFORM pg_notify('hub:new_casino', json_build_object(
35
+ 'id', NEW.id
36
+ )::text);
37
+ END IF;
38
+ RETURN NEW;
39
+ END;
40
+ $$ LANGUAGE plpgsql;
@@ -10,6 +10,7 @@ import { logger } from "../logger.js";
10
10
  import * as jwtService from "../services/jwt-service.js";
11
11
  import { extractGraphQLErrorInfo, isGraphQLError } from "../GraphQLError.js";
12
12
  import { z } from "zod";
13
+ import { prettifyError } from "zod/v4";
13
14
  const BaseUrlSchema = z
14
15
  .string()
15
16
  .refine((val) => URL.canParse(val), "Base URL must be a valid url")
@@ -53,17 +54,17 @@ export const HubAuthenticatePlugin = makeExtendSchemaPlugin(() => {
53
54
  const $context = context();
54
55
  const $superuserPool = $context.get("superuserPool");
55
56
  const $success = sideEffect([$input, $superuserPool, $context], ([rawInput, superuserPool, context]) => {
56
- return withPgPoolTransaction(superuserPool, async (pgClient) => {
57
- let input;
58
- try {
59
- input = InputSchema.parse(rawInput);
60
- }
61
- catch (e) {
62
- if (e instanceof z.ZodError) {
63
- throw new GraphQLError(e.errors[0].message);
64
- }
65
- throw e;
57
+ let input;
58
+ try {
59
+ input = InputSchema.parse(rawInput);
60
+ }
61
+ catch (e) {
62
+ if (e instanceof z.ZodError) {
63
+ throw new GraphQLError(prettifyError(e));
66
64
  }
65
+ throw e;
66
+ }
67
+ return withPgPoolTransaction(superuserPool, async (pgClient) => {
67
68
  const { userToken: jwt, casinoBaseUrl } = input;
68
69
  const casino = await pgClient
69
70
  .query({
@@ -78,6 +79,9 @@ export const HubAuthenticatePlugin = makeExtendSchemaPlugin(() => {
78
79
  if (!casino) {
79
80
  throw new GraphQLError(`hub server is unaware of casino with a base url of provided casinoBaseUrl`);
80
81
  }
82
+ if (casino.is_playground) {
83
+ throw new GraphQLError("Cannot authenticate with playground casino");
84
+ }
81
85
  if (!casino.api_key) {
82
86
  throw new GraphQLError("Casino secret not configured");
83
87
  }
@@ -180,6 +184,7 @@ export const HubAuthenticatePlugin = makeExtendSchemaPlugin(() => {
180
184
  casino_id: casino.id,
181
185
  experience_id: dbExperience.id,
182
186
  session_id: dbSession.id,
187
+ is_playground: false,
183
188
  },
184
189
  };
185
190
  context.pgSettings = {
@@ -0,0 +1 @@
1
+ export declare const HubCreatePlaygroundSessionPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,181 @@
1
+ import { GraphQLError } from "graphql";
2
+ import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
+ import { exactlyOneRow, maybeOneRow } from "../db/util.js";
4
+ import { constant, context, object, sideEffect } from "postgraphile/grafast";
5
+ import { withPgPoolTransaction, } from "../db/index.js";
6
+ import { logger } from "../logger.js";
7
+ import { z } from "zod";
8
+ import { prettifyError } from "zod/v4";
9
+ const InputSchema = z.object({
10
+ dummy: z.string().optional(),
11
+ });
12
+ export const HubCreatePlaygroundSessionPlugin = makeExtendSchemaPlugin(() => {
13
+ return {
14
+ typeDefs: gql `
15
+ input HubCreatePlaygroundSessionInput {
16
+ dummy: String
17
+ }
18
+
19
+ extend type Mutation {
20
+ hubCreatePlaygroundSession(
21
+ input: HubCreatePlaygroundSessionInput!
22
+ ): HubAuthenticatePayload
23
+ }
24
+ `,
25
+ objects: {
26
+ Mutation: {
27
+ plans: {
28
+ hubCreatePlaygroundSession(_, { $input }) {
29
+ try {
30
+ const $context = context();
31
+ const $superuserPool = $context.get("superuserPool");
32
+ const $success = sideEffect([$input, $superuserPool, $context], ([rawInput, superuserPool, context]) => {
33
+ return withPgPoolTransaction(superuserPool, async (pgClient) => {
34
+ let _input;
35
+ try {
36
+ _input = InputSchema.parse(rawInput);
37
+ }
38
+ catch (e) {
39
+ if (e instanceof z.ZodError) {
40
+ throw new GraphQLError(prettifyError(e));
41
+ }
42
+ throw e;
43
+ }
44
+ const dbPlaygroundCasino = await pgClient
45
+ .query({
46
+ text: `
47
+ SELECT c.*
48
+ FROM hub.casino c
49
+ WHERE c.is_playground = true
50
+ LIMIT 1
51
+ `,
52
+ values: [],
53
+ })
54
+ .then(maybeOneRow);
55
+ if (!dbPlaygroundCasino) {
56
+ throw new GraphQLError("No playground casino found");
57
+ }
58
+ const DUMMY_MP_ID = uuidv7();
59
+ const randomUname = createRandomUname();
60
+ const dbUser = await pgClient
61
+ .query({
62
+ text: `
63
+ INSERT INTO hub.user(casino_id, mp_user_id, uname)
64
+ VALUES($1, $2, $3)
65
+ RETURNING id, uname, mp_user_id
66
+ `,
67
+ values: [
68
+ dbPlaygroundCasino.id,
69
+ DUMMY_MP_ID,
70
+ randomUname,
71
+ ],
72
+ })
73
+ .then(exactlyOneRow);
74
+ const dbExperience = await pgClient
75
+ .query({
76
+ text: `
77
+ INSERT INTO hub.experience(casino_id, mp_experience_id, name)
78
+ VALUES($1, $2, $3)
79
+ RETURNING id
80
+ `,
81
+ values: [
82
+ dbPlaygroundCasino.id,
83
+ DUMMY_MP_ID,
84
+ `Playground Experience ${randomUname}`,
85
+ ],
86
+ })
87
+ .then(exactlyOneRow);
88
+ await pgClient.query({
89
+ text: `
90
+ INSERT INTO hub.balance(user_id, experience_id, casino_id, currency_key, amount)
91
+ VALUES($1, $2, $3, $4, 1000)
92
+ `,
93
+ values: [
94
+ dbUser.id,
95
+ dbExperience.id,
96
+ dbPlaygroundCasino.id,
97
+ "HOUSE",
98
+ ],
99
+ });
100
+ const dbSession = await pgClient
101
+ .query({
102
+ text: `
103
+ INSERT INTO hub.session(casino_id, user_id, experience_id, user_token)
104
+ VALUES($1, $2, $3, $4)
105
+ RETURNING id, key
106
+ `,
107
+ values: [
108
+ dbPlaygroundCasino.id,
109
+ dbUser.id,
110
+ dbExperience.id,
111
+ DUMMY_MP_ID,
112
+ ],
113
+ })
114
+ .then(exactlyOneRow);
115
+ const ret = {
116
+ userId: dbUser.id,
117
+ uname: dbUser.uname,
118
+ experienceId: dbExperience.id,
119
+ sessionKey: dbSession.key,
120
+ };
121
+ context.identity = {
122
+ kind: "user",
123
+ session: {
124
+ user_id: dbUser.id,
125
+ mp_user_id: dbUser.mp_user_id,
126
+ casino_id: dbPlaygroundCasino.id,
127
+ experience_id: dbExperience.id,
128
+ session_id: dbSession.id,
129
+ is_playground: true,
130
+ },
131
+ };
132
+ context.pgSettings = {
133
+ "session.user_id": dbUser.id,
134
+ "session.casino_id": dbPlaygroundCasino.id,
135
+ "session.experience_id": dbExperience.id,
136
+ "session.session_id": dbSession.id,
137
+ };
138
+ return ret;
139
+ });
140
+ });
141
+ return object({
142
+ query: constant(true),
143
+ success: $success,
144
+ });
145
+ }
146
+ catch (error) {
147
+ logger.error(error);
148
+ throw error;
149
+ }
150
+ },
151
+ },
152
+ },
153
+ },
154
+ };
155
+ });
156
+ function uuidv7() {
157
+ const now = BigInt(Date.now());
158
+ let timestamp = now & ((1n << 48n) - 1n);
159
+ const rand = crypto.getRandomValues(new Uint8Array(10));
160
+ const bytes = new Uint8Array(16);
161
+ for (let i = 5; i >= 0; i--) {
162
+ bytes[i] = Number(timestamp & 0xffn);
163
+ timestamp >>= 8n;
164
+ }
165
+ bytes.set(rand, 6);
166
+ bytes[6] = (bytes[6] & 0x0f) | 0x70;
167
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
168
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
169
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
170
+ }
171
+ function createRandomUname() {
172
+ const MAX_MP_UNAME_LENGTH = 15;
173
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
174
+ let result = "play_";
175
+ const SUFFIX_LENGTH = MAX_MP_UNAME_LENGTH - result.length;
176
+ for (let i = 0; i < SUFFIX_LENGTH; i++) {
177
+ const idx = Math.floor(Math.random() * chars.length);
178
+ result += chars[idx];
179
+ }
180
+ return result;
181
+ }
@@ -11,6 +11,7 @@ import { logger } from "../logger.js";
11
11
  import { validateRisk } from "../risk-policy.js";
12
12
  import { insertAuditLog } from "../audit-log.js";
13
13
  import { dbRevealHashChain } from "../hash-chain/reveal-hash-chain.js";
14
+ import { prettifyError } from "zod/v4";
14
15
  const FLOAT_EPSILON = 1e-10;
15
16
  function sum(ns) {
16
17
  return ns.reduce((a, b) => a + b, 0);
@@ -121,10 +122,14 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
121
122
  }
122
123
  catch (e) {
123
124
  if (e instanceof z.ZodError) {
124
- throw new GraphQLError(e.errors[0].message);
125
+ throw new GraphQLError(prettifyError(e));
125
126
  }
126
127
  throw e;
127
128
  }
129
+ if (identity.session.is_playground &&
130
+ input.currency !== "HOUSE") {
131
+ throw new GraphQLError("Playground users can only bet with HOUSE currency");
132
+ }
128
133
  const betConfig = betConfigs[betKind];
129
134
  if (!betConfig) {
130
135
  throw new GraphQLError(`Invalid bet kind`);
@@ -3,6 +3,11 @@ import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
3
  import { withPgPoolTransaction } from "../db/index.js";
4
4
  import { exactlyOneRow, maybeOneRow } from "../db/util.js";
5
5
  import { GraphQLError } from "graphql";
6
+ import z, { prettifyError } from "zod/v4";
7
+ const InputSchema = z.object({
8
+ amount: z.number().min(1),
9
+ currency: z.string(),
10
+ });
6
11
  export const HubWithdrawPlugin = makeExtendSchemaPlugin((build) => {
7
12
  const hubWithdrawalRequests = build.input.pgRegistry.pgResources.hub_withdrawal_request;
8
13
  return {
@@ -27,19 +32,26 @@ export const HubWithdrawPlugin = makeExtendSchemaPlugin((build) => {
27
32
  hubWithdraw(_, { $input }) {
28
33
  const $identity = context().get("identity");
29
34
  const $superuserPool = context().get("superuserPool");
30
- const $withdrawalRequestId = sideEffect([$input, $identity, $superuserPool], ([input, identity, superuserPool]) => {
35
+ const $withdrawalRequestId = sideEffect([$input, $identity, $superuserPool], ([rawInput, identity, superuserPool]) => {
31
36
  if (identity?.kind !== "user") {
32
37
  throw new GraphQLError("You must be logged in");
33
38
  }
39
+ if (identity.session.is_playground) {
40
+ throw new GraphQLError("Playground users cannot withdraw");
41
+ }
42
+ let input;
43
+ try {
44
+ input = InputSchema.parse(rawInput);
45
+ }
46
+ catch (e) {
47
+ if (e instanceof z.ZodError) {
48
+ throw new GraphQLError(prettifyError(e));
49
+ }
50
+ throw e;
51
+ }
34
52
  const { session } = identity;
35
53
  return withPgPoolTransaction(superuserPool, async (pgClient) => {
36
54
  const { amount, currency } = input;
37
- if (amount < 1) {
38
- throw new GraphQLError("Withdraw amount must be at least 1");
39
- }
40
- if (!Number.isInteger(amount)) {
41
- throw new GraphQLError("Withdraw amount must be an integer");
42
- }
43
55
  const dbCurrency = await pgClient
44
56
  .query({
45
57
  text: `
@@ -13,8 +13,9 @@ export async function startCasinoTransferProcessor({ casinoId, signal, pool, })
13
13
  throw new Error(`processor already running for casino ${casinoId}`);
14
14
  }
15
15
  const casino = await dbGetCasinoById(pool, casinoId);
16
- const secret = await dbGetCasinoSecretById(pool, casinoId);
17
16
  assert(casino, `Casino not found for casino id ${casinoId}`);
17
+ assert(!casino.is_playground, `Cannot start processor for playground casino ${casinoId}`);
18
+ const secret = await dbGetCasinoSecretById(pool, casinoId);
18
19
  assert(secret, `Secret not found for casino id ${casinoId}`);
19
20
  activeCasinos.add(casinoId);
20
21
  startPollingProcessor({ casinoId, signal, pool });
@@ -30,7 +31,7 @@ export async function startCasinoTransferProcessor({ casinoId, signal, pool, })
30
31
  export function initializeTransferProcessors({ signal, pool, }) {
31
32
  (async () => {
32
33
  try {
33
- const casinos = await db.listCasinos(pool);
34
+ const casinos = await db.listCasinos(pool, { isPlayground: "exclude" });
34
35
  for (const casino of casinos) {
35
36
  if (!URL.canParse(casino.graphql_url)) {
36
37
  logger.warn(`Skipping casino ${casino.id} due to invalid graphql_url: "${casino.graphql_url}"`);
@@ -7,6 +7,7 @@ export type UserSessionContext = {
7
7
  casino_id: string;
8
8
  experience_id: string;
9
9
  session_id: string;
10
+ is_playground: boolean;
10
11
  };
11
12
  export type PluginIdentity = {
12
13
  kind: "user";
@@ -25,11 +25,13 @@ import { HubOutcomeInputNonNullFieldsPlugin } from "../plugins/hub-outcome-input
25
25
  import { HubPutAlertPlugin } from "../plugins/hub-put-alert.js";
26
26
  import { HubRevealHashChainPlugin } from "../hash-chain/plugins/hub-reveal-hash-chain.js";
27
27
  import { HubPreimageHashFieldPlugin } from "../hash-chain/plugins/hub-preimage-hash-field.js";
28
+ import { HubCreatePlaygroundSessionPlugin } from "../plugins/hub-create-playground-session.js";
28
29
  export const requiredPlugins = [
29
30
  SmartTagsPlugin,
30
31
  IdToNodeIdPlugin,
31
32
  HubPrefixPlugin,
32
33
  HubAuthenticatePlugin,
34
+ HubCreatePlaygroundSessionPlugin,
33
35
  HubCurrentXPlugin,
34
36
  HubBalanceAlertPlugin,
35
37
  HubPutAlertPlugin,
@@ -115,6 +117,7 @@ export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abo
115
117
  casino_id: reqIdentity.user.casino_id,
116
118
  experience_id: reqIdentity.user.experience_id,
117
119
  session_id: reqIdentity.sessionId,
120
+ is_playground: reqIdentity.isPlayground,
118
121
  },
119
122
  };
120
123
  }
@@ -163,6 +166,7 @@ async function handleWebsocketContext(context, ws) {
163
166
  casino_id: result.user.casino_id,
164
167
  experience_id: result.user.experience_id,
165
168
  session_id: result.sessionId,
169
+ is_playground: result.isPlayground,
166
170
  };
167
171
  return {
168
172
  pgSettings: {
@@ -34,6 +34,7 @@ const authentication = (context) => {
34
34
  kind: "user",
35
35
  user: result.user,
36
36
  sessionId: result.sessionId,
37
+ isPlayground: result.isPlayground,
37
38
  };
38
39
  }
39
40
  return next();
@@ -8,6 +8,11 @@ export const SmartTagsPlugin = makeJSONPgSmartTagsPlugin({
8
8
  behavior: ["-attribute:update"],
9
9
  },
10
10
  },
11
+ "hub.casino.is_playground": {
12
+ tags: {
13
+ behavior: ["-attribute:update"],
14
+ },
15
+ },
11
16
  "hub.session.user_token": {
12
17
  tags: {
13
18
  behavior: ["-orderBy"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.14.7",
3
+ "version": "1.15.0-dev.1",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [