@moneypot/hub 1.20.6 → 1.22.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.
@@ -123,35 +123,59 @@ export function startPollingProcessor({ casinoId, signal, pool, }) {
123
123
  logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (3)`);
124
124
  break;
125
125
  }
126
- cursor = await processTransfersUntilEmpty({
127
- afterCursor: cursor,
128
- graphqlClient,
129
- casinoInfo: { ...casino, ...casinoSecret },
130
- signal,
131
- pool,
132
- });
126
+ let anyTaskFailed = false;
127
+ try {
128
+ cursor = await processTransfersUntilEmpty({
129
+ afterCursor: cursor,
130
+ graphqlClient,
131
+ casinoInfo: { ...casino, ...casinoSecret },
132
+ signal,
133
+ pool,
134
+ });
135
+ }
136
+ catch (e) {
137
+ anyTaskFailed = true;
138
+ logger.error(e, `transfer polling failed for casino ${casinoId}`);
139
+ }
133
140
  if (signal.aborted) {
134
141
  logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (4)`);
135
142
  break;
136
143
  }
137
- await processWithdrawals({
138
- abortSignal: signal,
139
- casinoId: casino.id,
140
- graphqlClient,
141
- pool,
142
- });
144
+ try {
145
+ await processWithdrawals({
146
+ abortSignal: signal,
147
+ casinoId: casino.id,
148
+ graphqlClient,
149
+ pool,
150
+ });
151
+ }
152
+ catch (e) {
153
+ anyTaskFailed = true;
154
+ logger.error(e, `withdrawal processing failed for casino ${casinoId}`);
155
+ }
143
156
  if (signal.aborted) {
144
157
  logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (5)`);
145
158
  break;
146
159
  }
147
- await processTakeRequests({
148
- abortSignal: signal,
149
- controllerId: casinoSecret.controller_id,
150
- casinoId: casino.id,
151
- graphqlClient,
152
- pool,
153
- });
154
- processorState.backoffTime = MIN_BACKOFF_TIME;
160
+ try {
161
+ await processTakeRequests({
162
+ abortSignal: signal,
163
+ controllerId: casinoSecret.controller_id,
164
+ casinoId: casino.id,
165
+ graphqlClient,
166
+ pool,
167
+ });
168
+ }
169
+ catch (e) {
170
+ anyTaskFailed = true;
171
+ logger.error(e, `take request processing failed for casino ${casinoId}`);
172
+ }
173
+ if (anyTaskFailed) {
174
+ processorState.backoffTime = Math.min(processorState.backoffTime * 2, MAX_BACKOFF_TIME);
175
+ }
176
+ else {
177
+ processorState.backoffTime = MIN_BACKOFF_TIME;
178
+ }
155
179
  }
156
180
  catch (e) {
157
181
  processorState.backoffTime = Math.min(processorState.backoffTime * 2, MAX_BACKOFF_TIME);
@@ -143,10 +143,6 @@ export async function processTransfer({ casinoId, controllerId, transfer, graphq
143
143
  logger.debug(`I sent ${user.uname} ${transfer.amount} base units of ${currency.id}`);
144
144
  switch (transfer.status) {
145
145
  case TransferStatusKind.Canceled:
146
- await db.settleWithdrawal(pool, {
147
- withdrawalId: transfer.id,
148
- newStatus: "CANCELED",
149
- });
150
146
  return;
151
147
  case TransferStatusKind.Unclaimed:
152
148
  throw new Error(`Unexpected UNCLAIMED withdrawal.`);
@@ -90,7 +90,7 @@ export function startWebsocketProcessor({ casinoId, graphqlUrl, signal, controll
90
90
  return;
91
91
  }
92
92
  assert(mpTransfer.__typename === "ExperienceTransfer", `Expected experience transfer but got ${mpTransfer.__typename}`);
93
- processTransfer({
93
+ await processTransfer({
94
94
  casinoId,
95
95
  controllerId,
96
96
  transfer: mpTransfer,
@@ -15,6 +15,8 @@ import { HubUserBalanceByCurrencyPlugin } from "../plugins/hub-user-balance-by-c
15
15
  import { logger } from "../logger.js";
16
16
  import { ValidateCasinoFieldsPlugin } from "../plugins/validate-fields.js";
17
17
  import { HubAddCasinoPlugin } from "../plugins/hub-add-casino.js";
18
+ import { HubAdjustBankrollPlugin } from "../plugins/hub-adjust-bankroll.js";
19
+ import { GraphQLErrorLogPlugin } from "../plugins/graphql-error-log.js";
18
20
  import { HubBalanceAlertPlugin } from "../plugins/hub-balance-alert.js";
19
21
  import { custom as customPgOmitArchivedPlugin } from "@graphile-contrib/pg-omit-archived";
20
22
  import { HubCurrentXPlugin } from "../plugins/hub-current-x.js";
@@ -44,6 +46,8 @@ export const requiredPlugins = [
44
46
  HubUserBalanceByCurrencyPlugin,
45
47
  HubAddCasinoPlugin,
46
48
  ValidateCasinoFieldsPlugin,
49
+ HubAdjustBankrollPlugin,
50
+ GraphQLErrorLogPlugin,
47
51
  HubOutcomeInputNonNullFieldsPlugin,
48
52
  HubBadHashChainErrorPlugin,
49
53
  HubCreateHashChainPlugin,
@@ -23,9 +23,14 @@ export const SmartTagsPlugin = makeJSONPgSmartTagsPlugin({
23
23
  behavior: ["-orderBy"],
24
24
  },
25
25
  },
26
- "hub.bankroll.amount": {
26
+ "hub.user.client_id": {
27
27
  tags: {
28
- behavior: ["+attribute:update"],
28
+ behavior: ["-select", "-filterBy", "-orderBy"],
29
+ },
30
+ },
31
+ "hub.experience.client_id": {
32
+ tags: {
33
+ behavior: ["-select", "-filterBy", "-orderBy"],
29
34
  },
30
35
  },
31
36
  },
@@ -35,11 +40,6 @@ export const SmartTagsPlugin = makeJSONPgSmartTagsPlugin({
35
40
  behavior: ["-resource:select"],
36
41
  },
37
42
  },
38
- "hub.bankroll": {
39
- tags: {
40
- behavior: ["+resource:update"],
41
- },
42
- },
43
43
  "hub.casino": {
44
44
  tags: {
45
45
  behavior: [
@@ -215,8 +215,8 @@ export async function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest,
215
215
  const existingRequest = await pgClient
216
216
  .query({
217
217
  text: `
218
- SELECT id, mp_take_request_id, status, reserved_amount, user_id, experience_id, currency_key
219
- FROM hub.take_request
218
+ SELECT id, mp_take_request_id, status, reserved_amount, user_id, experience_id, currency_key, mp_transfer_id
219
+ FROM hub.take_request
220
220
  WHERE mp_take_request_id = $1 AND casino_id = $2
221
221
  FOR UPDATE
222
222
  `,
@@ -332,6 +332,10 @@ async function processExistingTakeRequest({ pgClient, takeRequest, casinoId, gra
332
332
  logger.info(`[processExistingTakeRequest] Take request ${takeRequest.id} in PENDING state`);
333
333
  break;
334
334
  case LocalTakeRequestStatus.PROCESSING: {
335
+ if (takeRequest.mp_transfer_id) {
336
+ logger.info(`[processExistingTakeRequest] Take request ${takeRequest.id} already has transfer ${takeRequest.mp_transfer_id}`);
337
+ break;
338
+ }
335
339
  const { dbUser, dbExperience, dbCurrency } = await loadRequiredEntities(pgClient, {
336
340
  type: "localId",
337
341
  userId: takeRequest.user_id,
@@ -439,7 +443,6 @@ async function attemptTransfer({ pgClient, takeRequestId, mpTakeRequestId, mpExp
439
443
  catch (error) {
440
444
  logger.error(error, `[attemptTransfer] Error creating transfer`);
441
445
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
442
- const shouldRetry = error instanceof Error && isNetworkError(error);
443
446
  await pgClient.query({
444
447
  text: `
445
448
  UPDATE hub.take_request
@@ -450,41 +453,12 @@ async function attemptTransfer({ pgClient, takeRequestId, mpTakeRequestId, mpExp
450
453
  `,
451
454
  values: [takeRequestId, errorMessage.slice(0, 200)],
452
455
  });
453
- if (!shouldRetry) {
454
- logger.info({ takeRequestId }, `[attemptTransfer] Non-retryable error, marking as FAILED and refunding`);
455
- await pgClient.query({
456
- text: `
457
- UPDATE hub.take_request
458
- SET status = $1,
459
- debug = COALESCE(debug, '') || ' | Non-retryable error, refunding'
460
- WHERE id = $2
461
- `,
462
- values: [LocalTakeRequestStatus.FAILED, takeRequestId],
463
- });
464
- await refundTakeRequest(pgClient, takeRequestId);
465
- return null;
466
- }
467
- const TRANSFER_RETRY_TIMEOUT_SECONDS = 30;
468
- const takeRequestAfterAttempt = await pgClient
469
- .query({
470
- text: `SELECT transfer_first_failure_at FROM hub.take_request WHERE id = $1`,
471
- values: [takeRequestId],
472
- })
473
- .then(exactlyOneRow);
474
- const elapsedMs = Date.now() - takeRequestAfterAttempt.transfer_first_failure_at.getTime();
475
- if (elapsedMs >= TRANSFER_RETRY_TIMEOUT_SECONDS * 1000) {
476
- logger.error({ takeRequestId, elapsedMs }, `[attemptTransfer] Retry timeout (${TRANSFER_RETRY_TIMEOUT_SECONDS}s) reached, marking as FAILED and refunding`);
477
- await pgClient.query({
478
- text: `
479
- UPDATE hub.take_request
480
- SET status = $1,
481
- debug = COALESCE(debug, '') || ' | Transfer retry timeout reached, refunding'
482
- WHERE id = $2
483
- `,
484
- values: [LocalTakeRequestStatus.FAILED, takeRequestId],
485
- });
486
- await refundTakeRequest(pgClient, takeRequestId);
487
- }
456
+ await settleTakeRequestAfterFailedTransfer({
457
+ pgClient,
458
+ graphqlClient,
459
+ takeRequestId,
460
+ mpTakeRequestId,
461
+ });
488
462
  return null;
489
463
  }
490
464
  }
@@ -633,6 +607,10 @@ export async function completeTransfer({ mpTakeRequestId, takeRequestId, mpTrans
633
607
  .then(exactlyOneRow);
634
608
  if (takeRequestData.refunded_at) {
635
609
  logger.warn({ mpTransferId, takeRequestId }, `[completeTransfer] Transfer was already refunded. This should never happen.`);
610
+ await pgClient.query({
611
+ text: `UPDATE hub.take_request SET transfer_needs_completion = FALSE WHERE id = $1`,
612
+ values: [takeRequestId],
613
+ });
636
614
  break;
637
615
  }
638
616
  const dbLockedBalance = await pgClient
@@ -1094,11 +1072,76 @@ async function refundTakeRequest(pgClient, takeRequestId) {
1094
1072
  logger.info({ takeRequestId, amount: takeRequestData.reserved_amount }, `[refundTakeRequest] Successfully refunded`);
1095
1073
  return true;
1096
1074
  }
1075
+ const NETWORK_ERROR_MARKERS = [
1076
+ "ECONNREFUSED",
1077
+ "ECONNRESET",
1078
+ "ETIMEDOUT",
1079
+ "EHOSTUNREACH",
1080
+ "ENETUNREACH",
1081
+ "socket hang up",
1082
+ ];
1097
1083
  function isNetworkError(e) {
1098
- return (e.message?.includes("ECONNREFUSED") ||
1099
- e.message?.includes("ECONNRESET") ||
1100
- e.message?.includes("ETIMEDOUT") ||
1101
- e.message?.includes("EHOSTUNREACH") ||
1102
- e.message?.includes("ENETUNREACH") ||
1103
- e.message?.includes("socket hang up"));
1084
+ for (let cur = e, depth = 0; cur && depth < 5; depth++) {
1085
+ const err = cur;
1086
+ const text = `${String(err.code ?? "")} ${String(err.message ?? "")}`;
1087
+ if (NETWORK_ERROR_MARKERS.some((marker) => text.includes(marker))) {
1088
+ return true;
1089
+ }
1090
+ cur = err.cause;
1091
+ }
1092
+ return false;
1093
+ }
1094
+ async function settleTakeRequestAfterFailedTransfer({ pgClient, graphqlClient, takeRequestId, mpTakeRequestId, }) {
1095
+ let mpTakeRequest = null;
1096
+ try {
1097
+ const rejectResult = await graphqlClient.request(MP_REJECT_TAKE_REQUEST, {
1098
+ mpTakeRequestId,
1099
+ });
1100
+ if (rejectResult.rejectTakeRequest.result.__typename !==
1101
+ "RejectTakeRequestSuccess") {
1102
+ mpTakeRequest = await mpGetTakeRequest(graphqlClient, mpTakeRequestId);
1103
+ if (!mpTakeRequest) {
1104
+ throw new Error(`Take request ${mpTakeRequestId} not found on casino`);
1105
+ }
1106
+ }
1107
+ }
1108
+ catch (e) {
1109
+ logger.error({ err: e, takeRequestId, networkError: isNetworkError(e) }, `[settleTakeRequestAfterFailedTransfer] Transfer outcome unknown, keeping the reservation and retrying`);
1110
+ return;
1111
+ }
1112
+ if (mpTakeRequest?.experienceTransfer) {
1113
+ logger.info({ takeRequestId, mpTransferId: mpTakeRequest.experienceTransfer.id }, `[settleTakeRequestAfterFailedTransfer] Casino already made the transfer`);
1114
+ await pgClient.query({
1115
+ text: `
1116
+ UPDATE hub.take_request
1117
+ SET mp_transfer_id = $1,
1118
+ mp_transfer_status = $2,
1119
+ transfer_needs_completion = TRUE,
1120
+ mp_status = $3
1121
+ WHERE id = $4
1122
+ `,
1123
+ values: [
1124
+ mpTakeRequest.experienceTransfer.id,
1125
+ MpTransferStatus.Pending,
1126
+ mpTakeRequest.status,
1127
+ takeRequestId,
1128
+ ],
1129
+ });
1130
+ return;
1131
+ }
1132
+ const [status, mpStatus] = mpTakeRequest
1133
+ ? [LocalTakeRequestStatus.FAILED, mpTakeRequest.status]
1134
+ : [LocalTakeRequestStatus.REJECTED, MpTakeRequestStatus.ControllerRejected];
1135
+ logger.info({ takeRequestId, mpStatus }, `[settleTakeRequestAfterFailedTransfer] Casino confirmed no transfer, refunding`);
1136
+ await pgClient.query({
1137
+ text: `
1138
+ UPDATE hub.take_request
1139
+ SET status = $1,
1140
+ mp_status = $2,
1141
+ debug = COALESCE(debug, '') || ' | Casino confirmed no transfer, refunding'
1142
+ WHERE id = $3
1143
+ `,
1144
+ values: [status, mpStatus, takeRequestId],
1145
+ });
1146
+ await refundTakeRequest(pgClient, takeRequestId);
1104
1147
  }
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.20.6",
3
+ "version": "1.22.0",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [
7
7
  "moneypot"
8
8
  ],
9
9
  "description": "Official MoneyPot hub server",
10
- "packageManager": "pnpm@10.25.0",
11
10
  "type": "module",
12
11
  "main": "dist/src/index.js",
13
12
  "types": "dist/src/index.d.ts",
@@ -35,25 +34,6 @@
35
34
  "add-casino": "./dist/cli/add-casino.js",
36
35
  "db-migrate": "./dist/cli/db-migrate.js"
37
36
  },
38
- "scripts": {
39
- "build:hub": "rm -rf dist/src && tsc && cp -R src/pg-versions dist/src/ && ./scripts/strip-sql.sh",
40
- "build": "pnpm run build:hub && rm -rf dist/dashboard && pnpm run build-dashboard",
41
- "check": "tsc --noEmit && pnpm run check-dashboard",
42
- "check-tests": "tsc -p tests/tsconfig.json --noEmit",
43
- "check-dashboard": "cd ./dashboard && pnpm run check",
44
- "format": "prettier --write \"**/*.ts\"",
45
- "lint": "eslint --fix . && pnpm run lint-dashboard",
46
- "lint-dashboard": "cd ./dashboard && pnpm run lint",
47
- "prepublishOnly": "pnpm run build",
48
- "codegen": "npx graphql-codegen",
49
- "codegen-watch": "graphql-codegen --watch",
50
- "build-dashboard": "cd ./dashboard && pnpm run build && rm -rf ../dist/dashboard && cp -Rv dist ../dist/dashboard",
51
- "test:startup": "dropdb --if-exists hub-test && createdb hub-test && SUPERUSER_DATABASE_URL=postgres://localhost:5432/hub-test DATABASE_URL=postgres://localhost:5432/hub-test tsx src/test-startup.ts",
52
- "test": "vitest run",
53
- "test:watch": "vitest",
54
- "test:bail": "pnpm test -- --bail --silent --reporter=basic 2>&1 | head -25",
55
- "prepare": "husky"
56
- },
57
37
  "dependencies": {
58
38
  "@graphile-contrib/pg-omit-archived": "4.0.0-rc.1",
59
39
  "@moneypot/hash-herald": "^1.0.0",
@@ -94,5 +74,22 @@
94
74
  "peerDependencies": {
95
75
  "graphile-config": "^1.0.0-rc.2"
96
76
  },
97
- "license": "UNLICENSED"
98
- }
77
+ "license": "UNLICENSED",
78
+ "scripts": {
79
+ "build:hub": "rm -rf dist/src && tsc && cp -R src/pg-versions dist/src/ && ./scripts/strip-sql.sh",
80
+ "build": "pnpm run build:hub && rm -rf dist/dashboard && pnpm run build-dashboard",
81
+ "check": "tsc --noEmit && pnpm run check-dashboard",
82
+ "check-tests": "tsc -p tests/tsconfig.json --noEmit",
83
+ "check-dashboard": "cd ./dashboard && pnpm run check",
84
+ "format": "prettier --write \"**/*.ts\"",
85
+ "lint": "eslint --fix . && pnpm run lint-dashboard",
86
+ "lint-dashboard": "cd ./dashboard && pnpm run lint",
87
+ "codegen": "npx graphql-codegen",
88
+ "codegen-watch": "graphql-codegen --watch",
89
+ "build-dashboard": "cd ./dashboard && pnpm run build && rm -rf ../dist/dashboard && cp -Rv dist ../dist/dashboard",
90
+ "test:startup": "dropdb --if-exists hub-test && createdb hub-test && NODE_ENV=development SUPERUSER_DATABASE_URL=postgres://localhost:5432/hub-test DATABASE_URL=postgres://localhost:5432/hub-test tsx src/test-startup.ts",
91
+ "test": "vitest run",
92
+ "test:watch": "vitest",
93
+ "test:bail": "pnpm test -- --bail --silent --reporter=basic 2>&1 | head -25"
94
+ }
95
+ }