@moneypot/hub 1.11.0 → 1.12.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.
package/README.md CHANGED
@@ -76,10 +76,16 @@ insert into hub.api_key default values returning key;
76
76
 
77
77
  ## Changelog
78
78
 
79
- ### 1.10.x
79
+ ### 1.12.x
80
+
81
+ - Added `hub.audit_log` table to track balance and bankroll changes.
82
+ - Read more: <https://docs.moneypot.com/hub/balance-audit>
83
+ - Removed built-in hub faucet and old withdraw system.
84
+
85
+ ### 1.11.x
80
86
 
81
87
  - Migrated database-related "globals" into a `ServerContext` object.
82
- - Changed `configureApp(app: ExpressServer)` to `configureApp(args: { app: ExpressServer, context: ServerContext })`
88
+ - Changed `configureApp(app: ExpressServer)` to `configureApp(args: { app: ExpressServer, superuserPool: pg.Pool })`
83
89
  - In a postgraphile plugin, you can access the superuser database pool via `const $superuserPool = context().get("superuserPool");`
84
90
 
85
91
  ### 1.9.x
@@ -0,0 +1,32 @@
1
+ import { DbBalance, DbBankroll } from "./db/types.js";
2
+ import { QueryExecutor } from "./db/util.js";
3
+ export type AuditLogType = "player-balance" | "house-bankroll" | "both";
4
+ type AuditAction = "hub:init" | "hub:deposit" | "hub:take_request:pending" | "hub:take_request:refund" | "hub:outcome_bet";
5
+ type AuditRefType = "hub.deposit" | "hub.outcome_bet" | "hub.take_request";
6
+ export type BaseAudit = {
7
+ action: AuditAction;
8
+ metadata?: Record<string, unknown> | null;
9
+ } & ({
10
+ refType: AuditRefType;
11
+ refId: string;
12
+ } | {
13
+ refType?: null;
14
+ refId?: null;
15
+ });
16
+ export type NewBalanceAudit = {
17
+ balanceId: DbBalance["id"];
18
+ balanceOld: DbBalance["amount"];
19
+ balanceNew: DbBalance["amount"];
20
+ balanceDelta: DbBalance["amount"];
21
+ };
22
+ export type NewBankrollAudit = {
23
+ bankrollId: DbBankroll["id"];
24
+ bankrollOld: DbBankroll["amount"];
25
+ bankrollNew: DbBankroll["amount"];
26
+ bankrollDelta: DbBankroll["amount"];
27
+ };
28
+ export type NewBothAudit = NewBalanceAudit & NewBankrollAudit;
29
+ export declare function insertAuditLog(pgClient: QueryExecutor, type: "player-balance", audit: NewBalanceAudit & BaseAudit): Promise<void>;
30
+ export declare function insertAuditLog(pgClient: QueryExecutor, type: "house-bankroll", audit: NewBankrollAudit & BaseAudit): Promise<void>;
31
+ export declare function insertAuditLog(pgClient: QueryExecutor, type: "both", audit: NewBothAudit & BaseAudit): Promise<void>;
32
+ export {};
@@ -0,0 +1,47 @@
1
+ import { logger } from "./logger.js";
2
+ export async function insertAuditLog(pgClient, type, audit) {
3
+ await pgClient.query(`
4
+ INSERT INTO hub.audit_log (
5
+ balance_id,
6
+ bankroll_id,
7
+
8
+ balance_old,
9
+ balance_new,
10
+ balance_delta,
11
+
12
+ bankroll_old,
13
+ bankroll_new,
14
+ bankroll_delta,
15
+
16
+ action,
17
+ metadata,
18
+
19
+ ref_type,
20
+ ref_id
21
+ )
22
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
23
+ `, buildAuditParams(type, audit));
24
+ logger.info({ ...audit }, "audit log inserted");
25
+ }
26
+ function buildAuditParams(type, audit) {
27
+ const balance = type === "player-balance" || type === "both"
28
+ ? audit
29
+ : null;
30
+ const bankroll = type === "house-bankroll" || type === "both"
31
+ ? audit
32
+ : null;
33
+ return [
34
+ balance?.balanceId ?? null,
35
+ bankroll?.bankrollId ?? null,
36
+ balance?.balanceOld ?? null,
37
+ balance?.balanceNew ?? null,
38
+ balance?.balanceDelta ?? null,
39
+ bankroll?.bankrollOld ?? null,
40
+ bankroll?.bankrollNew ?? null,
41
+ bankroll?.bankrollDelta ?? null,
42
+ audit.action,
43
+ audit.metadata,
44
+ audit.refType ?? null,
45
+ audit.refId ?? null,
46
+ ];
47
+ }
@@ -4,6 +4,7 @@ import { exactlyOneRow, maybeOneRow } from "./util.js";
4
4
  import { logger } from "../logger.js";
