@tanakayuto/intmax402-express 0.2.0 → 0.2.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/dist/verify-payment.js +32 -7
- package/package.json +8 -8
package/dist/verify-payment.js
CHANGED
|
@@ -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
|
|
56
|
-
|
|
57
|
-
|
|
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 (
|
|
68
|
-
if (match.amount
|
|
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 (
|
|
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,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanakayuto/intmax402-express",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"clean": "rm -rf dist",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
6
11
|
"dependencies": {
|
|
7
|
-
"@tanakayuto/intmax402-core": "0.2.
|
|
12
|
+
"@tanakayuto/intmax402-core": "0.2.1",
|
|
8
13
|
"ethers": "^6.16.0",
|
|
9
14
|
"intmax2-server-sdk": "^1.5.2"
|
|
10
15
|
},
|
|
@@ -37,10 +42,5 @@
|
|
|
37
42
|
"express",
|
|
38
43
|
"web3",
|
|
39
44
|
"zk"
|
|
40
|
-
]
|
|
41
|
-
"scripts": {
|
|
42
|
-
"build": "tsc",
|
|
43
|
-
"clean": "rm -rf dist",
|
|
44
|
-
"typecheck": "tsc --noEmit"
|
|
45
|
-
}
|
|
45
|
+
]
|
|
46
46
|
}
|