@moneypot/hub 1.4.9 → 1.5.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.
@@ -1,4 +1,4 @@
1
- import { access, context, object, ObjectStep, polymorphicBranch, sideEffect, } from "postgraphile/grafast";
1
+ import { access, context, object, ObjectStep, sideEffect, } from "postgraphile/grafast";
2
2
  import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
3
  import { z } from "zod";
4
4
  import { GraphQLError } from "graphql";
@@ -98,167 +98,168 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
98
98
  `;
99
99
  return {
100
100
  typeDefs,
101
- plans: {
101
+ objects: {
102
102
  Mutation: {
103
- hubMakeOutcomeBet: (_, { $input }) => {
104
- const $identity = context().get("identity");
105
- const $result = sideEffect([$identity, $input], async ([identity, rawInput]) => {
106
- if (identity?.kind !== "user") {
107
- throw new GraphQLError("Unauthorized");
108
- }
109
- let input;
110
- let betKind;
111
- try {
112
- input = InputSchema.parse(rawInput);
113
- betKind = BetKindSchema.parse(rawInput.kind);
114
- }
115
- catch (e) {
116
- if (e instanceof z.ZodError) {
117
- throw new GraphQLError(e.errors[0].message);
103
+ plans: {
104
+ hubMakeOutcomeBet: (_, { $input }) => {
105
+ const $identity = context().get("identity");
106
+ const $result = sideEffect([$identity, $input], async ([identity, rawInput]) => {
107
+ if (identity?.kind !== "user") {
108
+ throw new GraphQLError("Unauthorized");
118
109
  }
119
- throw e;
120
- }
121
- const betConfig = betConfigs[betKind];
122
- if (!betConfig) {
123
- throw new GraphQLError(`Invalid bet kind`);
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`);
110
+ let input;
111
+ let betKind;
112
+ try {
113
+ input = InputSchema.parse(rawInput);
114
+ betKind = BetKindSchema.parse(rawInput.kind);
129
115
  }
