@moneypot/hub 0.0.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 +108 -0
- package/dist/cli/add-casino.d.ts +2 -0
- package/dist/cli/add-casino.js +116 -0
- package/dist/dashboard/assets/index-BtrbrisP.js +360 -0
- package/dist/dashboard/assets/index-tK7EUtyc.css +5 -0
- package/dist/dashboard/index.html +13 -0
- package/dist/src/GraphQLError.d.ts +8 -0
- package/dist/src/GraphQLError.js +79 -0
- package/dist/src/__generated__/fragment-masking.d.ts +19 -0
- package/dist/src/__generated__/fragment-masking.js +16 -0
- package/dist/src/__generated__/gql.d.ts +26 -0
- package/dist/src/__generated__/gql.js +15 -0
- package/dist/src/__generated__/graphql.d.ts +3129 -0
- package/dist/src/__generated__/graphql.js +454 -0
- package/dist/src/__generated__/index.d.ts +2 -0
- package/dist/src/__generated__/index.js +2 -0
- package/dist/src/config.d.ts +14 -0
- package/dist/src/config.js +57 -0
- package/dist/src/db/index.d.ts +89 -0
- package/dist/src/db/index.js +339 -0
- package/dist/src/db/internal.d.ts +7 -0
- package/dist/src/db/internal.js +33 -0
- package/dist/src/db/public.d.ts +7 -0
- package/dist/src/db/public.js +20 -0
- package/dist/src/db/types.d.ts +80 -0
- package/dist/src/db/types.js +1 -0
- package/dist/src/db/util.d.ts +6 -0
- package/dist/src/db/util.js +9 -0
- package/dist/src/express.d.ts +13 -0
- package/dist/src/express.js +1 -0
- package/dist/src/grafast.d.ts +1 -0
- package/dist/src/grafast.js +1 -0
- package/dist/src/graphile.d.ts +1 -0
- package/dist/src/graphile.js +1 -0
- package/dist/src/graphql-client.d.ts +6 -0
- package/dist/src/graphql-client.js +8 -0
- package/dist/src/graphql-queries.d.ts +18 -0
- package/dist/src/graphql-queries.js +123 -0
- package/dist/src/graphql.d.ts +1 -0
- package/dist/src/graphql.js +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.js +65 -0
- package/dist/src/logger.d.ts +9 -0
- package/dist/src/logger.js +21 -0
- package/dist/src/pg-versions/001-schema.sql +456 -0
- package/dist/src/plugins/caas-add-casino.d.ts +1 -0
- package/dist/src/plugins/caas-add-casino.js +150 -0
- package/dist/src/plugins/caas-authenticate.d.ts +1 -0
- package/dist/src/plugins/caas-authenticate.js +175 -0
- package/dist/src/plugins/caas-balance-alert.d.ts +1 -0
- package/dist/src/plugins/caas-balance-alert.js +43 -0
- package/dist/src/plugins/caas-claim-faucet.d.ts +1 -0
- package/dist/src/plugins/caas-claim-faucet.js +85 -0
- package/dist/src/plugins/caas-current-x.d.ts +1 -0
- package/dist/src/plugins/caas-current-x.js +62 -0
- package/dist/src/plugins/caas-schema-prefix.d.ts +1 -0
- package/dist/src/plugins/caas-schema-prefix.js +25 -0
- package/dist/src/plugins/caas-user-balance-by-currency.d.ts +1 -0
- package/dist/src/plugins/caas-user-balance-by-currency.js +55 -0
- package/dist/src/plugins/caas-withdraw.d.ts +1 -0
- package/dist/src/plugins/caas-withdraw.js +133 -0
- package/dist/src/plugins/debug.d.ts +1 -0
- package/dist/src/plugins/debug.js +14 -0
- package/dist/src/plugins/hub-add-casino.d.ts +1 -0
- package/dist/src/plugins/hub-add-casino.js +150 -0
- package/dist/src/plugins/hub-authenticate.d.ts +1 -0
- package/dist/src/plugins/hub-authenticate.js +175 -0
- package/dist/src/plugins/hub-balance-alert.d.ts +1 -0
- package/dist/src/plugins/hub-balance-alert.js +43 -0
- package/dist/src/plugins/hub-claim-faucet.d.ts +1 -0
- package/dist/src/plugins/hub-claim-faucet.js +85 -0
- package/dist/src/plugins/hub-current-x.d.ts +1 -0
- package/dist/src/plugins/hub-current-x.js +62 -0
- package/dist/src/plugins/hub-schema-prefix.d.ts +1 -0
- package/dist/src/plugins/hub-schema-prefix.js +25 -0
- package/dist/src/plugins/hub-user-balance-by-currency.d.ts +1 -0
- package/dist/src/plugins/hub-user-balance-by-currency.js +55 -0
- package/dist/src/plugins/hub-withdraw.d.ts +1 -0
- package/dist/src/plugins/hub-withdraw.js +133 -0
- package/dist/src/plugins/id-to-node-id.d.ts +1 -0
- package/dist/src/plugins/id-to-node-id.js +31 -0
- package/dist/src/plugins/validate-fields.d.ts +1 -0
- package/dist/src/plugins/validate-fields.js +61 -0
- package/dist/src/process-transfers.d.ts +7 -0
- package/dist/src/process-transfers.js +413 -0
- package/dist/src/process-withdrawal-request.d.ts +5 -0
- package/dist/src/process-withdrawal-request.js +129 -0
- package/dist/src/server/graphile.config.d.ts +33 -0
- package/dist/src/server/graphile.config.js +166 -0
- package/dist/src/server/handle-errors.d.ts +10 -0
- package/dist/src/server/handle-errors.js +88 -0
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/server/index.js +69 -0
- package/dist/src/server/middleware/authentication.d.ts +4 -0
- package/dist/src/server/middleware/authentication.js +55 -0
- package/dist/src/server/middleware/cors.d.ts +3 -0
- package/dist/src/server/middleware/cors.js +14 -0
- package/dist/src/services/jwt-service.d.ts +13 -0
- package/dist/src/services/jwt-service.js +131 -0
- package/dist/src/smart-tags.d.ts +1 -0
- package/dist/src/smart-tags.js +55 -0
- package/dist/src/util.d.ts +12 -0
- package/dist/src/util.js +4 -0
- package/dist/src/validate.d.ts +9 -0
- package/dist/src/validate.js +91 -0
- package/package.json +69 -0
|
@@ -0,0 +1,413 @@
|
|
|
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 config from "./config.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { gql } from "./__generated__/gql.js";
|
|
12
|
+
import { logger } from "./logger.js";
|
|
13
|
+
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
|
+
const MP_COMPLETE_TRANSFER = gql(`
|
|
18
|
+
mutation CompleteTransfer($mpTransferId: UUID!) {
|
|
19
|
+
completeTransfer(input: { id: $mpTransferId }) {
|
|
20
|
+
result {
|
|
21
|
+
... on CompleteTransferSuccess {
|
|
22
|
+
__typename
|
|
23
|
+
transfer {
|
|
24
|
+
id
|
|
25
|
+
... on ExperienceTransfer {
|
|
26
|
+
id
|
|
27
|
+
status
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
... on InvalidTransferStatus {
|
|
32
|
+
__typename
|
|
33
|
+
currentStatus
|
|
34
|
+
message
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`);
|
|
40
|
+
const MP_CLAIM_TRANSFER = gql(`
|
|
41
|
+
mutation ClaimTransfer($mpTransferId: UUID!) {
|
|
42
|
+
claimTransfer(input: { id: $mpTransferId }) {
|
|
43
|
+
result {
|
|
44
|
+
... on ClaimTransferSuccess {
|
|
45
|
+
__typename
|
|
46
|
+
transfer {
|
|
47
|
+
id
|
|
48
|
+
... on ExperienceTransfer {
|
|
49
|
+
id
|
|
50
|
+
status
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
... on InvalidTransferStatus {
|
|
55
|
+
__typename
|
|
56
|
+
currentStatus
|
|
57
|
+
message
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`);
|
|
63
|
+
const casinoMap = new Map();
|
|
64
|
+
const MIN_BACKOFF_TIME = 5000;
|
|
65
|
+
const MAX_BACKOFF_TIME = 30 * 1000;
|
|
66
|
+
async function listenForNewCasinos() {
|
|
67
|
+
const pgClient = new pg.Client(config.SUPERUSER_DATABASE_URL);
|
|
68
|
+
await pgClient.connect();
|
|
69
|
+
const NewCasinoPayload = z.object({
|
|
70
|
+
id: z.string(),
|
|
71
|
+
});
|
|
72
|
+
pgClient.on("notification", async (msg) => {
|
|
73
|
+
logger.debug(`[listenForNewCasinos] received notification:`, msg);
|
|
74
|
+
switch (msg.channel) {
|
|
75
|
+
case "hub:new_casino": {
|
|
76
|
+
if (!msg.payload) {
|
|
77
|
+
logger.error("hub:new_casino notification has no payload");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let json;
|
|
81
|
+
try {
|
|
82
|
+
json = JSON.parse(msg.payload);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
logger.error("Error parsing new casino notification:", error);
|
|
86
|
+
}
|
|
87
|
+
const result = NewCasinoPayload.safeParse(json);
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
logger.error("Error parsing new casino notification:", result.error);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
startTransferProcessor({ casinoId: result.data.id });
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
await pgClient.query("LISTEN new_casino");
|
|
98
|
+
}
|
|
99
|
+
async function processTransfer({ casinoId, controllerId, transfer, graphqlClient, }) {
|
|
100
|
+
assert(transfer, "Expected transfer");
|
|
101
|
+
assert(transfer.__typename === "ExperienceTransfer", `Expected ExperienceTransfer but got ${transfer.__typename}`);
|
|
102
|
+
logger.debug(`processing transfer ${transfer.id} for casino ${casinoId}...`);
|
|
103
|
+
logger.debug("transfer", transfer);
|
|
104
|
+
assert(transfer.experienceByExperienceId, "Expected experienceByExperienceId");
|
|
105
|
+
const isIncoming = controllerId === transfer.toHolderId;
|
|
106
|
+
const user = isIncoming
|
|
107
|
+
? transfer.holderByFromHolderId
|
|
108
|
+
: transfer.holderByToHolderId;
|
|
109
|
+
assert(user?.__typename === "User", "Expected user transfer participant");
|
|
110
|
+
const currency = transfer.currencyByCurrency;
|
|
111
|
+
const dbSender = await db.upsertUser(superuserPool, {
|
|
112
|
+
casinoId,
|
|
113
|
+
mpUserId: user.id,
|
|
114
|
+
uname: user.uname,
|
|
115
|
+
});
|
|
116
|
+
const dbExperience = await db.upsertExperience(superuserPool, {
|
|
117
|
+
casinoId,
|
|
118
|
+
mpExperienceId: transfer.experienceByExperienceId.id,
|
|
119
|
+
name: transfer.experienceByExperienceId.name,
|
|
120
|
+
});
|
|
121
|
+
await db.upsertCurrencies(superuserPool, {
|
|
122
|
+
casinoId,
|
|
123
|
+
currencies: [currency],
|
|
124
|
+
});
|
|
125
|
+
if (isIncoming) {
|
|
126
|
+
logger.debug(`${user.uname} sent me ${transfer.amount} base units of ${currency.id}`);
|
|
127
|
+
switch (transfer.status) {
|
|
128
|
+
case TransferStatusKind.Pending:
|
|
129
|
+
throw new Error(`Unexpected PENDING deposit transfer: ${JSON.stringify(transfer)}`);
|
|
130
|
+
case TransferStatusKind.Canceled:
|
|
131
|
+
case TransferStatusKind.Expired:
|
|
132
|
+
return;
|
|
133
|
+
case TransferStatusKind.Completed: {
|
|
134
|
+
await db.insertDeposit(superuserPool, {
|
|
135
|
+
casinoId,
|
|
136
|
+
mpTransferId: transfer.id,
|
|
137
|
+
userId: dbSender.id,
|
|
138
|
+
experienceId: dbExperience.id,
|
|
139
|
+
amount: transfer.amount,
|
|
140
|
+
currency: currency.id,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
case TransferStatusKind.Unclaimed: {
|
|
145
|
+
let data;
|
|
146
|
+
try {
|
|
147
|
+
data = await graphqlClient.request(MP_CLAIM_TRANSFER, {
|
|
148
|
+
mpTransferId: transfer.id,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
logger.error(`Error sending claimTransfer(${transfer.id}) to ${casinoId}:`, e);
|
|
153
|
+
throw e;
|
|
154
|
+
}
|
|
155
|
+
logger.debug("MP_CLAIM_TRANSFER response:", data);
|
|
156
|
+
if (data.claimTransfer?.result.__typename !== "ClaimTransferSuccess") {
|
|
157
|
+
throw new Error(`Failed to claim transfer: ${JSON.stringify(data.claimTransfer)}`);
|
|
158
|
+
}
|
|
159
|
+
await db.insertDeposit(superuserPool, {
|
|
160
|
+
casinoId,
|
|
161
|
+
mpTransferId: transfer.id,
|
|
162
|
+
userId: dbSender.id,
|
|
163
|
+
experienceId: dbExperience.id,
|
|
164
|
+
amount: transfer.amount,
|
|
165
|
+
currency: currency.id,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
default: {
|
|
170
|
+
const exhaustiveCheck = transfer.status;
|
|
171
|
+
throw new Error(`Unexpected transfer status: ${exhaustiveCheck}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
logger.debug(`I sent ${user.uname} ${transfer.amount} base units of ${currency.id}`);
|
|
177
|
+
switch (transfer.status) {
|
|
178
|
+
case TransferStatusKind.Canceled:
|
|
179
|
+
throw new Error("TODO: Unexpected CANCELED withdrawal. Need to refund the user.");
|
|
180
|
+
case TransferStatusKind.Unclaimed:
|
|
181
|
+
throw new Error(`Unexpected UNCLAIMED withdrawal.`);
|
|
182
|
+
case TransferStatusKind.Expired:
|
|
183
|
+
throw new Error(`Unexpected EXPIRED withdrawal.`);
|
|
184
|
+
case TransferStatusKind.Pending:
|
|
185
|
+
case TransferStatusKind.Completed:
|
|
186
|
+
return;
|
|
187
|
+
default: {
|
|
188
|
+
const exhaustiveCheck = transfer.status;
|
|
189
|
+
throw new Error(`Unexpected transfer status: ${exhaustiveCheck}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function processTransfersUntilEmpty({ afterCursor, graphqlClient, casinoInfo, }) {
|
|
195
|
+
let hasNextPage = true;
|
|
196
|
+
const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
197
|
+
while (hasNextPage) {
|
|
198
|
+
await processWithdrawalRequests({
|
|
199
|
+
casinoId: casinoInfo.id,
|
|
200
|
+
graphqlClient,
|
|
201
|
+
});
|
|
202
|
+
const data = await graphqlClient.request(PAGINATE_TRANSFERS, {
|
|
203
|
+
controllerId: casinoInfo.controller_id,
|
|
204
|
+
limit: 10,
|
|
205
|
+
after: afterCursor,
|
|
206
|
+
});
|
|
207
|
+
const transfers = data.transfersByHolder?.edges.flatMap((x) => x?.node || []) || [];
|
|
208
|
+
if (transfers.length === 0) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
if (transfers.length > 0) {
|
|
213
|
+
logger.debug(`processing ${transfers.length} transfers...`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const transfer of transfers) {
|
|
217
|
+
const cursor = transfer.id;
|
|
218
|
+
if (transfer.__typename !== "ExperienceTransfer") {
|
|
219
|
+
logger.error(`transfer ${transfer.id} is not an ExperienceTransfer, skipping`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
await processTransfer({
|
|
223
|
+
casinoId: casinoInfo.id,
|
|
224
|
+
controllerId: casinoInfo.controller_id,
|
|
225
|
+
transfer,
|
|
226
|
+
graphqlClient,
|
|
227
|
+
});
|
|
228
|
+
await db.setTransferCursor(superuserPool, {
|
|
229
|
+
cursor,
|
|
230
|
+
casinoId: casinoInfo.id,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
hasNextPage = data.transfersByHolder.pageInfo.hasNextPage;
|
|
234
|
+
afterCursor = data.transfersByHolder.pageInfo.endCursor || undefined;
|
|
235
|
+
await timeout(1000);
|
|
236
|
+
}
|
|
237
|
+
return afterCursor;
|
|
238
|
+
}
|
|
239
|
+
async function processWithdrawals({ casinoId, graphqlClient, }) {
|
|
240
|
+
const withdrawals = await db.getPendingWithdrawals(superuserPool, {
|
|
241
|
+
casinoId,
|
|
242
|
+
limit: 10,
|
|
243
|
+
});
|
|
244
|
+
if (withdrawals.length > 0) {
|
|
245
|
+
logger.debug(`[sendWithdrawalLoop] withdrawals:`, withdrawals);
|
|
246
|
+
}
|
|
247
|
+
for (const withdrawal of withdrawals) {
|
|
248
|
+
const response = await graphqlClient.request(MP_COMPLETE_TRANSFER, {
|
|
249
|
+
mpTransferId: withdrawal.mp_transfer_id,
|
|
250
|
+
});
|
|
251
|
+
const __typename = response.completeTransfer?.result.__typename;
|
|
252
|
+
assert(__typename, `completeTransfer response missing result`);
|
|
253
|
+
switch (__typename) {
|
|
254
|
+
case undefined:
|
|
255
|
+
break;
|
|
256
|
+
case "CompleteTransferSuccess": {
|
|
257
|
+
await db.settleWithdrawal({
|
|
258
|
+
withdrawalId: withdrawal.id,
|
|
259
|
+
newStatus: "COMPLETED",
|
|
260
|
+
});
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case "InvalidTransferStatus": {
|
|
264
|
+
logger.error(`Invalid transfer state for transfer ${withdrawal.mp_transfer_id}: ${response.completeTransfer?.result.message}`);
|
|
265
|
+
const currentState = response.completeTransfer?.result.currentStatus;
|
|
266
|
+
assert(currentState, "Expected currentState");
|
|
267
|
+
switch (currentState) {
|
|
268
|
+
case TransferStatusKind.Canceled:
|
|
269
|
+
case TransferStatusKind.Completed:
|
|
270
|
+
await db.settleWithdrawal({
|
|
271
|
+
withdrawalId: withdrawal.id,
|
|
272
|
+
newStatus: currentState,
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
case TransferStatusKind.Pending:
|
|
276
|
+
case TransferStatusKind.Unclaimed:
|
|
277
|
+
case TransferStatusKind.Expired:
|
|
278
|
+
throw new Error(`Withdrawal shouldn't have status: ${currentState}`);
|
|
279
|
+
default: {
|
|
280
|
+
const exhaustive = currentState;
|
|
281
|
+
throw new Error(`Unexpected currentState: ${exhaustive}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
default: {
|
|
286
|
+
const exhaustiveCheck = __typename;
|
|
287
|
+
throw new Error(`Unexpected completeTransfer result: ${exhaustiveCheck}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export function casinoIdsInProcess() {
|
|
293
|
+
return Array.from(casinoMap.keys());
|
|
294
|
+
}
|
|
295
|
+
export function startTransferProcessor({ casinoId, }) {
|
|
296
|
+
logger.info(`starting processor for casino ${casinoId}`);
|
|
297
|
+
if (casinoMap.has(casinoId)) {
|
|
298
|
+
throw new Error(`processor already running for casino ${casinoId}`);
|
|
299
|
+
}
|
|
300
|
+
const processorState = {
|
|
301
|
+
emitter: new EventEmitter(),
|
|
302
|
+
backoffTime: MIN_BACKOFF_TIME,
|
|
303
|
+
lastAttempt: 0,
|
|
304
|
+
};
|
|
305
|
+
casinoMap.set(casinoId, processorState);
|
|
306
|
+
(async () => {
|
|
307
|
+
let cursor = await db.getTransferCursor(superuserPool, {
|
|
308
|
+
casinoId,
|
|
309
|
+
});
|
|
310
|
+
if (cursor && !isUuid(cursor)) {
|
|
311
|
+
logger.info(`Migrating from old transfer cursor format: "${cursor}"...`);
|
|
312
|
+
const json = Buffer.from(cursor, "base64").toString("utf8");
|
|
313
|
+
const parsed = JSON.parse(json);
|
|
314
|
+
assert(Array.isArray(parsed), `Expected premigrated transfer cursor to decode into json array, got ${json}`);
|
|
315
|
+
const transferId = parsed[1];
|
|
316
|
+
logger.info(`Migrated cursor: ${cursor} => ${transferId}`);
|
|
317
|
+
cursor = transferId;
|
|
318
|
+
}
|
|
319
|
+
let upsertedCurrencies = false;
|
|
320
|
+
let shouldStop = false;
|
|
321
|
+
processorState.emitter.on("once", () => {
|
|
322
|
+
shouldStop = true;
|
|
323
|
+
});
|
|
324
|
+
while (!shouldStop) {
|
|
325
|
+
try {
|
|
326
|
+
const now = Date.now();
|
|
327
|
+
const timeToWait = Math.max(0, processorState.backoffTime - (now - processorState.lastAttempt));
|
|
328
|
+
if (timeToWait > 0) {
|
|
329
|
+
await new Promise((resolve) => setTimeout(resolve, timeToWait));
|
|
330
|
+
}
|
|
331
|
+
processorState.lastAttempt = Date.now();
|
|
332
|
+
const casino = await dbGetCasinoById(superuserPool, casinoId);
|
|
333
|
+
if (!casino) {
|
|
334
|
+
logger.warn(`casino ${casinoId} not found. Stopping processor...`);
|
|
335
|
+
shouldStop = true;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
const casinoSecret = await dbGetCasinoSecretById(superuserPool, casino.id);
|
|
339
|
+
if (!casinoSecret) {
|
|
340
|
+
logger.warn(`Casino secret hasn't been set for casino ${casinoId}. Skipping...`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const graphqlClient = createGraphqlClient({
|
|
344
|
+
graphqlUrl: casino.graphql_url,
|
|
345
|
+
apiKey: casinoSecret.api_key,
|
|
346
|
+
});
|
|
347
|
+
await jwtService.refreshCasinoJwksTask(superuserPool, {
|
|
348
|
+
casinoId: casino.id,
|
|
349
|
+
graphqlClient,
|
|
350
|
+
});
|
|
351
|
+
if (!upsertedCurrencies) {
|
|
352
|
+
const currencies = await graphqlClient
|
|
353
|
+
.request(GET_CURRENCIES)
|
|
354
|
+
.then((res) => {
|
|
355
|
+
return (res.allCurrencies?.nodes || []).flatMap((x) => x ? [x] : []);
|
|
356
|
+
});
|
|
357
|
+
if (currencies.length > 0) {
|
|
358
|
+
await db.upsertCurrencies(superuserPool, {
|
|
359
|
+
casinoId: casino.id,
|
|
360
|
+
currencies,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
upsertedCurrencies = true;
|
|
364
|
+
}
|
|
365
|
+
cursor = await processTransfersUntilEmpty({
|
|
366
|
+
afterCursor: cursor,
|
|
367
|
+
graphqlClient,
|
|
368
|
+
casinoInfo: { ...casino, ...casinoSecret },
|
|
369
|
+
});
|
|
370
|
+
await processWithdrawals({ casinoId: casino.id, graphqlClient });
|
|
371
|
+
processorState.backoffTime = MIN_BACKOFF_TIME;
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
processorState.backoffTime = Math.min(processorState.backoffTime * 2, MAX_BACKOFF_TIME);
|
|
375
|
+
logger.error(`transfer processor failed:`, e);
|
|
376
|
+
logger.debug(`Next retry for casino ${casinoId} in ${processorState.backoffTime}ms`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
logger.debug(`processor stopped for casino ${casinoId}`);
|
|
380
|
+
casinoMap.delete(casinoId);
|
|
381
|
+
})();
|
|
382
|
+
}
|
|
383
|
+
export function stopTransferProcessor(casinoId) {
|
|
384
|
+
const processorState = casinoMap.get(casinoId);
|
|
385
|
+
if (!processorState) {
|
|
386
|
+
logger.warn(`processor not running for casino ${casinoId}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
processorState.emitter.emit("stop");
|
|
390
|
+
}
|
|
391
|
+
export function initializeTransferProcessors() {
|
|
392
|
+
(async () => {
|
|
393
|
+
try {
|
|
394
|
+
const casinos = await db.listCasinos(superuserPool);
|
|
395
|
+
for (const casino of casinos) {
|
|
396
|
+
if (!URL.canParse(casino.graphql_url)) {
|
|
397
|
+
logger.warn(`Skipping casino ${casino.id} due to invalid graphql_url: "${casino.graphql_url}"`);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (config.NODE_ENV === "production" &&
|
|
401
|
+
new URL(casino.graphql_url).hostname === "localhost") {
|
|
402
|
+
logger.warn(`${casino.id} has localhost endpoint "${casino.graphql_url}" while NODE_ENV=production.`);
|
|
403
|
+
}
|
|
404
|
+
logger.info(`Starting casino processor for "${casino.name}" at "${casino.graphql_url}"`);
|
|
405
|
+
startTransferProcessor({ casinoId: casino.id });
|
|
406
|
+
}
|
|
407
|
+
await listenForNewCasinos();
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
logger.error(`Error initializing transfer processors:`, e);
|
|
411
|
+
}
|
|
412
|
+
})();
|
|
413
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { assert } from "tsafe";
|
|
2
|
+
import { TransferStatusKind } from "./__generated__/graphql.js";
|
|
3
|
+
import { withPgPoolTransaction, superuserPool } from "./db/index.js";
|
|
4
|
+
import { maybeOneRow, exactlyOneRow } from "./db/util.js";
|
|
5
|
+
import { START_PENDING_EXPERIENCE_TRANSFER } from "./graphql-queries.js";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
|
+
async function processWithdrawalRequest({ requestId, graphqlClient, }) {
|
|
8
|
+
return withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
9
|
+
const dbWithdrawalRequest = await pgClient
|
|
10
|
+
.query({
|
|
11
|
+
text: `
|
|
12
|
+
select *
|
|
13
|
+
from hub.withdrawal_request
|
|
14
|
+
where id = $1
|
|
15
|
+
|
|
16
|
+
FOR UPDATE
|
|
17
|
+
`,
|
|
18
|
+
values: [requestId],
|
|
19
|
+
})
|
|
20
|
+
.then(maybeOneRow);
|
|
21
|
+
if (!dbWithdrawalRequest) {
|
|
22
|
+
throw new Error("Withdrawal request not found");
|
|
23
|
+
}
|
|
24
|
+
if (dbWithdrawalRequest.mp_transfer_id) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const dbUser = await pgClient
|
|
28
|
+
.query({
|
|
29
|
+
text: `select * from hub.user where id = $1`,
|
|
30
|
+
values: [dbWithdrawalRequest.user_id],
|
|
31
|
+
})
|
|
32
|
+
.then(exactlyOneRow);
|
|
33
|
+
const dbExperience = await pgClient
|
|
34
|
+
.query({
|
|
35
|
+
text: `select * from hub.experience where id = $1`,
|
|
36
|
+
values: [dbWithdrawalRequest.experience_id],
|
|
37
|
+
})
|
|
38
|
+
.then(exactlyOneRow);
|
|
39
|
+
const mpResult = await graphqlClient
|
|
40
|
+
.request({
|
|
41
|
+
document: START_PENDING_EXPERIENCE_TRANSFER,
|
|
42
|
+
variables: {
|
|
43
|
+
mpUserId: dbUser.mp_user_id,
|
|
44
|
+
mpExperienceId: dbExperience.mp_experience_id,
|
|
45
|
+
amount: dbWithdrawalRequest.amount,
|
|
46
|
+
currency: dbWithdrawalRequest.currency_key,
|
|
47
|
+
metadata: JSON.stringify({
|
|
48
|
+
id: `withdrawal_request:${dbWithdrawalRequest.id}`,
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
.then((res) => res.transferCurrencyExperienceToUser?.result);
|
|
53
|
+
if (mpResult?.__typename !== "TransferCurrencyExperienceToUserSuccess" &&
|
|
54
|
+
mpResult?.__typename !== "TransferMetadataIdExists") {
|
|
55
|
+
throw new Error(`Failed to start MP transfer: ${mpResult}`);
|
|
56
|
+
}
|
|
57
|
+
const mpTransferId = mpResult.transfer.id;
|
|
58
|
+
const mpStatus = mpResult.transfer.status;
|
|
59
|
+
await pgClient
|
|
60
|
+
.query({
|
|
61
|
+
text: `
|
|
62
|
+
update hub.withdrawal_request
|
|
63
|
+
set mp_transfer_id = $2
|
|
64
|
+
where id = $1
|
|
65
|
+
`,
|
|
66
|
+
values: [dbWithdrawalRequest.id, mpTransferId],
|
|
67
|
+
})
|
|
68
|
+
.then((res) => {
|
|
69
|
+
assert(res.rowCount === 1, `Expected to update a hub.withdrawal_request.mp_transfer_id`);
|
|
70
|
+
});
|
|
71
|
+
if (mpStatus === TransferStatusKind.Pending ||
|
|
72
|
+
mpStatus === TransferStatusKind.Completed) {
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
logger.warn(`Expected status to be PENDING or COMPLETED, but got ${mpStatus}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await pgClient.query({
|
|
79
|
+
text: `
|
|
80
|
+
insert into hub.withdrawal(
|
|
81
|
+
id,
|
|
82
|
+
casino_id,
|
|
83
|
+
mp_transfer_id,
|
|
84
|
+
user_id,
|
|
85
|
+
experience_id,
|
|
86
|
+
amount,
|
|
87
|
+
currency_key,
|
|
88
|
+
status,
|
|
89
|
+
withdrawal_request_id
|
|
90
|
+
)
|
|
91
|
+
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
92
|
+
`,
|
|
93
|
+
values: [
|
|
94
|
+
dbWithdrawalRequest.id,
|
|
95
|
+
dbWithdrawalRequest.casino_id,
|
|
96
|
+
mpTransferId,
|
|
97
|
+
dbWithdrawalRequest.user_id,
|
|
98
|
+
dbWithdrawalRequest.experience_id,
|
|
99
|
+
dbWithdrawalRequest.amount,
|
|
100
|
+
dbWithdrawalRequest.currency_key,
|
|
101
|
+
mpResult.transfer.status,
|
|
102
|
+
dbWithdrawalRequest.id,
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export async function processWithdrawalRequests({ casinoId, graphqlClient, }) {
|
|
108
|
+
const pendingRequests = await superuserPool.query({
|
|
109
|
+
text: `
|
|
110
|
+
SELECT wr.*
|
|
111
|
+
FROM hub.withdrawal_request wr
|
|
112
|
+
WHERE wr.casino_id = $1
|
|
113
|
+
AND wr.mp_transfer_id IS NULL
|
|
114
|
+
LIMIT 10
|
|
115
|
+
`,
|
|
116
|
+
values: [casinoId],
|
|
117
|
+
});
|
|
118
|
+
for (const request of pendingRequests.rows) {
|
|
119
|
+
try {
|
|
120
|
+
await processWithdrawalRequest({
|
|
121
|
+
requestId: request.id,
|
|
122
|
+
graphqlClient,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
logger.error(`Failed to process withdrawal request ${request.id}:`, error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import "graphile-config";
|
|
2
|
+
import "postgraphile";
|
|
3
|
+
type UserSessionContext = {
|
|
4
|
+
user_id: string;
|
|
5
|
+
mp_user_id: string;
|
|
6
|
+
casino_id: string;
|
|
7
|
+
experience_id: string;
|
|
8
|
+
session_id: string;
|
|
9
|
+
};
|
|
10
|
+
type PluginIdentity = {
|
|
11
|
+
kind: "user";
|
|
12
|
+
session: UserSessionContext;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "operator";
|
|
15
|
+
};
|
|
16
|
+
export type PluginContext = Grafast.Context & {
|
|
17
|
+
identity?: PluginIdentity;
|
|
18
|
+
};
|
|
19
|
+
export declare const requiredPlugins: readonly GraphileConfig.Plugin[];
|
|
20
|
+
declare global {
|
|
21
|
+
namespace GraphileBuild {
|
|
22
|
+
interface SchemaOptions {
|
|
23
|
+
pgDeletedColumnName?: string;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export declare const defaultPlugins: readonly GraphileConfig.Plugin[];
|
|
28
|
+
export declare function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, }: {
|
|
29
|
+
plugins: readonly GraphileConfig.Plugin[];
|
|
30
|
+
exportSchemaSDLPath: string;
|
|
31
|
+
extraPgSchemas: string[];
|
|
32
|
+
}): GraphileConfig.Preset;
|
|
33
|
+
export {};
|