@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.
@@ -0,0 +1,13 @@
1
+ import { TransferFieldsFragment } from "../__generated__/graphql.js";
2
+ import { GraphQLClient } from "graphql-request";
3
+ export declare const MP_COMPLETE_TRANSFER: import("@graphql-typed-document-node/core").TypedDocumentNode<import("../__generated__/graphql.js").CompleteTransferMutation, import("../__generated__/graphql.js").Exact<{
4
+ mpTransferId: import("../__generated__/graphql.js").Scalars["UUID"]["input"];
5
+ }>>;
6
+ export declare function processTransfer({ casinoId, controllerId, transfer, graphqlClient, }: {
7
+ casinoId: string;
8
+ controllerId: string;
9
+ transfer: Extract<TransferFieldsFragment, {
10
+ __typename: "ExperienceTransfer";
11
+ }>;
12
+ graphqlClient: GraphQLClient;
13
+ }): Promise<void>;
@@ -0,0 +1,151 @@
1
+ import * as db from "../db/index.js";
2
+ import { TransferStatusKind, } from "../__generated__/graphql.js";
3
+ import { superuserPool } from "../db/index.js";
4
+ import { gql } from "../__generated__/gql.js";
5
+ import { logger } from "../logger.js";
6
+ import { assert } from "tsafe";
7
+ export const MP_COMPLETE_TRANSFER = gql(`
8
+ mutation CompleteTransfer($mpTransferId: UUID!) {
9
+ completeTransfer(input: { id: $mpTransferId }) {
10
+ result {
11
+ ... on CompleteTransferSuccess {
12
+ __typename
13
+ transfer {
14
+ id
15
+ ... on ExperienceTransfer {
16
+ id
17
+ status
18
+ }
19
+ }
20
+ }
21
+ ... on InvalidTransferStatus {
22
+ __typename
23
+ currentStatus
24
+ message
25
+ }
26
+ }
27
+ }
28
+ }
29
+ `);
30
+ const MP_CLAIM_TRANSFER = gql(`
31
+ mutation ClaimTransfer($mpTransferId: UUID!) {
32
+ claimTransfer(input: { id: $mpTransferId }) {
33
+ result {
34
+ ... on ClaimTransferSuccess {
35
+ __typename
36
+ transfer {
37
+ id
38
+ ... on ExperienceTransfer {
39
+ id
40
+ status
41
+ }
42
+ }
43
+ }
44
+ ... on InvalidTransferStatus {
45
+ __typename
46
+ currentStatus
47
+ message
48
+ }
49
+ }
50
+ }
51
+ }
52
+ `);
53
+ export async function processTransfer({ casinoId, controllerId, transfer, graphqlClient, }) {
54
+ assert(transfer, "Expected transfer");
55
+ assert(transfer.__typename === "ExperienceTransfer", `Expected ExperienceTransfer but got ${transfer.__typename}`);
56
+ logger.debug(`processing transfer ${transfer.id} for casino ${casinoId}...`);
57
+ logger.debug("transfer", transfer);
58
+ assert(transfer.experienceByExperienceId, "Expected experienceByExperienceId");
59
+ const isIncoming = controllerId === transfer.toHolderId;
60
+ const user = isIncoming
61
+ ? transfer.holderByFromHolderId
62
+ : transfer.holderByToHolderId;
63
+ assert(user?.__typename === "User", "Expected user transfer participant");
64
+ const currency = transfer.currencyByCurrency;
65
+ const dbSender = await db.upsertUser(superuserPool, {
66
+ casinoId,
67
+ mpUserId: user.id,
68
+ uname: user.uname,
69
+ });
70
+ const dbExperience = await db.upsertExperience(superuserPool, {
71
+ casinoId,
72
+ mpExperienceId: transfer.experienceByExperienceId.id,
73
+ name: transfer.experienceByExperienceId.name,
74
+ });
75
+ await db.upsertCurrencies(superuserPool, {
76
+ casinoId,
77
+ currencies: [currency],
78
+ });
79
+ if (isIncoming) {
80
+ logger.debug(`${user.uname} sent me ${transfer.amount} base units of ${currency.id}`);
81
+ switch (transfer.status) {
82
+ case TransferStatusKind.Pending:
83
+ throw new Error(`Unexpected PENDING deposit transfer: ${JSON.stringify(transfer)}`);
84
+ case TransferStatusKind.Canceled:
85
+ case TransferStatusKind.Expired:
86
+ return;
87
+ case TransferStatusKind.Completed: {
88
+ await db.insertDeposit({
89
+ casinoId,
90
+ mpTransferId: transfer.id,
91
+ userId: dbSender.id,
92
+ experienceId: dbExperience.id,
93
+ amount: transfer.amount,
94
+ currency: currency.id,
95
+ });
96
+ return;
97
+ }
98
+ case TransferStatusKind.Unclaimed: {
99
+ let data;
100
+ try {
101
+ data = await graphqlClient.request(MP_CLAIM_TRANSFER, {
102
+ mpTransferId: transfer.id,
103
+ });
104
+ }
105
+ catch (e) {
106
+ logger.error(`Error sending claimTransfer(${transfer.id}) to ${casinoId}:`, e);
107
+ throw e;
108
+ }
109
+ logger.debug("MP_CLAIM_TRANSFER response:", data);
110
+ if (data.claimTransfer?.result.__typename !== "ClaimTransferSuccess") {
111
+ throw new Error(`Failed to claim transfer: ${JSON.stringify(data.claimTransfer)}`);
112
+ }
113
+ await db.insertDeposit({
114
+ casinoId,
115
+ mpTransferId: transfer.id,
116
+ userId: dbSender.id,
117
+ experienceId: dbExperience.id,
118
+ amount: transfer.amount,
119
+ currency: currency.id,
120
+ });
121
+ return;
122
+ }
123
+ default: {
124
+ const exhaustiveCheck = transfer.status;
125
+ throw new Error(`Unexpected transfer status: ${exhaustiveCheck}`);
126
+ }
127
+ }
128
+ }
129
+ else {
130
+ logger.debug(`I sent ${user.uname} ${transfer.amount} base units of ${currency.id}`);
131
+ switch (transfer.status) {
132
+ case TransferStatusKind.Canceled:
133
+ await db.settleWithdrawal({
134
+ withdrawalId: transfer.id,
135
+ newStatus: "CANCELED",
136
+ });
137
+ return;
138
+ case TransferStatusKind.Unclaimed:
139
+ throw new Error(`Unexpected UNCLAIMED withdrawal.`);
140
+ case TransferStatusKind.Expired:
141
+ throw new Error(`Unexpected EXPIRED withdrawal.`);
142
+ case TransferStatusKind.Pending:
143
+ case TransferStatusKind.Completed:
144
+ return;
145
+ default: {
146
+ const exhaustiveCheck = transfer.status;
147
+ throw new Error(`Unexpected transfer status: ${exhaustiveCheck}`);
148
+ }
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,7 @@
1
+ export declare function startWebsocketProcessor({ casinoId, graphqlUrl, signal, controllerId, apiKey, }: {
2
+ casinoId: string;
3
+ graphqlUrl: string;
4
+ signal: AbortSignal;
5
+ controllerId: string;
6
+ apiKey: string;
7
+ }): void;
@@ -0,0 +1,133 @@
1
+ import { createClient as createWebsocketClient, } from "graphql-ws";
2
+ import { gql } from "../__generated__/gql.js";
3
+ import { print } from "graphql";
4
+ import { createGraphqlClient } from "../graphql-client.js";
5
+ import { useFragment } from "../__generated__/fragment-masking.js";
6
+ import { TRANSFER_FIELDS } from "./graphql.js";
7
+ import { logger } from "../logger.js";
8
+ import { processTransfer } from "./process-transfer.js";
9
+ import assert from "assert";
10
+ import { mpGetTakeRequest, processSingleTakeRequest, } from "../take-request/process-take-request.js";
11
+ function httpToWs(url) {
12
+ if (url.protocol === "http:") {
13
+ url.protocol = "ws:";
14
+ }
15
+ else if (url.protocol === "https:") {
16
+ url.protocol = "wss:";
17
+ }
18
+ return url;
19
+ }
20
+ const MP_NEW_EXPERIENCE_TRANSFER = gql(`
21
+ subscription MpNewExperienceTransfer {
22
+ newExperienceTransfer {
23
+ id
24
+ }
25
+ }
26
+ `);
27
+ const MP_GET_EXPERIENCE_TRANSFER = gql(`
28
+ query MpGetExperienceTransfer($id: UUID!) {
29
+ experienceTransferById(id: $id) {
30
+ ...TransferFields
31
+ }
32
+ }
33
+ `);
34
+ const MP_NEW_TAKE_REQUEST = gql(`
35
+ subscription MpNewTakeRequest {
36
+ newTakeRequest {
37
+ id
38
+ }
39
+ }
40
+ `);
41
+ async function mpGetExperienceTransfer(graphqlClient, id) {
42
+ const result = await graphqlClient.request(MP_GET_EXPERIENCE_TRANSFER, {
43
+ id,
44
+ });
45
+ const x = result.experienceTransferById;
46
+ if (!x) {
47
+ return null;
48
+ }
49
+ const mpTransfer = useFragment(TRANSFER_FIELDS, x);
50
+ return mpTransfer;
51
+ }
52
+ export function startWebsocketProcessor({ casinoId, graphqlUrl, signal, controllerId, apiKey, }) {
53
+ console.log(`Starting websocket processor for ${graphqlUrl} using apiKey ${apiKey}`);
54
+ const client = createWebsocketClient({
55
+ url: httpToWs(new URL(graphqlUrl)).toString(),
56
+ connectionParams: {
57
+ authorization: `apikey:${apiKey}`,
58
+ },
59
+ retryAttempts: Infinity,
60
+ shouldRetry: () => {
61
+ return true;
62
+ },
63
+ keepAlive: 1_000,
64
+ });
65
+ client.on("connected", () => {
66
+ logger.info(`[websocketProcessor] Connected to websocket ${graphqlUrl}`);
67
+ });
68
+ client.on("closed", () => {
69
+ logger.info(`[websocketProcessor] Disconnected from websocket ${graphqlUrl}`);
70
+ });
71
+ client.on("error", (error) => {
72
+ logger.error(`[websocketProcessor] Error on websocket ${graphqlUrl}`, error);
73
+ });
74
+ const graphqlClient = createGraphqlClient({ graphqlUrl, apiKey });
75
+ const dispose1 = createSubscription(client, MP_NEW_EXPERIENCE_TRANSFER, async (result) => {
76
+ console.log(`😎 Get transfer from websocket`, result);
77
+ const transferId = result.data?.newExperienceTransfer?.id;
78
+ if (!transferId) {
79
+ logger.warn(`Weird, no transfer id in websocket result`, result);
80
+ return;
81
+ }
82
+ const mpTransfer = await mpGetExperienceTransfer(graphqlClient, transferId);
83
+ if (!mpTransfer) {
84
+ logger.warn(`No experience transfer found in casino api for mp transfer id ${transferId}`);
85
+ return;
86
+ }
87
+ assert(mpTransfer.__typename === "ExperienceTransfer", `Expected experience transfer but got ${mpTransfer.__typename}`);
88
+ console.log(`🎉 Loaded transfer from MP API`, mpTransfer);
89
+ processTransfer({
90
+ casinoId,
91
+ controllerId,
92
+ transfer: mpTransfer,
93
+ graphqlClient,
94
+ });
95
+ });
96
+ const dispose2 = createSubscription(client, MP_NEW_TAKE_REQUEST, async (result) => {
97
+ console.log(`😎 Get take request from websocket`, result);
98
+ const mpTakeRequestId = result.data?.newTakeRequest?.id;
99
+ if (!mpTakeRequestId) {
100
+ logger.warn(`Weird, no take request id in websocket result`, result);
101
+ return;
102
+ }
103
+ const mpTakeRequest = await mpGetTakeRequest(graphqlClient, mpTakeRequestId);
104
+ if (!mpTakeRequest) {
105
+ logger.warn(`No take request found in casino api for mp take request id ${mpTakeRequestId}`);
106
+ return;
107
+ }
108
+ console.log(`🎉 Loaded take request from MP API`, mpTakeRequest);
109
+ await processSingleTakeRequest({
110
+ mpTakeRequestId,
111
+ mpTakeRequest,
112
+ casinoId,
113
+ graphqlClient,
114
+ });
115
+ });
116
+ signal.addEventListener("abort", () => {
117
+ dispose1();
118
+ dispose2();
119
+ });
120
+ }
121
+ function createSubscription(client, query, handler) {
122
+ return client.subscribe({ query: print(query) }, {
123
+ next: (result) => {
124
+ handler(result).catch((e) => {
125
+ console.error(`Error while handling websocket result`, e);
126
+ });
127
+ },
128
+ error: (err) => {
129
+ console.error(`Error during WebSocket subscription`, err);
130
+ },
131
+ complete: () => console.log("Subscription closed"),
132
+ });
133
+ }
@@ -2,7 +2,6 @@ import { z } from "zod";
2
2
  import * as jose from "jose";
3
3
  import { gql } from "../__generated__/gql.js";
4
4
  import { maybeOneRow } from "../db/util.js";
5
- import { logger } from "../logger.js";
6
5
  const CACHE_TTL = "5 minutes";
7
6
  const GET_CASINO_JWKS = gql(`
8
7
  query GetCasinoJWKS {
@@ -22,7 +21,6 @@ const JSONWebKeySetSchema = z.object({
22
21
  .min(1),
23
22
  });
24
23
  async function dbInsertCasinoJWKS(pgClient, { casinoId, jwks: unvalidatedJwks, }) {
25
- logger.debug(`[dbInsertCasinoJWKS] Inserting JWKS for casino ${casinoId}`, unvalidatedJwks);
26
24
  const result = JSONWebKeySetSchema.safeParse(unvalidatedJwks);
27
25
  if (!result.success) {
28
26
  throw new Error(`Will not insert invalid JWKS into database: ${result.error}`);
@@ -60,9 +58,7 @@ async function dbGetCasinoJWKS(pgClient, { casinoId }) {
60
58
  return jwks ?? null;
61
59
  }
62
60
  async function mpFetchCasinoJWKS(graphqlClient) {
63
- console.log("sending request to casino");
64
61
  const response = await graphqlClient.request(GET_CASINO_JWKS);
65
- console.log("response", response);
66
62
  const jwks = response.jwks;
67
63
  if (jwks.keys.length === 0) {
68
64
  throw new Error("No JWKS keys found in response");
@@ -1,7 +1,16 @@
1
1
  import { GraphQLClient } from "graphql-request";
2
+ import { MpTakeRequestFieldsFragment } from "../__generated__/graphql.js";
3
+ export declare const MP_TAKE_REQUEST_FIELDS: import("@graphql-typed-document-node/core").TypedDocumentNode<MpTakeRequestFieldsFragment, unknown>;
4
+ export declare function mpGetTakeRequest(graphqlClient: GraphQLClient, id: string): Promise<MpTakeRequestFieldsFragment | null>;
2
5
  export declare function processTakeRequests({ abortSignal, controllerId, casinoId, graphqlClient, }: {
3
6
  abortSignal: AbortSignal;
4
7
  controllerId: string;
5
8
  casinoId: string;
6
9
  graphqlClient: GraphQLClient;
7
10
  }): Promise<void>;
11
+ export declare function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, }: {
12
+ mpTakeRequestId: string;
13
+ mpTakeRequest?: MpTakeRequestFieldsFragment;
14
+ casinoId: string;
15
+ graphqlClient: GraphQLClient;
16
+ }): Promise<string | null>;
@@ -3,6 +3,35 @@ import { TakeRequestStatus as MpTakeRequestStatus, TransferStatusKind as MpTrans
3
3
  import { exactlyOneRow, maybeOneRow, superuserPool, withPgPoolTransaction, } from "../db/index.js";
4
4
  import { assert } from "tsafe";
5
5
  import { PgAdvisoryLock } from "../pg-advisory-lock.js";
6
+ import { useFragment } from "../__generated__/fragment-masking.js";
7
+ import { logger } from "../logger.js";
8
+ export const MP_TAKE_REQUEST_FIELDS = gql(`
9
+ fragment MpTakeRequestFields on TakeRequest {
10
+ id
11
+ status
12
+ amount
13
+ currencyKey
14
+ userId
15
+ experienceId
16
+ experienceTransfer {
17
+ id
18
+ amount
19
+ }
20
+ }
21
+ `);
22
+ const MP_GET_TAKE_REQUEST = gql(`
23
+ query MpGetTakeRequest($id: UUID!) {
24
+ takeRequestById(id: $id) {
25
+ ...MpTakeRequestFields
26
+ }
27
+ }
28
+ `);
29
+ export async function mpGetTakeRequest(graphqlClient, id) {
30
+ const result = await graphqlClient.request(MP_GET_TAKE_REQUEST, { id });
31
+ return result.takeRequestById
32
+ ? useFragment(MP_TAKE_REQUEST_FIELDS, result.takeRequestById)
33
+ : null;
34
+ }
6
35
  const MP_PAGINATE_PENDING_TAKE_REQUESTS = gql(`
7
36
  query MpPaginatedPendingTakeRequests($controllerId: UUID!, $after: Cursor) {
8
37
  allTakeRequests(
@@ -17,16 +46,7 @@ const MP_PAGINATE_PENDING_TAKE_REQUESTS = gql(`
17
46
  edges {
18
47
  cursor
19
48
  node {
20
- id
21
- status
22
- amount
23
- currencyKey
24
- userId
25
- experienceId
26
- experienceTransfer {
27
- id
28
- amount
29
- }
49
+ ...MpTakeRequestFields
30
50
  }
31
51
  }
32
52
  }
@@ -151,7 +171,6 @@ export async function processTakeRequests({ abortSignal, controllerId, casinoId,
151
171
  return;
152
172
  }
153
173
  const takeRequests = await fetchPendingTakeRequests(graphqlClient, controllerId);
154
- console.log(`[processTakeRequests] Found ${takeRequests.length} take requests`);
155
174
  for (const takeRequest of takeRequests) {
156
175
  if (abortSignal.aborted) {
157
176
  break;
@@ -174,9 +193,14 @@ async function fetchPendingTakeRequests(graphqlClient, controllerId) {
174
193
  const result = await graphqlClient.request(MP_PAGINATE_PENDING_TAKE_REQUESTS, {
175
194
  controllerId,
176
195
  });
177
- return (result.allTakeRequests?.edges.flatMap((edge) => edge?.node || []) || []);
196
+ return (result.allTakeRequests?.edges.flatMap((edge) => {
197
+ const takeRequest = edge?.node;
198
+ if (!takeRequest)
199
+ return [];
200
+ return useFragment(MP_TAKE_REQUEST_FIELDS, takeRequest);
201
+ }) || []);
178
202
  }
179
- async function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, }) {
203
+ export async function processSingleTakeRequest({ mpTakeRequestId, mpTakeRequest, casinoId, graphqlClient, }) {
180
204
  return withPgPoolTransaction(superuserPool, async (pgClient) => {
181
205
  await PgAdvisoryLock.forMpTakeRequestProcessing(pgClient, {
182
206
  mpTakeRequestId,
@@ -441,7 +465,9 @@ async function processPendingTransferCompletions({ casinoId, graphqlClient, abor
441
465
  values: [casinoId],
442
466
  })
443
467
  .then((result) => result.rows);
444
- console.log(`[processPendingTransferCompletions] Found ${pendingCompletions.length} transfers needing completion`);
468
+ if (pendingCompletions.length > 0) {
469
+ logger.info(`[processPendingTransferCompletions] Found ${pendingCompletions.length} transfers needing completion`);
470
+ }
445
471
  for (const request of pendingCompletions) {
446
472
  if (abortSignal.aborted) {
447
473
  break;
@@ -581,7 +607,9 @@ async function processStuckRequests({ casinoId, graphqlClient, abortSignal, }) {
581
607
  values: [casinoId, LocalTakeRequestStatus.PROCESSING],
582
608
  })
583
609
  .then((result) => result.rows);
584
- console.log(`[processStuckRequests] Found ${stuckRequests.length} stuck take requests`);
610
+ if (stuckRequests.length > 0) {
611
+ logger.info(`[processStuckRequests] Found ${stuckRequests.length} stuck take requests`);
612
+ }
585
613
  for (const request of stuckRequests) {
586
614
  if (abortSignal.aborted) {
587
615
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.5.1",
3
+ "version": "1.6.0-dev.0",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [
@@ -49,6 +49,7 @@
49
49
  "express": "^5.0.1",
50
50
  "graphql": "^16.8.1",
51
51
  "graphql-request": "^7.0.0",
52
+ "graphql-ws": "^6.0.5",
52
53
  "jose": "^6.0.11",
53
54
  "pg": "^8.12.0",
54
55
  "pg-connection-string": "^2.6.4",
@@ -63,7 +64,7 @@
63
64
  "@graphql-codegen/client-preset": "^4.2.5",
64
65
  "@graphql-typed-document-node/core": "^3.2.0",
65
66
  "@types/express": "^5.0.0",
66
- "@types/node": "^22.1.0",
67
+ "@types/node": "^24.0.3",
67
68
  "@types/pg": "^8.11.5",
68
69
  "eslint": "^9.8.0",
69
70
  "globals": "^16.0.0",
@@ -1,10 +0,0 @@
1
- import * as db from "./db/index.js";
2
- export declare function casinoIdsInProcess(): string[];
3
- export declare function startTransferProcessor({ casinoId, signal, }: {
4
- casinoId: db.DbCasino["id"];
5
- signal: AbortSignal;
6
- }): void;
7
- export declare function stopTransferProcessor(casinoId: string): void;
8
- export declare function initializeTransferProcessors({ signal, }: {
9
- signal: AbortSignal;
10
- }): void;