@moneypot/hub 1.5.2 → 1.6.0-dev.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.
@@ -0,0 +1,89 @@
1
+ import { startPollingProcessor } from "./polling-processor.js";
2
+ import { startWebsocketProcessor } from "./websocket-processor.js";
3
+ import { superuserPool } from "../db/index.js";
4
+ import { dbGetCasinoById, dbGetCasinoSecretById } from "../db/internal.js";
5
+ import assert from "assert";
6
+ import { logger } from "../logger.js";
7
+ import * as config from "../config.js";
8
+ import * as db from "../db/index.js";
9
+ import * as pg from "pg";
10
+ import { z } from "zod";
11
+ const activeCasinos = new Set();
12
+ export async function startCasinoTransferProcessor({ casinoId, signal, }) {
13
+ if (activeCasinos.has(casinoId)) {
14
+ throw new Error(`processor already running for casino ${casinoId}`);
15
+ }
16
+ const casino = await dbGetCasinoById(superuserPool, casinoId);
17
+ const secret = await dbGetCasinoSecretById(superuserPool, casinoId);
18
+ assert(casino, `Casino not found for casino id ${casinoId}`);
19
+ assert(secret, `Secret not found for casino id ${casinoId}`);
20
+ activeCasinos.add(casinoId);
21
+ startPollingProcessor({ casinoId, signal });
22
+ startWebsocketProcessor({
23
+ casinoId,
24
+ graphqlUrl: casino.graphql_url,
25
+ signal,
26
+ controllerId: secret.controller_id,
27
+ apiKey: secret.api_key,
28
+ });
29
+ }
30
+ export function initializeTransferProcessors({ signal, }) {
31
+ (async () => {
32
+ try {
33
+ const casinos = await db.listCasinos(superuserPool);
34
+ for (const casino of casinos) {
35
+ if (!URL.canParse(casino.graphql_url)) {
36
+ logger.warn(`Skipping casino ${casino.id} due to invalid graphql_url: "${casino.graphql_url}"`);
37
+ continue;
38
+ }
39
+ if (config.NODE_ENV === "production" &&
40
+ new URL(casino.graphql_url).hostname === "localhost") {
41
+ logger.warn(`${casino.id} has localhost endpoint "${casino.graphql_url}" while NODE_ENV=production.`);
42
+ }
43
+ logger.info(`Starting casino processor for "${casino.name}" at "${casino.graphql_url}"`);
44
+ startCasinoTransferProcessor({ casinoId: casino.id, signal });
45
+ }
46
+ await listenForNewCasinos({ signal });
47
+ }
48
+ catch (e) {
49
+ logger.error(`Error initializing transfer processors:`, e);
50
+ }
51
+ })();
52
+ }
53
+ async function listenForNewCasinos({ signal }) {
54
+ const pgClient = new pg.Client(config.SUPERUSER_DATABASE_URL);
55
+ await pgClient.connect();
56
+ const NewCasinoPayload = z.object({
57
+ id: z.string(),
58
+ });
59
+ pgClient.on("notification", async (msg) => {
60
+ logger.debug(`[listenForNewCasinos] received notification:`, msg);
61
+ switch (msg.channel) {
62
+ case "hub:new_casino": {
63
+ if (!msg.payload) {
64
+ logger.error("hub:new_casino notification has no payload");
65
+ return;
66
+ }
67
+ let json;
68
+ try {
69
+ json = JSON.parse(msg.payload);
70
+ }
71
+ catch (error) {
72
+ logger.error("Error parsing new casino notification:", error);
73
+ }
74
+ const result = NewCasinoPayload.safeParse(json);
75
+ if (!result.success) {
76
+ logger.error("Error parsing new casino notification:", result.error);
77
+ return;
78
+ }
79
+ startCasinoTransferProcessor({ casinoId: result.data.id, signal });
80
+ break;
81
+ }
82
+ }
83
+ });
84
+ signal.addEventListener("abort", () => {
85
+ pgClient.removeAllListeners("notification");
86
+ pgClient.end();
87
+ });
88
+ await pgClient.query("LISTEN new_casino");
89
+ }
@@ -0,0 +1,10 @@
1
+ import * as db from "../db/index.js";
2
+ export declare const PAGINATE_TRANSFERS: import("@graphql-typed-document-node/core").TypedDocumentNode<import("../__generated__/graphql.js").PaginateTransfersQuery, import("../__generated__/graphql.js").Exact<{
3
+ controllerId: import("../__generated__/graphql.js").Scalars["UUID"]["input"];
4
+ after?: import("../__generated__/graphql.js").InputMaybe<import("../__generated__/graphql.js").Scalars["Cursor"]["input"]>;
5
+ limit?: import("../__generated__/graphql.js").InputMaybe<import("../__generated__/graphql.js").Scalars["Int"]["input"]>;
6
+ }>>;
7
+ export declare function startPollingProcessor({ casinoId, signal, }: {
8
+ casinoId: db.DbCasino["id"];
9
+ signal: AbortSignal;
10
+ }): void;
@@ -1,335 +1,59 @@
1
- import * as db from "./db/index.js";
2
- import { GET_CURRENCIES, PAGINATE_TRANSFERS } from "./graphql-queries.js";
3
- import { TransferStatusKind, } from "./__generated__/graphql.js";
4
- import { createGraphqlClient } from "./graphql-client.js";
5
- import EventEmitter from "events";
6
- import { superuserPool } from "./db/index.js";
7
- import { dbGetCasinoById, dbGetCasinoSecretById } from "./db/internal.js";
8
- import pg from "pg";
9
- import * as config from "./config.js";
10
- import { z } from "zod";
11
- import { gql } from "./__generated__/gql.js";
12
- import { logger } from "./logger.js";
1
+ import * as db from "../db/index.js";
2
+ import { logger } from "../logger.js";
13
3
  import { assert } from "tsafe";