5
5
  import { setTimeout } from "node:timers/promises";
6
6
  import { assert } from "tsafe";
7
+ import { insertAuditLog } from "../audit-log.js";
7
8
  export * from "../hash-chain/db-hash-chain.js";
8
9
  export * from "./types.js";
9
10
  export * from "./public.js";
@@ -172,28 +173,51 @@ export async function insertDeposit(pool, o) {
172
173
  throw new UserFriendlyError("amount must be positive");
173
174
  }
174
175
  return withPgPoolTransaction(pool, async (client) => {
175
- const result = await client.query(`
176
+ let dbDeposit;
177
+ try {
178
+ dbDeposit = await client
179
+ .query(`
176
180
  INSERT INTO hub.deposit(casino_id, mp_transfer_id, user_id, experience_id, amount, currency_key)
177
181
  VALUES ($1, $2, $3, $4, $5, $6)
178
- ON CONFLICT (casino_id, mp_transfer_id) DO NOTHING
179
182
  RETURNING id
180
183
  `, [
181
- o.casinoId,
182
- o.mpTransferId,
183
- o.userId,
184
- o.experienceId,
185
- o.amount,
186
- o.currency,
187
- ]);
188
- if (result.rowCount === 0) {
189
- return null;
184
+ o.casinoId,
185
+ o.mpTransferId,
186
+ o.userId,
187
+ o.experienceId,
188
+ o.amount,
189
+ o.currency,
190
+ ])
191
+ .then(exactlyOneRow);
190
192
  }
191
- await client.query(`
193
+ catch (e) {
194
+ if (e instanceof pg.DatabaseError && e.code === "23505") {
195
+ if (e.constraint === "deposit_casino_id_mp_transfer_id_key") {
196
+ return null;
197
+ }
198
+ logger.error(e, `Unexpected dupe violation constraint. Expected "deposit_casino_id_mp_transfer_id_key" but got ${e.constraint}`);
199
+ return null;
200
+ }
201
+ throw e;
202
+ }
203
+ const dbBalance = await client
204
+ .query(`
192
205
  insert into hub.balance(casino_id, user_id, experience_id, currency_key, amount)
193
206
  values ($1, $2, $3, $4, $5)
194
207
  on conflict (casino_id, user_id, experience_id, currency_key) do update
195
208
  set amount = balance.amount + excluded.amount
196
- `, [o.casinoId, o.userId, o.experienceId, o.currency, o.amount]);
209
+ returning id, amount
210
+ `, [o.casinoId, o.userId, o.experienceId, o.currency, o.amount])
211
+ .then(exactlyOneRow);
212
+ await insertAuditLog(client, "player-balance", {
213
+ balanceId: dbBalance.id,
214
+ balanceOld: dbBalance.amount - o.amount,
215
+ balanceNew: dbBalance.amount,
216
+ balanceDelta: o.amount,
217
+ action: "hub:deposit",
218
+ refType: "hub.deposit",
219
+ refId: dbDeposit.id,
220
+ });
197
221
  });
198
222
  }
199
223
  export async function processWithdrawal(pgClient, { casinoId, mpTransferId, userId, experienceId, amount, currency, status, }) {
@@ -0,0 +1,95 @@
1
+ create table hub.audit_log (
2
+ id uuid primary key default hub_hidden.uuid_generate_v7(),
3
+
4
+ balance_id uuid null references hub.balance(id),
5
+ bankroll_id uuid null references hub.bankroll(id),
6
+
7
+ -- how much?
8
+ -- we require all three fields to force callsite to assert its assumptions and
9
+ -- catch potential desync between assumption and data
10
+ balance_old float null,
11
+ balance_new float null,
12
+ balance_delta float null,
13
+
14
+ bankroll_old float null,
15
+ bankroll_new float null,
16
+ bankroll_delta float null,
17
+
18
+ -- what happened?
19
+ action text not null, -- hub:init, hub:deposit, hub:take_request:pending, hub:take_request:refund, hub:outcome_bet, etc.
20
+ metadata jsonb null,
21
+
22
+ -- link to the item that caused the change, i.e. deposit, payout, send, take, etc.
23
+ ref_type text null,
24
+ ref_id text null, -- not a uuid since users might use it for their own non-uuid ids
25
+
26
+ -- Ensure we have at least one type of change
27
+ check (
28
+ (balance_delta is not null) or
29
+ (bankroll_delta is not null)
30
+ ),
31
+ -- Ensure old/new/delta are all present or all null
32
+ check (
33
+ (balance_old is null) = (balance_new is null) AND
34
+ (balance_old is null) = (balance_delta is null)
35
+ ),
36
+ check (
37
+ (bankroll_old is null) = (bankroll_new is null) AND
38
+ (bankroll_old is null) = (bankroll_delta is null)
39
+ )
40
+ );
41
+
42
+ create index on hub.audit_log(balance_id) where balance_id is not null;
43
+ create index on hub.audit_log(bankroll_id) where bankroll_id is not null;
44
+
45
+
46
+
47
+ -- seed audit_log with current balances
48
+ insert into hub.audit_log (
49
+ bankroll_id,
50
+ bankroll_old,
51
+ bankroll_new,
52
+ bankroll_delta,
53
+ action
54
+ )
55
+ select
56
+ b.id,
57
+ b.amount,
58
+ b.amount,
59
+ 0,
60
+ 'hub:init'
61
+ from hub.bankroll b;
62
+
63
+
64
+ -- seed audit_log with current balances
65
+ insert into hub.audit_log (
66
+ balance_id,
67
+ balance_old,
68
+ balance_new,
69
+ balance_delta,
70
+ action
71
+ )
72
+ select
73
+ b.id,
74
+ b.amount,
75
+ b.amount,
76
+ 0,
77
+ 'hub:init'
78
+ from hub.balance b
79
+ join hub.user u on u.id = b.user_id
80
+ ;
81
+
82
+ -- This view adds convenience fields that can be derived from the row data.
83
+ create view hub.audit_log_view as
84
+ select
85
+ al.*,
86
+ coalesce(b.currency_key, br.currency_key) as currency_key,
87
+ b.user_id,
88
+ u.uname,
89
+ u.mp_user_id,
90
+ coalesce(b.casino_id, br.casino_id) as casino_id,
91
+ b.experience_id
92
+ from hub.audit_log al
93
+ left join hub.balance b on b.id = al.balance_id
94
+ left join hub.user u on u.id = b.user_id
95
+ left join hub.bankroll br on br.id = al.bankroll_id;
@@ -9,6 +9,7 @@ import { getIntermediateHash, getPreimageHash, } 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
+ import { insertAuditLog } from "../audit-log.js";
12
13
  const FLOAT_EPSILON = 1e-10;
13
14
  function sum(ns) {
14
15
  return ns.reduce((a, b) => a + b, 0);
@@ -272,36 +273,26 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
272
273
  }, FLOAT_EPSILON);
273
274
  const outcome = input.outcomes[outcomeIdx];
274
275
  const netPlayerAmount = input.wager * outcome.profit;
276
+ const houseBankrollDelta = -netPlayerAmount;
275
277
  await pgClient.query({
276
278
  text: `
277
279
  UPDATE hub.balance
278
280
  SET amount = amount + $1
279
- WHERE user_id = $2
280
- AND casino_id = $3
281
- AND experience_id = $4
282
- AND currency_key = $5
281
+ WHERE id = $2
283
282
  `,
284
- values: [
285
- netPlayerAmount,
286
- session.user_id,
287
- session.casino_id,
288
- session.experience_id,
289
- dbCurrency.key,
290
- ],
283
+ values: [netPlayerAmount, dbPlayerBalance.id],
291
284
  });
292
285
  await pgClient.query({
293
286
  text: `
294
287
  UPDATE hub.bankroll
295
- SET amount = amount - $1,
288
+ SET amount = amount + $2,
296
289
  bets = bets + 1,
297
- wagered = wagered + $4
298
- WHERE currency_key = $2
299
- AND casino_id = $3
290
+ wagered = wagered + $3
291
+ WHERE id = $1
300
292
  `,
301
293
  values: [
302
- netPlayerAmount,
303
- dbCurrency.key,
304
- session.casino_id,
294
+ dbHouseBankroll.id,
295
+ houseBankrollDelta,
305
296
  input.wager,
306
297
  ],
307
298
  });
@@ -371,6 +362,23 @@ export function MakeOutcomeBetPlugin({ betConfigs }) {
371
362
  ],
372
363
  })
373
364
  .then(exactlyOneRow);
365
+ await insertAuditLog(pgClient, "both", {
366
+ balanceId: dbPlayerBalance.id,
367
+ balanceOld: dbPlayerBalance.amount,
368
+ balanceNew: dbPlayerBalance.amount + netPlayerAmount,
369
+ balanceDelta: netPlayerAmount,
370
+ bankrollId: dbHouseBankroll.id,
371
+ bankrollOld: dbHouseBankroll.amount,
372
+ bankrollNew: dbHouseBankroll.amount + houseBankrollDelta,
373
+ bankrollDelta: houseBankrollDelta,
374
+ action: "hub:outcome_bet",
375
+ refType: "hub.outcome_bet",
376
+ refId: bet.id,
377
+ metadata: {
378
+ wager: input.wager,
379
+ profit: outcome.profit,
380
+ },
381
+ });
374
382
  return {
375
383
  __typename: "HubMakeOutcomeBetSuccess",
376
384
  betId: bet.id,
@@ -8,9 +8,7 @@ import * as db from "../db/index.js";
8
8
  import { SmartTagsPlugin } from "../smart-tags.js";
9
9
  import { HubAuthenticatePlugin } from "../plugins/hub-authenticate.js";
10
10
  import { IdToNodeIdPlugin } from "../plugins/id-to-node-id.js";
11
- import { HubClaimFaucetPlugin } from "../plugins/hub-claim-faucet.js";
12
11
  import { DebugPlugin } from "../plugins/debug.js";
13
- import { HubWithdrawPlugin } from "../plugins/hub-withdraw.js";
14
12
  import { HubPrefixPlugin } from "../plugins/hub-schema-prefix.js";
15
13
  import { isUuid } from "../util.js";
16
14
  import { HubUserBalanceByCurrencyPlugin } from "../plugins/hub-user-balance-by-currency.js";
@@ -31,7 +29,6 @@ export const requiredPlugins = [
31
29
  HubPrefixPlugin,
32
30
  HubAuthenticatePlugin,
33
31
  HubCurrentXPlugin,
34
- HubWithdrawPlugin,
35
32
  HubBalanceAlertPlugin,
36
33
  HubPutAlertPlugin,
37
34
  HubUserBalanceByCurrencyPlugin,
@@ -45,7 +42,6 @@ export const defaultPlugins = [
45
42
  HubBadHashChainErrorPlugin,
46
43
  HubCreateHashChainPlugin,
47
44
  HubUserActiveHashChainPlugin,
48
- HubClaimFaucetPlugin,
49
45
  customPgOmitArchivedPlugin("deleted"),
50
46
  ];
51
47
  export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, context, }) {
@@ -16,7 +16,7 @@ export declare function processSingleTakeRequest({ mpTakeRequestId, mpTakeReques
16
16
  casinoId: string;
17
17
  graphqlClient: GraphQLClient;
18
18
  pool: pg.Pool;
19
- }): Promise<string | null>;
19
+ }): Promise<string | null | undefined>;
20
20
  export declare function completeTransfer({ mpTakeRequestId, takeRequestId, mpTransferId, graphqlClient, casinoId, pool, }: {
21
21
  mpTakeRequestId: string;
22
22
  takeRequestId: string;
@@ -5,6 +5,7 @@ import { assert } from "tsafe";
5
5
  import { PgAdvisoryLock } from "../pg-advisory-lock.js";
6
6
  import { useFragment } from "../__generated__/fragment-masking.js";
7
7
  import { logger } from "../logger.js";
8
+ import { insertAuditLog } from "../audit-log.js";
8
9
  export const MP_TAKE_REQUEST_FIELDS = gql(`
9
10
  fragment MpTakeRequestFields on TakeRequest {
10
11
  id
@@ -305,7 +306,16 @@ async function createAndProcessNewTakeRequest({ pgClient, mpTakeRequest, casinoI
305
306
  `,
306
307
  values: [LocalTakeRequestStatus.PROCESSING, newTakeRequest.id],
307
308
  });
308
- return await attemptTransfer({
309
+ await insertAuditLog(pgClient, "player-balance", {
310
+ balanceId: dbBalance.id,
311
+ balanceOld: dbBalance.amount,
312
+ balanceNew: dbBalance.amount - amountToTransfer,
313
+ balanceDelta: -amountToTransfer,
314
+ action: "hub:take_request:pending",
315
+ refType: "hub.take_request",
316
+ refId: newTakeRequest.id,
317
+ });
318
+ await attemptTransfer({
309
319
  pgClient,
310
320
  takeRequestId: newTakeRequest.id,
311
321
  mpTakeRequestId: mpTakeRequest.id,
@@ -582,7 +592,8 @@ export async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTrans
582
592
  logger.warn({ mpTransferId, takeRequestId }, `[completeTransfer] Transfer was already refunded. This should never happen.`);
583
593
  break;
584
594
  }
585
- await pgClient.query({
595
+ const dbBalanceAfterUpdate = await pgClient
596
+ .query({
586
597
  text: `
587
598
  UPDATE hub.balance
588
599
  SET amount = amount + $1
@@ -590,6 +601,8 @@ export async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTrans
590
601
  AND experience_id = $3
591
602
  AND casino_id = $4
592
603
  AND currency_key = $5
604
+
605
+ RETURNING id, amount
593
606
  `,
594
607
  values: [
595
608
  takeRequestData.reserved_amount,
@@ -598,6 +611,16 @@ export async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTrans
598
611
  takeRequestData.casino_id,
599
612
  takeRequestData.currency_key,
600
613
  ],
614
+ })
615
+ .then(exactlyOneRow);
616
+ await insertAuditLog(pgClient, "player-balance", {
617
+ balanceId: dbBalanceAfterUpdate.id,
618
+ balanceOld: dbBalanceAfterUpdate.amount - takeRequestData.reserved_amount,
619
+ balanceNew: dbBalanceAfterUpdate.amount,
620
+ balanceDelta: takeRequestData.reserved_amount,
621
+ action: "hub:take_request:refund",
622
+ refType: "hub.take_request",
623
+ refId: takeRequestId,
601
624
  });
602
625
  await pgClient.query({
603
626
  text: `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.11.0",
3
+ "version": "1.12.0-dev.1",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- export declare const HubClaimFaucetPlugin: GraphileConfig.Plugin;
@@ -1,93 +0,0 @@
1
- import { object } from "grafast";
2
- import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
3
- import { withPgPoolTransaction, } from "../db/index.js";
4
- import { constant, context, sideEffect } from "postgraphile/grafast";
5
- import { assert } from "tsafe";
6
- const CLAIM_AMOUNT = 1000;
7
- export const HubClaimFaucetPlugin = makeExtendSchemaPlugin(() => {
8
- return {
9
- typeDefs: gql `
10
- type HubClaimFaucetPayload {
11
- success: Boolean!
12
- query: Query
13
- }
14
-
15
- extend type Mutation {
16
- hubClaimFaucet: HubClaimFaucetPayload
17
- }
18
- `,
19
- objects: {
20
- Mutation: {
21
- plans: {
22
- hubClaimFaucet() {
23
- const $context = context();
24
- const $identity = $context.get("identity");
25
- const $superuserPool = $context.get("superuserPool");
26
- const $result = sideEffect([$identity, $superuserPool], ([identity, superuserPool]) => {
27
- if (identity?.kind !== "user") {
28
- throw new Error("Must be logged in as user");
29
- }
30
- const { session } = identity;
31
- return withPgPoolTransaction(superuserPool, async (pgClient) => {
32
- await upsertPlayCurrency(pgClient, session.casino_id);
33
- await pgClient.query({
34
- text: `
35
- insert into hub.faucet_claim (user_id, casino_id, experience_id, currency_key, amount)
36
- values ($1, $2, $3, $4, $5)
37
- `,
38
- values: [
39
- session.user_id,
40
- session.casino_id,
41
- session.experience_id,
42
- "PLAY",
43
- CLAIM_AMOUNT,
44
- ],
45
- });
46
- await pgClient.query({
47
- text: `
48
- INSERT INTO hub.balance (user_id, experience_id, casino_id, currency_key, amount)
49
- VALUES ($1, $2, $3, $4, $5)
50
- ON CONFLICT (user_id, experience_id, casino_id, currency_key) DO UPDATE
51
- SET amount = balance.amount + EXCLUDED.amount
52
- `,
53
- values: [
54
- session.user_id,
55
- session.experience_id,
56
- session.casino_id,
57
- "PLAY",
58
- CLAIM_AMOUNT,
59
- ],
60
- });
61
- return true;
62
- });
63
- });
64
- return object({
65
- result: $result,
66
- });
67
- },
68
- },
69
- },
70
- HubClaimFaucetPayload: {
71
- plans: {
72
- success($data) {
73
- return $data.get("result");
74
- },
75
- query() {
76
- return constant(true);
77
- },
78
- },
79
- },
80
- },
81
- };
82
- });
83
- async function upsertPlayCurrency(pgClient, casinoId) {
84
- assert(pgClient._inTransaction, "pgClient must be in transaction");
85
- return pgClient.query({
86
- text: `
87
- insert into hub.currency (casino_id, key, display_unit_name, display_unit_scale)
88
- values ($1, 'PLAY', 'tokens', 1)
89
- on conflict (casino_id, key) do nothing
90
- `,
91
- values: [casinoId],
92
- });
93
- }