@tanakayuto/intmax402-express 0.2.0 → 0.2.2

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.
@@ -9,16 +9,24 @@ const intmax2_server_sdk_1 = require("intmax2-server-sdk");
9
9
  let client = null;
10
10
  let loginPromise = null;
11
11
  // Used txHash tracking with TTL for replay prevention
12
+ // Value is expiry timestamp (ms), or PENDING (-1) while verification is in progress
13
+ const PENDING = -1;
12
14
  const usedTxHashes = new Map();
13
15
  const TX_HASH_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
14
16
  function cleanupExpiredHashes() {
15
17
  const now = Date.now();
16
18
  for (const [hash, expiry] of usedTxHashes) {
19
+ // Skip PENDING entries (active verifications)
20
+ if (expiry === PENDING)
21
+ continue;
17
22
  if (now > expiry) {
18
23
  usedTxHashes.delete(hash);
19
24
  }
20
25
  }
21
26
  }
27
+ // Start periodic cleanup (Fix 5: Map periodic cleanup)
28
+ const cleanupInterval = setInterval(cleanupExpiredHashes, 60 * 60 * 1000); // 1 hour
29
+ cleanupInterval.unref(); // Don't prevent process exit
22
30
  async function initPaymentVerifier(config) {
23
31
  if (client && client.isLoggedIn)
24
32
  return;
@@ -46,37 +54,54 @@ async function verifyPayment(txHash, expectedAmount, serverAddress, tokenIndex)
46
54
  if (!client || !client.isLoggedIn) {
47
55
  return { valid: false, error: "Payment verifier not initialized" };
48
56
  }
49
- // Replay prevention: check if txHash was already used
57
+ // Replay prevention: check if txHash was already used (or pending)
50
58
  cleanupExpiredHashes();
51
59
  if (usedTxHashes.has(txHash)) {
52
60
  return { valid: false, error: "Transaction already used" };
53
61
  }
62
+ // Fix 1: Mark as PENDING immediately to prevent race conditions
63
+ usedTxHashes.set(txHash, PENDING);
54
64
  try {
55
- // Fetch recent transfers (incoming)
56
- const response = await client.fetchTransfers({ cursor: null, limit: 50 });
57
- const transfers = response.items;
65
+ // Fix 3: Fetch up to 200 transfers using pagination
66
+ let response = await client.fetchTransfers({ cursor: null, limit: 100 });
67
+ let transfers = [...response.items];
68
+ // Check next page if not found yet and more items exist
69
+ if (!transfers.find((tx) => tx.digest === txHash) && response.pagination?.has_more && response.pagination?.next_cursor != null) {
70
+ const response2 = await client.fetchTransfers({ cursor: response.pagination.next_cursor, limit: 100 });
71
+ transfers = transfers.concat(response2.items);
72
+ }
58
73
  // Find matching transaction by digest
59
74
  const match = transfers.find((tx) => tx.digest === txHash);
60
75
  if (!match) {
76
+ // Fix 1: Rollback on validation failure
77
+ usedTxHashes.delete(txHash);
61
78
  return { valid: false, error: "Transaction not found in recent transfers" };
62
79
  }
63
80
  // Verify recipient matches server address
64
81
  if (match.to?.toLowerCase() !== serverAddress.toLowerCase()) {
82
+ // Fix 1: Rollback on validation failure
83
+ usedTxHashes.delete(txHash);
65
84
  return { valid: false, error: "Recipient does not match server address" };
66
85
  }
67
- // Verify amount (compare as BigInt-safe string)
68
- if (match.amount !== expectedAmount) {
86
+ // Fix 2: Verify amount using BigInt comparison (allows >= expectedAmount)
87
+ if (BigInt(match.amount) < BigInt(expectedAmount)) {
88
+ // Fix 1: Rollback on validation failure
89
+ usedTxHashes.delete(txHash);
69
90
  return { valid: false, error: `Amount mismatch: expected ${expectedAmount}, got ${match.amount}` };
70
91
  }
71
92
  // Verify token if specified
72
93
  if (tokenIndex !== undefined && match.tokenIndex !== tokenIndex) {
94
+ // Fix 1: Rollback on validation failure
95
+ usedTxHashes.delete(txHash);
73
96
  return { valid: false, error: `Token mismatch: expected index ${tokenIndex}, got ${match.tokenIndex}` };
74
97
  }
75
- // Mark as used (replay prevention)
98
+ // Fix 1: Mark as used with proper expiry (replace PENDING)
76
99
  usedTxHashes.set(txHash, Date.now() + TX_HASH_TTL_MS);
77
100
  return { valid: true };
78
101
  }
79
102
  catch (err) {
103
+ // Fix 1: Rollback on error so the tx can be retried
104
+ usedTxHashes.delete(txHash);
80
105
  const message = err instanceof Error ? err.message : String(err);
81
106
  return { valid: false, error: `Payment verification failed: ${message}` };
82
107
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@tanakayuto/intmax402-express",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
7
- "@tanakayuto/intmax402-core": "0.2.0",
7
+ "@tanakayuto/intmax402-core": "0.2.2",
8
8
  "ethers": "^6.16.0",
9
9
  "intmax2-server-sdk": "^1.5.2"
10
10
  },