14
- import { isUuid } from "./util.js";
15
- import { processWithdrawalRequests } from "./process-withdrawal-request.js";
16
- import * as jwtService from "./services/jwt-service.js";
17
- import { processTakeRequests } from "./take-request/process-take-request.js";
18
- const MP_COMPLETE_TRANSFER = gql(`
19
- mutation CompleteTransfer($mpTransferId: UUID!) {
20
- completeTransfer(input: { id: $mpTransferId }) {
21
- result {
22
- ... on CompleteTransferSuccess {
23
- __typename
24
- transfer {
25
- id
26
- ... on ExperienceTransfer {
27
- id
28
- status
29
- }
30
- }
31
- }
32
- ... on InvalidTransferStatus {
33
- __typename
34
- currentStatus
35
- message
36
- }
4
+ import { isUuid } from "../util.js";
5
+ import { createGraphqlClient } from "../graphql-client.js";
6
+ import { GET_CURRENCIES } from "../graphql-queries.js";
7
+ import { processWithdrawalRequests } from "../process-withdrawal-request.js";
8
+ import { processTakeRequests } from "../take-request/process-take-request.js";
9
+ import * as jwtService from "../services/jwt-service.js";
10
+ import { dbGetCasinoById, dbGetCasinoSecretById } from "../db/internal.js";
11
+ import { superuserPool } from "../db/index.js";
12
+ import { TransferStatusKind } from "../__generated__/graphql.js";
13
+ import { MP_COMPLETE_TRANSFER, processTransfer } from "./process-transfer.js";
14
+ import { gql } from "../__generated__/gql.js";
15
+ import { useFragment } from "../__generated__/fragment-masking.js";
16
+ import { TRANSFER_FIELDS } from "./graphql.js";
17
+ export const PAGINATE_TRANSFERS = gql(`
18
+ query PaginateTransfers(
19
+ $controllerId: UUID!
20
+ $after: Cursor
21
+ $limit: Int = 10
22
+ ) {
23
+ transfersByHolder(
24
+ input: {
25
+ holderId: $controllerId
26
+ after: $after
27
+ first: $limit
28
+ orderBy: ID_ASC
29
+ type: EXPERIENCE
37
30
  }
38
- }
39
- }
40
- `);
41
- const MP_CLAIM_TRANSFER = gql(`
42
- mutation ClaimTransfer($mpTransferId: UUID!) {
43
- claimTransfer(input: { id: $mpTransferId }) {
44
- result {
45
- ... on ClaimTransferSuccess {
46
- __typename
47
- transfer {
48
- id
49
- ... on ExperienceTransfer {
50
- id
51
- status
52
- }
53
- }
54
- }
55
- ... on InvalidTransferStatus {
56
- __typename
57
- currentStatus
58
- message
31
+ ) {
32
+ pageInfo {
33
+ endCursor
34
+ hasNextPage
35
+ }
36
+ edges {
37
+ cursor
38
+ node {
39
+ ...TransferFields
59
40
  }
60
41
  }
61
42
  }
62
43
  }
63
44
  `);
64
- const casinoMap = new Map();
65
45
  const MIN_BACKOFF_TIME = 5000;
66
46
  const MAX_BACKOFF_TIME = 30 * 1000;
67
- async function listenForNewCasinos({ signal }) {
68
- const pgClient = new pg.Client(config.SUPERUSER_DATABASE_URL);
69
- await pgClient.connect();
70
- const NewCasinoPayload = z.object({
71
- id: z.string(),
72
- });
73
- pgClient.on("notification", async (msg) => {
74
- logger.debug(`[listenForNewCasinos] received notification:`, msg);
75
- switch (msg.channel) {
76
- case "hub:new_casino": {
77
- if (!msg.payload) {
78
- logger.error("hub:new_casino notification has no payload");
79
- return;
80
- }
81
- let json;
82
- try {
83
- json = JSON.parse(msg.payload);
84
- }
85
- catch (error) {
86
- logger.error("Error parsing new casino notification:", error);
87
- }
88
- const result = NewCasinoPayload.safeParse(json);
89
- if (!result.success) {
90
- logger.error("Error parsing new casino notification:", result.error);
91
- return;
92
- }
93
- startTransferProcessor({ casinoId: result.data.id, signal });
94
- break;
95
- }
96
- }
97
- });
98
- signal.addEventListener("abort", () => {
99
- pgClient.removeAllListeners("notification");
100
- pgClient.end();
101
- });
102
- await pgClient.query("LISTEN new_casino");
103
- }
104
- async function processTransfer({ casinoId, controllerId, transfer, graphqlClient, }) {
105
- assert(transfer, "Expected transfer");
106
- assert(transfer.__typename === "ExperienceTransfer", `Expected ExperienceTransfer but got ${transfer.__typename}`);
107
- logger.debug(`processing transfer ${transfer.id} for casino ${casinoId}...`);
108
- logger.debug("transfer", transfer);
109
- assert(transfer.experienceByExperienceId, "Expected experienceByExperienceId");
110
- const isIncoming = controllerId === transfer.toHolderId;
111
- const user = isIncoming
112
- ? transfer.holderByFromHolderId
113
- : transfer.holderByToHolderId;
114
- assert(user?.__typename === "User", "Expected user transfer participant");
115
- const currency = transfer.currencyByCurrency;
116
- const dbSender = await db.upsertUser(superuserPool, {
117
- casinoId,
118
- mpUserId: user.id,
119
- uname: user.uname,
120
- });
121
- const dbExperience = await db.upsertExperience(superuserPool, {
122
- casinoId,
123
- mpExperienceId: transfer.experienceByExperienceId.id,
124
- name: transfer.experienceByExperienceId.name,
125
- });
126
- await db.upsertCurrencies(superuserPool, {
127
- casinoId,
128
- currencies: [currency],
129
- });
130
- if (isIncoming) {
131
- logger.debug(`${user.uname} sent me ${transfer.amount} base units of ${currency.id}`);
132
- switch (transfer.status) {
133
- case TransferStatusKind.Pending:
134
- throw new Error(`Unexpected PENDING deposit transfer: ${JSON.stringify(transfer)}`);
135
- case TransferStatusKind.Canceled:
136
- case TransferStatusKind.Expired:
137
- return;
138
- case TransferStatusKind.Completed: {
139
- await db.insertDeposit(superuserPool, {
140
- casinoId,
141
- mpTransferId: transfer.id,
142
- userId: dbSender.id,
143
- experienceId: dbExperience.id,
144
- amount: transfer.amount,
145
- currency: currency.id,
146
- });
147
- return;
148
- }
149
- case TransferStatusKind.Unclaimed: {
150
- let data;
151
- try {
152
- data = await graphqlClient.request(MP_CLAIM_TRANSFER, {
153
- mpTransferId: transfer.id,
154
- });
155
- }
156
- catch (e) {
157
- logger.error(`Error sending claimTransfer(${transfer.id}) to ${casinoId}:`, e);
158
- throw e;
159
- }
160
- logger.debug("MP_CLAIM_TRANSFER response:", data);
161
- if (data.claimTransfer?.result.__typename !== "ClaimTransferSuccess") {
162
- throw new Error(`Failed to claim transfer: ${JSON.stringify(data.claimTransfer)}`);
163
- }
164
- await db.insertDeposit(superuserPool, {
165
- casinoId,
166
- mpTransferId: transfer.id,
167
- userId: dbSender.id,
168
- experienceId: dbExperience.id,
169
- amount: transfer.amount,
170
- currency: currency.id,
171
- });
172
- return;
173
- }
174
- default: {
175
- const exhaustiveCheck = transfer.status;
176
- throw new Error(`Unexpected transfer status: ${exhaustiveCheck}`);
177
- }
178
- }
179
- }
180
- else {
181
- logger.debug(`I sent ${user.uname} ${transfer.amount} base units of ${currency.id}`);
182
- switch (transfer.status) {
183
- case TransferStatusKind.Canceled:
184
- throw new Error("TODO: Unexpected CANCELED withdrawal. Need to refund the user.");
185
- case TransferStatusKind.Unclaimed:
186
- throw new Error(`Unexpected UNCLAIMED withdrawal.`);
187
- case TransferStatusKind.Expired:
188
- throw new Error(`Unexpected EXPIRED withdrawal.`);
189
- case TransferStatusKind.Pending:
190
- case TransferStatusKind.Completed:
191
- return;
192
- default: {
193
- const exhaustiveCheck = transfer.status;
194
- throw new Error(`Unexpected transfer status: ${exhaustiveCheck}`);
195
- }
196
- }
197
- }
198
- }
199
- async function processTransfersUntilEmpty({ afterCursor, graphqlClient, casinoInfo, signal, }) {
200
- let hasNextPage = true;
201
- const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
202
- while (hasNextPage && !signal.aborted) {
203
- await processWithdrawalRequests({
204
- casinoId: casinoInfo.id,
205
- graphqlClient,
206
- });
207
- if (signal.aborted) {
208
- logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
209
- break;
210
- }
211
- const data = await graphqlClient.request(PAGINATE_TRANSFERS, {
212
- controllerId: casinoInfo.controller_id,
213
- limit: 10,
214
- after: afterCursor,
215
- });
216
- const transfers = data.transfersByHolder?.edges.flatMap((x) => x?.node || []) || [];
217
- if (transfers.length === 0) {
218
- break;
219
- }
220
- else {
221
- if (transfers.length > 0) {
222
- logger.debug(`processing ${transfers.length} transfers...`);
223
- }
224
- }
225
- for (const transfer of transfers) {
226
- const cursor = transfer.id;
227
- if (transfer.__typename !== "ExperienceTransfer") {
228
- logger.error(`transfer ${transfer.id} is not an ExperienceTransfer, skipping`);
229
- continue;
230
- }
231
- if (signal.aborted) {
232
- logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
233
- break;
234
- }
235
- await processTransfer({
236
- casinoId: casinoInfo.id,
237
- controllerId: casinoInfo.controller_id,
238
- transfer,
239
- graphqlClient,
240
- });
241
- await db.setTransferCursor(superuserPool, {
242
- cursor,
243
- casinoId: casinoInfo.id,
244
- });
245
- }
246
- hasNextPage = data.transfersByHolder.pageInfo.hasNextPage;
247
- afterCursor = data.transfersByHolder.pageInfo.endCursor || undefined;
248
- if (signal.aborted) {
249
- logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
250
- break;
251
- }
252
- await timeout(1000);
253
- }
254
- return afterCursor;
255
- }
256
- async function processWithdrawals({ abortSignal, casinoId, graphqlClient, }) {
257
- if (abortSignal.aborted) {
258
- return;
259
- }
260
- const withdrawals = await db.getPendingWithdrawals(superuserPool, {
261
- casinoId,
262
- limit: 10,
263
- });
264
- if (withdrawals.length > 0) {
265
- logger.debug(`[sendWithdrawalLoop] withdrawals:`, withdrawals);
266
- }
267
- for (const withdrawal of withdrawals) {
268
- if (abortSignal.aborted) {
269
- break;
270
- }
271
- const response = await graphqlClient.request(MP_COMPLETE_TRANSFER, {
272
- mpTransferId: withdrawal.mp_transfer_id,
273
- });
274
- const __typename = response.completeTransfer?.result.__typename;
275
- assert(__typename, `completeTransfer response missing result`);
276
- switch (__typename) {
277
- case undefined:
278
- break;
279
- case "CompleteTransferSuccess": {
280
- await db.settleWithdrawal({
281
- withdrawalId: withdrawal.id,
282
- newStatus: "COMPLETED",
283
- });
284
- break;
285
- }
286
- case "InvalidTransferStatus": {
287
- logger.error(`Invalid transfer state for transfer ${withdrawal.mp_transfer_id}: ${response.completeTransfer?.result.message}`);
288
- const currentState = response.completeTransfer?.result.currentStatus;
289
- assert(currentState, "Expected currentState");
290
- switch (currentState) {
291
- case TransferStatusKind.Canceled:
292
- case TransferStatusKind.Completed:
293
- await db.settleWithdrawal({
294
- withdrawalId: withdrawal.id,
295
- newStatus: currentState,
296
- });
297
- continue;
298
- case TransferStatusKind.Pending:
299
- case TransferStatusKind.Unclaimed:
300
- case TransferStatusKind.Expired:
301
- throw new Error(`Withdrawal shouldn't have status: ${currentState}`);
302
- default: {
303
- const exhaustive = currentState;
304
- throw new Error(`Unexpected currentState: ${exhaustive}`);
305
- }
306
- }
307
- }
308
- default: {
309
- const exhaustiveCheck = __typename;
310
- throw new Error(`Unexpected completeTransfer result: ${exhaustiveCheck}`);
311
- }
312
- }
313
- }
314
- }
315
- export function casinoIdsInProcess() {
316
- return Array.from(casinoMap.keys());
317
- }
318
- export function startTransferProcessor({ casinoId, signal, }) {
47
+ export function startPollingProcessor({ casinoId, signal, }) {
319
48
  if (signal.aborted) {
320
49
  logger.info(`[startTransferProcessor] AbortSignal aborted. Not starting processor for casino ${casinoId}`);
321
50
  return;
322
51
  }
323
52
  logger.info(`starting processor for casino ${casinoId}`);
324
- if (casinoMap.has(casinoId)) {
325
- throw new Error(`processor already running for casino ${casinoId}`);
326
- }
327
53
  const processorState = {
328
- emitter: new EventEmitter(),
329
54
  backoffTime: MIN_BACKOFF_TIME,
330
55
  lastAttempt: 0,
331
56
  };
332
- casinoMap.set(casinoId, processorState);
333
57
  (async () => {
334
58
  let cursor = await db.getTransferCursor(superuserPool, {
335
59
  casinoId,
@@ -345,7 +69,7 @@ export function startTransferProcessor({ casinoId, signal, }) {
345
69
  }
346
70
  let upsertedCurrencies = false;
347
71
  let shouldStop = false;
348
- processorState.emitter.on("once", () => {
72
+ signal.addEventListener("abort", () => {
349
73
  shouldStop = true;
350
74
  });
351
75
  while (!shouldStop && !signal.aborted) {
@@ -372,7 +96,7 @@ export function startTransferProcessor({ casinoId, signal, }) {
372
96
  apiKey: casinoSecret.api_key,
373
97
  });
374
98
  if (signal.aborted) {
375
- logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
99
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (1)`);
376
100
  break;
377
101
  }
378
102
  await jwtService.refreshCasinoJwksTask(superuserPool, {
@@ -381,7 +105,7 @@ export function startTransferProcessor({ casinoId, signal, }) {
381
105
  });
382
106
  if (!upsertedCurrencies) {
383
107
  if (signal.aborted) {
384
- logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
108
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (2)`);
385
109
  break;
386
110
  }
387
111
  const currencies = await graphqlClient
@@ -398,7 +122,7 @@ export function startTransferProcessor({ casinoId, signal, }) {
398
122
  upsertedCurrencies = true;
399
123
  }
400
124
  if (signal.aborted) {
401
- logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
125
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (3)`);
402
126
  break;
403
127
  }
404
128
  cursor = await processTransfersUntilEmpty({
@@ -408,7 +132,7 @@ export function startTransferProcessor({ casinoId, signal, }) {
408
132
  signal,
409
133
  });
410
134
  if (signal.aborted) {
411
- logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
135
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (4)`);
412
136
  break;
413
137
  }
414
138
  await processWithdrawals({
@@ -417,7 +141,7 @@ export function startTransferProcessor({ casinoId, signal, }) {
417
141
  graphqlClient,
418
142
  });
419
143
  if (signal.aborted) {
420
- logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
144
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (5)`);
421
145
  break;
422
146
  }
423
147
  await processTakeRequests({
@@ -435,37 +159,122 @@ export function startTransferProcessor({ casinoId, signal, }) {
435
159
  }
436
160
  }
437
161
  logger.info(`processor stopped for casino ${casinoId}`);
438
- casinoMap.delete(casinoId);
439
162
  })();
440
163
  }
441
- export function stopTransferProcessor(casinoId) {
442
- const processorState = casinoMap.get(casinoId);
443
- if (!processorState) {
444
- logger.warn(`processor not running for casino ${casinoId}`);
164
+ async function processWithdrawals({ abortSignal, casinoId, graphqlClient, }) {
165
+ if (abortSignal.aborted) {
445
166
  return;
446
167
  }
447
- processorState.emitter.emit("stop");
448
- }
449
- export function initializeTransferProcessors({ signal, }) {
450
- (async () => {
451
- try {
452
- const casinos = await db.listCasinos(superuserPool);
453
- for (const casino of casinos) {
454
- if (!URL.canParse(casino.graphql_url)) {
455
- logger.warn(`Skipping casino ${casino.id} due to invalid graphql_url: "${casino.graphql_url}"`);
456
- continue;
457
- }
458
- if (config.NODE_ENV === "production" &&
459
- new URL(casino.graphql_url).hostname === "localhost") {
460
- logger.warn(`${casino.id} has localhost endpoint "${casino.graphql_url}" while NODE_ENV=production.`);
168
+ const withdrawals = await db.getPendingWithdrawals(superuserPool, {
169
+ casinoId,
170
+ limit: 10,
171
+ });
172
+ if (withdrawals.length > 0) {
173
+ logger.debug(`[sendWithdrawalLoop] withdrawals:`, withdrawals);
174
+ }
175
+ for (const withdrawal of withdrawals) {
176
+ if (abortSignal.aborted) {
177
+ break;
178
+ }
179
+ const response = await graphqlClient.request(MP_COMPLETE_TRANSFER, {
180
+ mpTransferId: withdrawal.mp_transfer_id,
181
+ });
182
+ const __typename = response.completeTransfer?.result.__typename;
183
+ assert(__typename, `completeTransfer response missing result`);
184
+ switch (__typename) {
185
+ case undefined:
186
+ break;
187
+ case "CompleteTransferSuccess": {
188
+ await db.settleWithdrawal({
189
+ withdrawalId: withdrawal.id,
190
+ newStatus: "COMPLETED",
191
+ });
192
+ break;
193
+ }
194
+ case "InvalidTransferStatus": {
195
+ logger.error(`Invalid transfer state for transfer ${withdrawal.mp_transfer_id}: ${response.completeTransfer?.result.message}`);
196
+ const currentState = response.completeTransfer?.result.currentStatus;
197
+ assert(currentState, "Expected currentState");
198
+ switch (currentState) {
199
+ case TransferStatusKind.Canceled:
200
+ case TransferStatusKind.Completed:
201
+ await db.settleWithdrawal({
202
+ withdrawalId: withdrawal.id,
203
+ newStatus: currentState,
204
+ });
205
+ continue;
206
+ case TransferStatusKind.Pending:
207
+ case TransferStatusKind.Unclaimed:
208
+ case TransferStatusKind.Expired:
209
+ throw new Error(`Withdrawal shouldn't have status: ${currentState}`);
210
+ default: {
211
+ const exhaustive = currentState;
212
+ throw new Error(`Unexpected currentState: ${exhaustive}`);
213
+ }
461
214
  }
462
- logger.info(`Starting casino processor for "${casino.name}" at "${casino.graphql_url}"`);
463
- startTransferProcessor({ casinoId: casino.id, signal });
464
215
  }
465
- await listenForNewCasinos({ signal });
216
+ default: {
217
+ const exhaustiveCheck = __typename;
218
+ throw new Error(`Unexpected completeTransfer result: ${exhaustiveCheck}`);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ async function processTransfersUntilEmpty({ afterCursor, graphqlClient, casinoInfo, signal, }) {
224
+ let hasNextPage = true;
225
+ const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
226
+ while (hasNextPage && !signal.aborted) {
227
+ await processWithdrawalRequests({
228
+ casinoId: casinoInfo.id,
229
+ graphqlClient,
230
+ });
231
+ if (signal.aborted) {
232
+ logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
233
+ break;
466
234
  }
467
- catch (e) {
468
- logger.error(`Error initializing transfer processors:`, e);
235
+ const data = await graphqlClient.request(PAGINATE_TRANSFERS, {
236
+ controllerId: casinoInfo.controller_id,
237
+ limit: 10,
238
+ after: afterCursor,
239
+ });
240
+ const transfers = data.transfersByHolder?.edges.flatMap((x) => x?.node || []) || [];
241
+ if (transfers.length === 0) {
242
+ break;
469
243
  }
470
- })();
244
+ else {
245
+ if (transfers.length > 0) {
246
+ logger.debug(`processing ${transfers.length} transfers...`);
247
+ }
248
+ }
249
+ for (const transferRef of transfers) {
250
+ const transfer = useFragment(TRANSFER_FIELDS, transferRef);
251
+ const cursor = transfer.id;
252
+ if (transfer.__typename !== "ExperienceTransfer") {
253
+ logger.error(`transfer ${transfer.id} is not an ExperienceTransfer, skipping`);
254
+ continue;
255
+ }
256
+ if (signal.aborted) {
257
+ logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
258
+ break;
259
+ }
260
+ await processTransfer({
261
+ casinoId: casinoInfo.id,
262
+ controllerId: casinoInfo.controller_id,
263
+ transfer,
264
+ graphqlClient,
265
+ });
266
+ await db.setTransferCursor(superuserPool, {
267
+ cursor,
268
+ casinoId: casinoInfo.id,
269
+ });
270
+ }
271
+ hasNextPage = data.transfersByHolder.pageInfo.hasNextPage;
272
+ afterCursor = data.transfersByHolder.pageInfo.endCursor || undefined;
273
+ if (signal.aborted) {
274
+ logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
275
+ break;
276
+ }
277
+ await timeout(1000);
278
+ }
279
+ return afterCursor;
471
280
  }