130
- }
131
- let initializedMetadata;
132
- if (betConfig.initializeMetadataFromUntrustedUserInput) {
133
- const result = betConfig.initializeMetadataFromUntrustedUserInput(input);
134
- if (result.ok) {
135
- initializedMetadata = result.value;
116
+ catch (e) {
117
+ if (e instanceof z.ZodError) {
118
+ throw new GraphQLError(e.errors[0].message);
119
+ }
120
+ throw e;
121
+ }
122
+ const betConfig = betConfigs[betKind];
123
+ if (!betConfig) {
124
+ throw new GraphQLError(`Invalid bet kind`);
125
+ }
126
+ if (!betConfig.allowLossBeyondWager) {
127
+ const minProfit = Math.min(...input.outcomes.map((o) => o.profit));
128
+ if (minProfit < -1) {
129
+ throw new GraphQLError(`Loss beyond wager not allowed: outcome profit must be >= -1`);
130
+ }
131
+ }
132
+ let initializedMetadata;
133
+ if (betConfig.initializeMetadataFromUntrustedUserInput) {
134
+ const result = betConfig.initializeMetadataFromUntrustedUserInput(input);
135
+ if (result.ok) {
136
+ initializedMetadata = result.value;
137
+ }
138
+ else {
139
+ throw new GraphQLError(`Invalid input: ${result.error}`);
140
+ }
136
141
  }
137
- else {
138
- throw new GraphQLError(`Invalid input: ${result.error}`);
142
+ const houseEV = calculateHouseEV(input.outcomes);
143
+ const minHouseEV = Math.max(0, betConfig.houseEdge - FLOAT_EPSILON);
144
+ if (houseEV < minHouseEV) {
145
+ throw new GraphQLError(`No deal. House EV too low: ${houseEV.toFixed(4)}`);
139
146
  }
140
- }
141
- const houseEV = calculateHouseEV(input.outcomes);
142
- const minHouseEV = Math.max(0, betConfig.houseEdge - FLOAT_EPSILON);
143
- if (houseEV < minHouseEV) {
144
- throw new GraphQLError(`No deal. House EV too low: ${houseEV.toFixed(4)}`);
145
- }
146
- const { session } = identity;
147
- const dbCurrency = await superuserPool
148
- .query({
149
- text: `
147
+ const { session } = identity;
148
+ const dbCurrency = await superuserPool
149
+ .query({
150
+ text: `
150
151
  SELECT key
151
152
  FROM hub.currency
152
153
  WHERE key = $1
153
154
  AND casino_id = $2
154
155
  `,
155
- values: [input.currency, session.casino_id],
156
- })
157
- .then(maybeOneRow);
158
- if (!dbCurrency) {
159
- throw new GraphQLError("Currency not found");
160
- }
161
- return withPgPoolTransaction(superuserPool, async (pgClient) => {
162
- const { dbPlayerBalance, dbHouseBankroll, found } = await dbLockPlayerBalanceAndHouseBankroll(pgClient, {
163
- userId: session.user_id,
164
- casinoId: session.casino_id,
165
- experienceId: session.experience_id,
166
- currencyKey: dbCurrency.key,
167
- });
168
- if (!found) {
169
- throw new GraphQLError("No balance entry found for player or house");
156
+ values: [input.currency, session.casino_id],
157
+ })
158
+ .then(maybeOneRow);
159
+ if (!dbCurrency) {
160
+ throw new GraphQLError("Currency not found");
170
161
  }
171
- const minProfit = Math.min(...input.outcomes.map((o) => o.profit));
172
- const maxPlayerLoss = Math.abs(input.wager * minProfit);
173
- console.log("Determining if player can afford bet", {
174
- wager: input.wager,
175
- minProfit,
176
- maxPlayerLoss,
177
- dbPlayerBalance,
178
- });
179
- if (dbPlayerBalance.amount < maxPlayerLoss) {
180
- if (minProfit === -1) {
181
- throw new GraphQLError(`You cannot afford wager`);
162
+ return withPgPoolTransaction(superuserPool, async (pgClient) => {
163
+ const { dbPlayerBalance, dbHouseBankroll, found } = await dbLockPlayerBalanceAndHouseBankroll(pgClient, {
164
+ userId: session.user_id,
165
+ casinoId: session.casino_id,
166
+ experienceId: session.experience_id,
167
+ currencyKey: dbCurrency.key,
168
+ });
169
+ if (!found) {
170
+ throw new GraphQLError("No balance entry found for player or house");
182
171
  }
183
- throw new GraphQLError("You cannot afford the worst outcome");
184
- }
185
- const maxProfitMultiplier = Math.max(...input.outcomes.map((o) => o.profit));
186
- const maxPotentialPayout = input.wager * maxProfitMultiplier;
187
- const maxAllowablePayout = dbHouseBankroll.amount * 0.01;
188
- if (maxPotentialPayout > maxAllowablePayout) {
189
- throw new GraphQLError(`House risk limit exceeded. Max payout: ${maxPotentialPayout.toFixed(4)}`);
190
- }
191
- const dbHashChain = await dbLockHubHashChain(pgClient, {
192
- userId: session.user_id,
193
- experienceId: session.experience_id,
194
- casinoId: session.casino_id,
195
- hashChainId: input.hashChainId,
196
- });
197
- if (!dbHashChain || !dbHashChain.active) {
198
- return {
199
- __typename: "HubBadHashChainError",
200
- message: "Active hash chain not found",
201
- };
202
- }
203
- if (dbHashChain.current_iteration <= 1) {
204
- if (dbHashChain.current_iteration === 1) {
205
- finishHashChainInBackground({
206
- hashChainId: input.hashChainId,
207
- }).catch((e) => {
208
- console.error("Error finishing hash chain in background", { hashChainId: input.hashChainId, error: e });
209
- });
172
+ const minProfit = Math.min(...input.outcomes.map((o) => o.profit));
173
+ const maxPlayerLoss = Math.abs(input.wager * minProfit);
174
+ console.log("Determining if player can afford bet", {
175
+ wager: input.wager,
176
+ minProfit,
177
+ maxPlayerLoss,
178
+ dbPlayerBalance,
179
+ });
180
+ if (dbPlayerBalance.amount < maxPlayerLoss) {
181
+ if (minProfit === -1) {
182
+ throw new GraphQLError(`You cannot afford wager`);
183
+ }
184
+ throw new GraphQLError("You cannot afford the worst outcome");
210
185
  }
211
- return {
212
- __typename: "HubBadHashChainError",
213
- message: "Hash chain drained. Create a new one.",
214
- };
215
- }
216
- const betHashIteration = dbHashChain.current_iteration - 1;
217
- assert(betHashIteration > 0, "Bet hash iteration must be > 0");
218
- const betHashResult = await getIntermediateHash({
219
- hashChainId: input.hashChainId,
220
- iteration: betHashIteration,
221
- });
222
- switch (betHashResult.type) {
223
- case "success":
224
- break;
225
- case "bad_hash_chain":
186
+ const maxProfitMultiplier = Math.max(...input.outcomes.map((o) => o.profit));
187
+ const maxPotentialPayout = input.wager * maxProfitMultiplier;
188
+ const maxAllowablePayout = dbHouseBankroll.amount * 0.01;
189
+ if (maxPotentialPayout > maxAllowablePayout) {
190
+ throw new GraphQLError(`House risk limit exceeded. Max payout: ${maxPotentialPayout.toFixed(4)}`);
191
+ }
192
+ const dbHashChain = await dbLockHubHashChain(pgClient, {
193
+ userId: session.user_id,
194
+ experienceId: session.experience_id,
195
+ casinoId: session.casino_id,
196
+ hashChainId: input.hashChainId,
197
+ });
198
+ if (!dbHashChain || !dbHashChain.active) {
226
199
  return {
227
200
  __typename: "HubBadHashChainError",
228
- message: "Hash chain not found",
201
+ message: "Active hash chain not found",
229
202
  };
230
- default: {
231
- const _exhaustiveCheck = betHashResult;
232
- throw new Error(`Unknown bet hash result: ${_exhaustiveCheck}`);
233
203
  }
234
- }
235
- const dbHash = await dbInsertHubHash(pgClient, {
236
- hashChainId: dbHashChain.id,
237
- kind: DbHashKind.INTERMEDIATE,
238
- digest: betHashResult.hash,
239
- iteration: betHashIteration,
240
- clientSeed: input.clientSeed,
241
- });
242
- const result = await pgClient.query(`
204
+ if (dbHashChain.current_iteration <= 1) {
205
+ if (dbHashChain.current_iteration === 1) {
206
+ finishHashChainInBackground({
207
+ hashChainId: input.hashChainId,
208
+ }).catch((e) => {
209
+ console.error("Error finishing hash chain in background", { hashChainId: input.hashChainId, error: e });
210
+ });
211
+ }
212
+ return {
213
+ __typename: "HubBadHashChainError",
214
+ message: "Hash chain drained. Create a new one.",
215
+ };
216
+ }
217
+ const betHashIteration = dbHashChain.current_iteration - 1;
218
+ assert(betHashIteration > 0, "Bet hash iteration must be > 0");
219
+ const betHashResult = await getIntermediateHash({
220
+ hashChainId: input.hashChainId,
221
+ iteration: betHashIteration,
222
+ });
223
+ switch (betHashResult.type) {
224
+ case "success":
225
+ break;
226
+ case "bad_hash_chain":
227
+ return {
228
+ __typename: "HubBadHashChainError",
229
+ message: "Hash chain not found",
230
+ };
231
+ default: {
232
+ const _exhaustiveCheck = betHashResult;
233
+ throw new Error(`Unknown bet hash result: ${_exhaustiveCheck}`);
234
+ }
235
+ }
236
+ const dbHash = await dbInsertHubHash(pgClient, {
237
+ hashChainId: dbHashChain.id,
238
+ kind: DbHashKind.INTERMEDIATE,
239
+ digest: betHashResult.hash,
240
+ iteration: betHashIteration,
241
+ clientSeed: input.clientSeed,
242
+ });
243
+ const result = await pgClient.query(`
243
244
  UPDATE hub.hash_chain
244
245
  SET current_iteration = $2
245
246
  WHERE id = $1
246
247
  `, [dbHashChain.id, betHashIteration]);
247
- if (result.rowCount !== 1) {
248
- throw new GraphQLError("Failed to update hash chain iteration");
249
- }
250
- const finalHash = makeFinalHash({
251
- serverHash: betHashResult.hash,
252
- clientSeed: input.clientSeed,
253
- });
254
- const { outcomeIdx, roll } = pickRandomOutcome({
255
- outcomes: input.outcomes,
256
- finalHash: finalHash,
257
- }, FLOAT_EPSILON);
258
- const outcome = input.outcomes[outcomeIdx];
259
- const netPlayerAmount = input.wager * outcome.profit;
260
- await pgClient.query({
261
- text: `
248
+ if (result.rowCount !== 1) {
249
+ throw new GraphQLError("Failed to update hash chain iteration");
250
+ }
251
+ const finalHash = makeFinalHash({
252
+ serverHash: betHashResult.hash,
253
+ clientSeed: input.clientSeed,
254
+ });
255
+ const { outcomeIdx, roll } = pickRandomOutcome({
256
+ outcomes: input.outcomes,
257
+ finalHash: finalHash,
258
+ }, FLOAT_EPSILON);
259
+ const outcome = input.outcomes[outcomeIdx];
260
+ const netPlayerAmount = input.wager * outcome.profit;
261
+ await pgClient.query({
262
+ text: `
262
263
  UPDATE hub.balance
263
264
  SET amount = amount + $1
264
265
  WHERE user_id = $2
@@ -266,16 +267,16 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
266
267
  AND experience_id = $4
267
268
  AND currency_key = $5
268
269
  `,
269
- values: [
270
- netPlayerAmount,
271
- session.user_id,
272
- session.casino_id,
273
- session.experience_id,
274
- dbCurrency.key,
275
- ],
276
- });
277
- await pgClient.query({
278
- text: `
270
+ values: [
271
+ netPlayerAmount,
272
+ session.user_id,
273
+ session.casino_id,
274
+ session.experience_id,
275
+ dbCurrency.key,
276
+ ],
277
+ });
278
+ await pgClient.query({
279
+ text: `
279
280
  UPDATE hub.bankroll
280
281
  SET amount = amount - $1,
281
282
  bets = bets + 1,
@@ -283,48 +284,48 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
283
284
  WHERE currency_key = $2
284
285
  AND casino_id = $3
285
286
  `,
286
- values: [
287
- netPlayerAmount,
288
- dbCurrency.key,
289
- session.casino_id,
290
- input.wager,
291
- ],
292
- });
293
- const immutableData = structuredClone({
294
- wager: input.wager,
295
- currencyKey: dbCurrency.key,
296
- clientSeed: input.clientSeed,
297
- hash: betHashResult.hash,
298
- outcomes: input.outcomes,
299
- outcomeIdx,
300
- roll,
301
- });
302
- const finalizedMetadata = betConfig.finalizeMetadata
303
- ? betConfig.finalizeMetadata(initializedMetadata, immutableData)
304
- : initializedMetadata;
305
- const newBet = {
306
- kind: rawInput.kind,
307
- wager: input.wager,
308
- profit: outcome.profit,
309
- currency_key: dbCurrency.key,
310
- hash_id: dbHash.id,
311
- user_id: session.user_id,
312
- casino_id: session.casino_id,
313
- experience_id: session.experience_id,
314
- metadata: finalizedMetadata || {},
315
- ...(betConfig.saveOutcomes
316
- ? {
317
- outcomes: input.outcomes,
318
- outcome_idx: outcomeIdx,
319
- }
320
- : {
321
- outcomes: [],
322
- outcome_idx: null,
323
- }),
324
- };
325
- const bet = await pgClient
326
- .query({
327
- text: `
287
+ values: [
288
+ netPlayerAmount,
289
+ dbCurrency.key,
290
+ session.casino_id,
291
+ input.wager,
292
+ ],
293
+ });
294
+ const immutableData = structuredClone({
295
+ wager: input.wager,
296
+ currencyKey: dbCurrency.key,
297
+ clientSeed: input.clientSeed,
298
+ hash: betHashResult.hash,
299
+ outcomes: input.outcomes,
300
+ outcomeIdx,
301
+ roll,
302
+ });
303
+ const finalizedMetadata = betConfig.finalizeMetadata
304
+ ? betConfig.finalizeMetadata(initializedMetadata, immutableData)
305
+ : initializedMetadata;
306
+ const newBet = {
307
+ kind: rawInput.kind,
308
+ wager: input.wager,
309
+ profit: outcome.profit,
310
+ currency_key: dbCurrency.key,
311
+ hash_id: dbHash.id,
312
+ user_id: session.user_id,
313
+ casino_id: session.casino_id,
314
+ experience_id: session.experience_id,
315
+ metadata: finalizedMetadata || {},
316
+ ...(betConfig.saveOutcomes
317
+ ? {
318
+ outcomes: input.outcomes,
319
+ outcome_idx: outcomeIdx,
320
+ }
321
+ : {
322
+ outcomes: [],
323
+ outcome_idx: null,
324
+ }),
325
+ };
326
+ const bet = await pgClient
327
+ .query({
328
+ text: `
328
329
  INSERT INTO hub.outcome_bet (
329
330
  user_id,
330
331
  casino_id,
@@ -341,47 +342,49 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
341
342
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
342
343
  RETURNING id
343
344
  `,
344
- values: [
345
- newBet.user_id,
346
- newBet.casino_id,
347
- newBet.experience_id,
348
- newBet.hash_id,
349
- newBet.kind,
350
- newBet.currency_key,
351
- newBet.wager,
352
- newBet.profit,
353
- newBet.outcomes.map((o) => `(${o.weight},${o.profit})`),
354
- newBet.outcome_idx,
355
- newBet.metadata,
356
- ],
357
- })
358
- .then(exactlyOneRow);
359
- return {
360
- __typename: "HubMakeOutcomeBetSuccess",
361
- betId: bet.id,
362
- };
345
+ values: [
346
+ newBet.user_id,
347
+ newBet.casino_id,
348
+ newBet.experience_id,
349
+ newBet.hash_id,
350
+ newBet.kind,
351
+ newBet.currency_key,
352
+ newBet.wager,
353
+ newBet.profit,
354
+ newBet.outcomes.map((o) => `(${o.weight},${o.profit})`),
355
+ newBet.outcome_idx,
356
+ newBet.metadata,
357
+ ],
358
+ })
359
+ .then(exactlyOneRow);
360
+ return {
361
+ __typename: "HubMakeOutcomeBetSuccess",
362
+ betId: bet.id,
363
+ };
364
+ });
365
+ });
366
+ return object({
367
+ result: $result,
363
368
  });
364
- });
365
- return object({
366
- result: $result,
367
- });
369
+ },
368
370
  },
369
371
  },
370
372
  HubMakeOutcomeBetSuccess: {
371
- __assertStep: ObjectStep,
372
- bet($data) {
373
- const $betId = access($data, "betId");
374
- return outcomeBetTable.get({ id: $betId });
373
+ assertStep: ObjectStep,
374
+ plans: {
375
+ bet($data) {
376
+ const $betId = access($data, "betId");
377
+ return outcomeBetTable.get({ id: $betId });
378
+ },
375
379
  },
376
380
  },
377
381
  HubMakeOutcomeBetPayload: {
378
- __assertStep: ObjectStep,
379
- result($data) {
380
- const $result = $data.get("result");
381
- return polymorphicBranch($result, {
382
- HubMakeOutcomeBetSuccess: {},
383
- HubBadHashChainError: {},
384
- });
382
+ assertStep: ObjectStep,
383
+ plans: {
384
+ result($data) {
385
+ const $result = $data.get("result");
386
+ return $result;
387
+ },
385
388
  },
386
389
  },
387
390
  },
@@ -14,45 +14,49 @@ export const HubPutAlertPlugin = makeExtendSchemaPlugin(() => {
14
14
  mpTransferId: UUID!
15
15
  }
16
16
  `,
17
- plans: {
17
+ objects: {
18
18
  Subscription: {
19
- hubPutAlert: {
20
- subscribePlan(_$root) {
21
- const $pgSubscriber = context().get("pgSubscriber");
22
- const $identity = context().get("identity");
23
- const $channelKey = lambda($identity, (identity) => {
24
- if (identity?.kind === "user") {
25
- return `hub:user:${identity.session.user_id}:put`;
26
- }
27
- else {
28
- return "";
29
- }
30
- });
31
- return listenWithFilter($pgSubscriber, $channelKey, jsonParse, $identity, (item, identity) => {
32
- if (typeof item !== "string") {
33
- console.warn(`hubPutAlert: item is not a string: ${JSON.stringify(item)}`);
34
- return false;
35
- }
36
- if (identity?.kind !== "user") {
37
- return false;
38
- }
39
- else {
40
- return (identity.session.experience_id ===
41
- JSON.parse(item).experience_id);
42
- }
43
- });
44
- },
45
- plan($event) {
46
- return $event;
19
+ plans: {
20
+ hubPutAlert: {
21
+ subscribePlan(_$root) {
22
+ const $pgSubscriber = context().get("pgSubscriber");
23
+ const $identity = context().get("identity");
24
+ const $channelKey = lambda($identity, (identity) => {
25
+ if (identity?.kind === "user") {
26
+ return `hub:user:${identity.session.user_id}:put`;
27
+ }
28
+ else {
29
+ return "";
30
+ }
31
+ });
32
+ return listenWithFilter($pgSubscriber, $channelKey, jsonParse, $identity, (item, identity) => {
33
+ if (typeof item !== "string") {
34
+ console.warn(`hubPutAlert: item is not a string: ${JSON.stringify(item)}`);
35
+ return false;
36
+ }
37
+ if (identity?.kind !== "user") {
38
+ return false;
39
+ }
40
+ else {
41
+ return (identity.session.experience_id ===
42
+ JSON.parse(item).experience_id);
43
+ }
44
+ });
45
+ },
46
+ plan($event) {
47
+ return $event;
48
+ },
47
49
  },
48
50
  },
49
51
  },
50
52
  HubPutAlertPayload: {
51
- currencyKey($event) {
52
- return $event.get("currency_key");
53
- },
54
- mpTransferId($event) {
55
- return $event.get("mp_transfer_id");
53
+ plans: {
54
+ currencyKey($event) {
55
+ return $event.get("currency_key");
56
+ },
57
+ mpTransferId($event) {
58
+ return $event.get("mp_transfer_id");
59
+ },
56
60
  },
57
61
  },
58
62
  },