@moneypot/hub 1.5.1 → 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.
- package/README.md +5 -1
- package/dist/src/__generated__/gql.d.ts +16 -4
- package/dist/src/__generated__/gql.js +8 -2
- package/dist/src/__generated__/graphql.d.ts +475 -140
- package/dist/src/__generated__/graphql.js +34 -2
- package/dist/src/db/index.d.ts +2 -2
- package/dist/src/db/index.js +9 -6
- package/dist/src/graphql-queries.d.ts +0 -5
- package/dist/src/graphql-queries.js +0 -61
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +15 -11
- package/dist/src/plugins/hub-add-casino.js +2 -2
- package/dist/src/process-transfers/graphql.d.ts +1 -0
- package/dist/src/process-transfers/graphql.js +39 -0
- package/dist/src/process-transfers/index.d.ts +9 -0
- package/dist/src/process-transfers/index.js +89 -0
- package/dist/src/process-transfers/polling-processor.d.ts +10 -0
- package/dist/src/{process-transfers.js → process-transfers/polling-processor.js} +154 -345
- package/dist/src/process-transfers/process-transfer.d.ts +13 -0
- package/dist/src/process-transfers/process-transfer.js +151 -0
- package/dist/src/process-transfers/websocket-processor.d.ts +7 -0
- package/dist/src/process-transfers/websocket-processor.js +133 -0
- package/dist/src/services/jwt-service.js +0 -4
- package/dist/src/take-request/process-take-request.d.ts +9 -0
- package/dist/src/take-request/process-take-request.js +43 -15
- package/package.json +3 -2
- package/dist/src/process-transfers.d.ts +0 -10
|
@@ -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 "
|
|
2
|
-
import {
|
|
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 "
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
-
|
|
468
|
-
|
|
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
|
}
|