@wopr-network/platform-core 1.67.0 → 1.68.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/dist/auth/better-auth.js +7 -0
- package/dist/billing/crypto/btc/checkout.d.ts +4 -0
- package/dist/billing/crypto/btc/checkout.js +1 -2
- package/dist/billing/crypto/btc/index.d.ts +0 -4
- package/dist/billing/crypto/btc/index.js +0 -2
- package/dist/billing/crypto/evm/__tests__/checkout.test.js +8 -11
- package/dist/billing/crypto/evm/__tests__/eth-checkout.test.js +15 -1
- package/dist/billing/crypto/evm/checkout.d.ts +2 -0
- package/dist/billing/crypto/evm/checkout.js +1 -2
- package/dist/billing/crypto/evm/eth-checkout.d.ts +13 -2
- package/dist/billing/crypto/evm/eth-checkout.js +2 -4
- package/dist/billing/crypto/evm/eth-settler.d.ts +1 -1
- package/dist/billing/crypto/evm/index.d.ts +2 -8
- package/dist/billing/crypto/evm/index.js +0 -3
- package/dist/billing/crypto/evm/types.d.ts +16 -0
- package/dist/billing/crypto/index.d.ts +1 -6
- package/dist/billing/crypto/index.js +2 -3
- package/dist/billing/crypto/types.d.ts +0 -43
- package/dist/billing/crypto/types.js +1 -24
- package/dist/email/client.js +16 -0
- package/package.json +4 -7
- package/src/auth/better-auth.ts +8 -0
- package/src/billing/crypto/btc/checkout.ts +3 -2
- package/src/billing/crypto/btc/index.ts +0 -4
- package/src/billing/crypto/evm/__tests__/checkout.test.ts +10 -12
- package/src/billing/crypto/evm/__tests__/eth-checkout.test.ts +17 -1
- package/src/billing/crypto/evm/__tests__/eth-settler.test.ts +1 -1
- package/src/billing/crypto/evm/checkout.ts +3 -2
- package/src/billing/crypto/evm/eth-checkout.ts +15 -6
- package/src/billing/crypto/evm/eth-settler.ts +1 -1
- package/src/billing/crypto/evm/index.ts +8 -7
- package/src/billing/crypto/evm/types.ts +17 -0
- package/src/billing/crypto/index.ts +14 -12
- package/src/billing/crypto/types.ts +0 -63
- package/src/email/client.ts +18 -0
- package/dist/billing/crypto/__tests__/address-gen.test.d.ts +0 -1
- package/dist/billing/crypto/__tests__/address-gen.test.js +0 -219
- package/dist/billing/crypto/__tests__/key-server.test.d.ts +0 -1
- package/dist/billing/crypto/__tests__/key-server.test.js +0 -742
- package/dist/billing/crypto/__tests__/watcher-service.test.d.ts +0 -1
- package/dist/billing/crypto/__tests__/watcher-service.test.js +0 -174
- package/dist/billing/crypto/address-gen.d.ts +0 -24
- package/dist/billing/crypto/address-gen.js +0 -176
- package/dist/billing/crypto/btc/__tests__/watcher.test.d.ts +0 -1
- package/dist/billing/crypto/btc/__tests__/watcher.test.js +0 -170
- package/dist/billing/crypto/btc/watcher.d.ts +0 -44
- package/dist/billing/crypto/btc/watcher.js +0 -118
- package/dist/billing/crypto/evm/__tests__/eth-watcher.test.d.ts +0 -1
- package/dist/billing/crypto/evm/__tests__/eth-watcher.test.js +0 -167
- package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.d.ts +0 -1
- package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.js +0 -159
- package/dist/billing/crypto/evm/__tests__/watcher.test.d.ts +0 -1
- package/dist/billing/crypto/evm/__tests__/watcher.test.js +0 -145
- package/dist/billing/crypto/evm/eth-watcher.d.ts +0 -66
- package/dist/billing/crypto/evm/eth-watcher.js +0 -121
- package/dist/billing/crypto/evm/watcher.d.ts +0 -51
- package/dist/billing/crypto/evm/watcher.js +0 -156
- package/dist/billing/crypto/key-server-entry.d.ts +0 -1
- package/dist/billing/crypto/key-server-entry.js +0 -122
- package/dist/billing/crypto/key-server.d.ts +0 -32
- package/dist/billing/crypto/key-server.js +0 -472
- package/dist/billing/crypto/oracle/__tests__/chainlink.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/chainlink.test.js +0 -83
- package/dist/billing/crypto/oracle/__tests__/coingecko.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/coingecko.test.js +0 -65
- package/dist/billing/crypto/oracle/__tests__/composite.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/composite.test.js +0 -48
- package/dist/billing/crypto/oracle/__tests__/convert.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/convert.test.js +0 -61
- package/dist/billing/crypto/oracle/__tests__/fixed.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/fixed.test.js +0 -20
- package/dist/billing/crypto/oracle/chainlink.d.ts +0 -26
- package/dist/billing/crypto/oracle/chainlink.js +0 -62
- package/dist/billing/crypto/oracle/coingecko.d.ts +0 -22
- package/dist/billing/crypto/oracle/coingecko.js +0 -71
- package/dist/billing/crypto/oracle/composite.d.ts +0 -14
- package/dist/billing/crypto/oracle/composite.js +0 -34
- package/dist/billing/crypto/oracle/convert.d.ts +0 -30
- package/dist/billing/crypto/oracle/convert.js +0 -51
- package/dist/billing/crypto/oracle/fixed.d.ts +0 -10
- package/dist/billing/crypto/oracle/fixed.js +0 -22
- package/dist/billing/crypto/oracle/index.d.ts +0 -9
- package/dist/billing/crypto/oracle/index.js +0 -6
- package/dist/billing/crypto/oracle/types.d.ts +0 -22
- package/dist/billing/crypto/oracle/types.js +0 -7
- package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +0 -1
- package/dist/billing/crypto/plugin/__tests__/integration.test.js +0 -58
- package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +0 -1
- package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +0 -46
- package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +0 -1
- package/dist/billing/crypto/plugin/__tests__/registry.test.js +0 -49
- package/dist/billing/crypto/plugin/index.d.ts +0 -2
- package/dist/billing/crypto/plugin/index.js +0 -1
- package/dist/billing/crypto/plugin/interfaces.d.ts +0 -97
- package/dist/billing/crypto/plugin/interfaces.js +0 -2
- package/dist/billing/crypto/plugin/registry.d.ts +0 -8
- package/dist/billing/crypto/plugin/registry.js +0 -21
- package/dist/billing/crypto/plugin-watcher-service.d.ts +0 -32
- package/dist/billing/crypto/plugin-watcher-service.js +0 -113
- package/dist/billing/crypto/tron/__tests__/address-convert.test.d.ts +0 -1
- package/dist/billing/crypto/tron/__tests__/address-convert.test.js +0 -55
- package/dist/billing/crypto/tron/address-convert.d.ts +0 -14
- package/dist/billing/crypto/tron/address-convert.js +0 -93
- package/dist/billing/crypto/watcher-service.d.ts +0 -55
- package/dist/billing/crypto/watcher-service.js +0 -438
- package/src/billing/crypto/__tests__/address-gen.test.ts +0 -264
- package/src/billing/crypto/__tests__/key-server.test.ts +0 -823
- package/src/billing/crypto/__tests__/watcher-service.test.ts +0 -242
- package/src/billing/crypto/address-gen.ts +0 -185
- package/src/billing/crypto/btc/__tests__/watcher.test.ts +0 -201
- package/src/billing/crypto/btc/watcher.ts +0 -161
- package/src/billing/crypto/evm/__tests__/eth-watcher.test.ts +0 -190
- package/src/billing/crypto/evm/__tests__/watcher-confirmations.test.ts +0 -191
- package/src/billing/crypto/evm/__tests__/watcher.test.ts +0 -167
- package/src/billing/crypto/evm/eth-watcher.ts +0 -182
- package/src/billing/crypto/evm/watcher.ts +0 -204
- package/src/billing/crypto/key-server-entry.ts +0 -144
- package/src/billing/crypto/key-server.ts +0 -617
- package/src/billing/crypto/oracle/__tests__/chainlink.test.ts +0 -107
- package/src/billing/crypto/oracle/__tests__/coingecko.test.ts +0 -75
- package/src/billing/crypto/oracle/__tests__/composite.test.ts +0 -61
- package/src/billing/crypto/oracle/__tests__/convert.test.ts +0 -74
- package/src/billing/crypto/oracle/__tests__/fixed.test.ts +0 -23
- package/src/billing/crypto/oracle/chainlink.ts +0 -86
- package/src/billing/crypto/oracle/coingecko.ts +0 -96
- package/src/billing/crypto/oracle/composite.ts +0 -35
- package/src/billing/crypto/oracle/convert.ts +0 -53
- package/src/billing/crypto/oracle/fixed.ts +0 -25
- package/src/billing/crypto/oracle/index.ts +0 -9
- package/src/billing/crypto/oracle/types.ts +0 -28
- package/src/billing/crypto/plugin/__tests__/integration.test.ts +0 -64
- package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +0 -51
- package/src/billing/crypto/plugin/__tests__/registry.test.ts +0 -58
- package/src/billing/crypto/plugin/index.ts +0 -17
- package/src/billing/crypto/plugin/interfaces.ts +0 -106
- package/src/billing/crypto/plugin/registry.ts +0 -26
- package/src/billing/crypto/plugin-watcher-service.ts +0 -148
- package/src/billing/crypto/tron/__tests__/address-convert.test.ts +0 -67
- package/src/billing/crypto/tron/address-convert.ts +0 -89
- package/src/billing/crypto/watcher-service.ts +0 -549
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watcher Service — boots chain watchers and sends webhook callbacks.
|
|
3
|
-
*
|
|
4
|
-
* Payment flow:
|
|
5
|
-
* 1. Watcher detects payment → handlePayment()
|
|
6
|
-
* 2. Accumulate native amount (supports partial payments)
|
|
7
|
-
* 3. When totalReceived >= expectedAmount AND confirmations >= required → confirmed + credit
|
|
8
|
-
* 4. Every payment/confirmation change enqueues a webhook delivery
|
|
9
|
-
* 5. Outbox processor retries failed deliveries with exponential backoff
|
|
10
|
-
*
|
|
11
|
-
* Amount comparison is ALWAYS in native crypto units (sats, wei, token base units).
|
|
12
|
-
* The exchange rate is locked at charge creation — no live price comparison.
|
|
13
|
-
*/
|
|
14
|
-
import { and, eq, isNull, lte, or } from "drizzle-orm";
|
|
15
|
-
import { cryptoCharges, webhookDeliveries } from "../../db/schema/crypto.js";
|
|
16
|
-
import { BtcWatcher, createBitcoindRpc } from "./btc/watcher.js";
|
|
17
|
-
import { EthWatcher } from "./evm/eth-watcher.js";
|
|
18
|
-
import { createRpcCaller, EvmWatcher } from "./evm/watcher.js";
|
|
19
|
-
import { hexToTron, isTronAddress, tronToHex } from "./tron/address-convert.js";
|
|
20
|
-
const MAX_DELIVERY_ATTEMPTS = 10;
|
|
21
|
-
const BACKOFF_BASE_MS = 5_000;
|
|
22
|
-
// --- SSRF validation ---
|
|
23
|
-
function isValidCallbackUrl(url, allowedPrefixes) {
|
|
24
|
-
try {
|
|
25
|
-
const parsed = new URL(url);
|
|
26
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
27
|
-
return false;
|
|
28
|
-
const host = parsed.hostname;
|
|
29
|
-
if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1")
|
|
30
|
-
return false;
|
|
31
|
-
if (host.startsWith("10.") || host.startsWith("192.168.") || host.startsWith("169.254."))
|
|
32
|
-
return false;
|
|
33
|
-
return allowedPrefixes.some((prefix) => url.startsWith(prefix));
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
// --- Webhook outbox ---
|
|
40
|
-
async function enqueueWebhook(db, chargeId, callbackUrl, payload) {
|
|
41
|
-
await db.insert(webhookDeliveries).values({
|
|
42
|
-
chargeId,
|
|
43
|
-
callbackUrl,
|
|
44
|
-
payload: JSON.stringify(payload),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
async function processDeliveries(db, allowedPrefixes, log, serviceKey) {
|
|
48
|
-
const now = new Date().toISOString();
|
|
49
|
-
const pending = await db
|
|
50
|
-
.select()
|
|
51
|
-
.from(webhookDeliveries)
|
|
52
|
-
.where(and(eq(webhookDeliveries.status, "pending"), or(isNull(webhookDeliveries.nextRetryAt), lte(webhookDeliveries.nextRetryAt, now))))
|
|
53
|
-
.limit(50);
|
|
54
|
-
let delivered = 0;
|
|
55
|
-
for (const row of pending) {
|
|
56
|
-
if (!isValidCallbackUrl(row.callbackUrl, allowedPrefixes)) {
|
|
57
|
-
await db
|
|
58
|
-
.update(webhookDeliveries)
|
|
59
|
-
.set({ status: "failed", lastError: "Invalid callbackUrl (SSRF blocked)" })
|
|
60
|
-
.where(eq(webhookDeliveries.id, row.id));
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
const headers = { "Content-Type": "application/json" };
|
|
65
|
-
if (serviceKey)
|
|
66
|
-
headers.Authorization = `Bearer ${serviceKey}`;
|
|
67
|
-
const res = await fetch(row.callbackUrl, {
|
|
68
|
-
method: "POST",
|
|
69
|
-
headers,
|
|
70
|
-
body: row.payload,
|
|
71
|
-
});
|
|
72
|
-
if (!res.ok)
|
|
73
|
-
throw new Error(`HTTP ${res.status}`);
|
|
74
|
-
await db.update(webhookDeliveries).set({ status: "delivered" }).where(eq(webhookDeliveries.id, row.id));
|
|
75
|
-
delivered++;
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
const attempts = row.attempts + 1;
|
|
79
|
-
if (attempts >= MAX_DELIVERY_ATTEMPTS) {
|
|
80
|
-
await db
|
|
81
|
-
.update(webhookDeliveries)
|
|
82
|
-
.set({ status: "failed", attempts, lastError: String(err) })
|
|
83
|
-
.where(eq(webhookDeliveries.id, row.id));
|
|
84
|
-
log("Webhook permanently failed", { chargeId: row.chargeId, attempts });
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
const backoffMs = BACKOFF_BASE_MS * 2 ** (attempts - 1);
|
|
88
|
-
const nextRetry = new Date(Date.now() + backoffMs).toISOString();
|
|
89
|
-
await db
|
|
90
|
-
.update(webhookDeliveries)
|
|
91
|
-
.set({ attempts, nextRetryAt: nextRetry, lastError: String(err) })
|
|
92
|
-
.where(eq(webhookDeliveries.id, row.id));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return delivered;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Handle a payment event. Accumulates partial payments in native units.
|
|
100
|
-
* Fires webhook on every payment/confirmation change with canonical statuses.
|
|
101
|
-
*
|
|
102
|
-
* 3-phase webhook lifecycle:
|
|
103
|
-
* 1. Tx first seen -> status: "partial", confirmations: 0
|
|
104
|
-
* 2. Each new block -> status: "partial", confirmations: current
|
|
105
|
-
* 3. Threshold reached + full payment -> status: "confirmed"
|
|
106
|
-
*
|
|
107
|
-
* @param nativeAmount — received amount in native base units (sats for BTC/DOGE, raw token units for ERC20).
|
|
108
|
-
* Pass "0" for confirmation-only updates (no new payment, just more confirmations).
|
|
109
|
-
*/
|
|
110
|
-
export async function handlePayment(db, chargeStore, address, nativeAmount, payload, log) {
|
|
111
|
-
const charge = await chargeStore.getByDepositAddress(address);
|
|
112
|
-
if (!charge) {
|
|
113
|
-
log("Payment to unknown address", { address });
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (charge.creditedAt) {
|
|
117
|
-
return; // Already fully paid and credited
|
|
118
|
-
}
|
|
119
|
-
const { confirmations, confirmationsRequired, amountReceivedCents, txHash } = payload;
|
|
120
|
-
// Accumulate: add this payment to the running total (if nativeAmount > 0)
|
|
121
|
-
const prevReceived = BigInt(charge.receivedAmount ?? "0");
|
|
122
|
-
const thisPayment = BigInt(nativeAmount);
|
|
123
|
-
const totalReceived = (prevReceived + thisPayment).toString();
|
|
124
|
-
const expected = BigInt(charge.expectedAmount ?? "0");
|
|
125
|
-
const isFull = expected > 0n && BigInt(totalReceived) >= expected;
|
|
126
|
-
const isConfirmed = isFull && confirmations >= confirmationsRequired;
|
|
127
|
-
// Update received_amount in DB (only when there's a new payment)
|
|
128
|
-
if (thisPayment > 0n) {
|
|
129
|
-
await db
|
|
130
|
-
.update(cryptoCharges)
|
|
131
|
-
.set({ receivedAmount: totalReceived, filledAmount: totalReceived })
|
|
132
|
-
.where(eq(cryptoCharges.referenceId, charge.referenceId));
|
|
133
|
-
}
|
|
134
|
-
// Determine canonical status
|
|
135
|
-
const status = isConfirmed ? "confirmed" : "partial";
|
|
136
|
-
// Update progress via new API
|
|
137
|
-
await chargeStore.updateProgress(charge.referenceId, {
|
|
138
|
-
status,
|
|
139
|
-
amountReceivedCents,
|
|
140
|
-
confirmations,
|
|
141
|
-
confirmationsRequired,
|
|
142
|
-
txHash,
|
|
143
|
-
});
|
|
144
|
-
if (isConfirmed) {
|
|
145
|
-
await chargeStore.markCredited(charge.referenceId);
|
|
146
|
-
log("Charge confirmed", {
|
|
147
|
-
chargeId: charge.referenceId,
|
|
148
|
-
confirmations,
|
|
149
|
-
confirmationsRequired,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
log("Payment progress", {
|
|
154
|
-
chargeId: charge.referenceId,
|
|
155
|
-
confirmations,
|
|
156
|
-
confirmationsRequired,
|
|
157
|
-
received: totalReceived,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
// Webhook on every event — product shows confirmation progress to user
|
|
161
|
-
if (charge.callbackUrl) {
|
|
162
|
-
await enqueueWebhook(db, charge.referenceId, charge.callbackUrl, {
|
|
163
|
-
chargeId: charge.referenceId,
|
|
164
|
-
chain: charge.chain,
|
|
165
|
-
address: charge.depositAddress,
|
|
166
|
-
amountExpectedCents: charge.amountUsdCents,
|
|
167
|
-
amountReceivedCents,
|
|
168
|
-
confirmations,
|
|
169
|
-
confirmationsRequired,
|
|
170
|
-
txHash,
|
|
171
|
-
status,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// --- Watcher boot ---
|
|
176
|
-
export async function startWatchers(opts) {
|
|
177
|
-
const { db, chargeStore, methodStore, cursorStore, oracle } = opts;
|
|
178
|
-
const pollMs = opts.pollIntervalMs ?? 15_000;
|
|
179
|
-
const deliveryMs = opts.deliveryIntervalMs ?? 10_000;
|
|
180
|
-
const log = opts.log ?? (() => { });
|
|
181
|
-
const allowedPrefixes = opts.allowedCallbackPrefixes ?? ["https://"];
|
|
182
|
-
const serviceKey = opts.serviceKey;
|
|
183
|
-
const timers = [];
|
|
184
|
-
const methods = await methodStore.listEnabled();
|
|
185
|
-
// Route watchers by DB-driven watcherType — no hardcoded chain names.
|
|
186
|
-
// Adding a new chain is a DB INSERT with watcher_type = "utxo" or "evm".
|
|
187
|
-
const utxoMethods = methods.filter((m) => m.watcherType === "utxo");
|
|
188
|
-
const evmMethods = methods.filter((m) => m.watcherType === "evm");
|
|
189
|
-
// --- UTXO Watchers (BTC, LTC, DOGE) ---
|
|
190
|
-
for (const method of utxoMethods) {
|
|
191
|
-
if (!method.rpcUrl)
|
|
192
|
-
continue;
|
|
193
|
-
const rpcCall = createBitcoindRpc({
|
|
194
|
-
rpcUrl: method.rpcUrl,
|
|
195
|
-
rpcUser: opts.bitcoindUser ?? "btcpay",
|
|
196
|
-
rpcPassword: opts.bitcoindPassword ?? "",
|
|
197
|
-
network: "mainnet",
|
|
198
|
-
confirmations: method.confirmations,
|
|
199
|
-
});
|
|
200
|
-
const activeAddresses = await chargeStore.listActiveDepositAddresses();
|
|
201
|
-
const chainAddresses = activeAddresses.filter((a) => a.chain === method.chain).map((a) => a.address);
|
|
202
|
-
const watcher = new BtcWatcher({
|
|
203
|
-
config: {
|
|
204
|
-
rpcUrl: method.rpcUrl,
|
|
205
|
-
rpcUser: opts.bitcoindUser ?? "btcpay",
|
|
206
|
-
rpcPassword: opts.bitcoindPassword ?? "",
|
|
207
|
-
network: "mainnet",
|
|
208
|
-
confirmations: method.confirmations,
|
|
209
|
-
},
|
|
210
|
-
chainId: method.chain,
|
|
211
|
-
rpcCall,
|
|
212
|
-
watchedAddresses: chainAddresses,
|
|
213
|
-
oracle,
|
|
214
|
-
cursorStore,
|
|
215
|
-
onPayment: async (event) => {
|
|
216
|
-
log("UTXO payment", {
|
|
217
|
-
chain: method.chain,
|
|
218
|
-
address: event.address,
|
|
219
|
-
txid: event.txid,
|
|
220
|
-
sats: event.amountSats,
|
|
221
|
-
confirmations: event.confirmations,
|
|
222
|
-
confirmationsRequired: event.confirmationsRequired,
|
|
223
|
-
});
|
|
224
|
-
await handlePayment(db, chargeStore, event.address, String(event.amountSats), {
|
|
225
|
-
txHash: event.txid,
|
|
226
|
-
confirmations: event.confirmations,
|
|
227
|
-
confirmationsRequired: event.confirmationsRequired,
|
|
228
|
-
amountReceivedCents: event.amountUsdCents,
|
|
229
|
-
}, log);
|
|
230
|
-
},
|
|
231
|
-
});
|
|
232
|
-
const importedAddresses = new Set();
|
|
233
|
-
for (const addr of chainAddresses) {
|
|
234
|
-
try {
|
|
235
|
-
await watcher.importAddress(addr);
|
|
236
|
-
importedAddresses.add(addr);
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
log("Failed to import address", { chain: method.chain, address: addr });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
log(`UTXO watcher started (${method.chain})`, { addresses: importedAddresses.size });
|
|
243
|
-
let utxoPolling = false;
|
|
244
|
-
timers.push(setInterval(async () => {
|
|
245
|
-
if (utxoPolling)
|
|
246
|
-
return; // Prevent overlapping polls
|
|
247
|
-
utxoPolling = true;
|
|
248
|
-
try {
|
|
249
|
-
const fresh = await chargeStore.listActiveDepositAddresses();
|
|
250
|
-
const freshChain = fresh.filter((a) => a.chain === method.chain).map((a) => a.address);
|
|
251
|
-
for (const addr of freshChain) {
|
|
252
|
-
if (!importedAddresses.has(addr)) {
|
|
253
|
-
try {
|
|
254
|
-
await watcher.importAddress(addr);
|
|
255
|
-
importedAddresses.add(addr);
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
log("Failed to import new address (will retry)", { chain: method.chain, address: addr });
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
watcher.setWatchedAddresses(freshChain);
|
|
263
|
-
await watcher.poll();
|
|
264
|
-
}
|
|
265
|
-
catch (err) {
|
|
266
|
-
log("UTXO poll error", { chain: method.chain, error: String(err) });
|
|
267
|
-
}
|
|
268
|
-
finally {
|
|
269
|
-
utxoPolling = false;
|
|
270
|
-
}
|
|
271
|
-
}, pollMs));
|
|
272
|
-
}
|
|
273
|
-
// --- Native ETH Watchers (block-scanning for value transfers) ---
|
|
274
|
-
const nativeEvmMethods = evmMethods.filter((m) => m.type === "native");
|
|
275
|
-
const erc20Methods = evmMethods.filter((m) => m.type === "erc20" && m.contractAddress);
|
|
276
|
-
const BACKFILL_BLOCKS = 1000; // Scan ~30min of blocks on first deploy to catch missed deposits
|
|
277
|
-
// Address conversion for EVM-watched chains with non-0x address formats (Tron T...).
|
|
278
|
-
// Only applies to chains routed through the EVM watcher but storing non-hex addresses.
|
|
279
|
-
// UTXO chains (DOGE p2pkh) never enter this path — they use the UTXO watcher.
|
|
280
|
-
const isTronMethod = (method) => (method.addressType === "p2pkh" || method.addressType === "keccak-b58check") && method.chain === "tron";
|
|
281
|
-
const toWatcherAddr = (addr, method) => isTronMethod(method) && isTronAddress(addr) ? tronToHex(addr) : addr;
|
|
282
|
-
const fromWatcherAddr = (addr, method) => isTronMethod(method) ? hexToTron(addr) : addr;
|
|
283
|
-
for (const method of nativeEvmMethods) {
|
|
284
|
-
if (!method.rpcUrl)
|
|
285
|
-
continue;
|
|
286
|
-
const rpcCall = createRpcCaller(method.rpcUrl, JSON.parse(method.rpcHeaders ?? "{}"));
|
|
287
|
-
let latestBlock;
|
|
288
|
-
try {
|
|
289
|
-
const latestHex = (await rpcCall("eth_blockNumber", []));
|
|
290
|
-
latestBlock = Number.parseInt(latestHex, 16);
|
|
291
|
-
}
|
|
292
|
-
catch (err) {
|
|
293
|
-
log("Skipping ETH watcher — RPC unreachable", { chain: method.chain, token: method.token, error: String(err) });
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
const backfillStart = Math.max(0, latestBlock - BACKFILL_BLOCKS);
|
|
297
|
-
const activeAddresses = await chargeStore.listActiveDepositAddresses();
|
|
298
|
-
// Only watch addresses for native charges on this chain (not ERC20 charges)
|
|
299
|
-
const chainAddresses = activeAddresses
|
|
300
|
-
.filter((a) => a.chain === method.chain && a.token === method.token)
|
|
301
|
-
.map((a) => a.address);
|
|
302
|
-
const watcher = new EthWatcher({
|
|
303
|
-
chain: method.chain,
|
|
304
|
-
rpcCall,
|
|
305
|
-
oracle,
|
|
306
|
-
fromBlock: backfillStart,
|
|
307
|
-
watchedAddresses: chainAddresses.map((a) => toWatcherAddr(a, method)),
|
|
308
|
-
cursorStore,
|
|
309
|
-
confirmations: method.confirmations,
|
|
310
|
-
onPayment: async (event) => {
|
|
311
|
-
const dbAddr = fromWatcherAddr(event.to, method);
|
|
312
|
-
log("ETH payment", {
|
|
313
|
-
chain: event.chain,
|
|
314
|
-
to: dbAddr,
|
|
315
|
-
txHash: event.txHash,
|
|
316
|
-
valueWei: event.valueWei,
|
|
317
|
-
confirmations: event.confirmations,
|
|
318
|
-
confirmationsRequired: event.confirmationsRequired,
|
|
319
|
-
});
|
|
320
|
-
await handlePayment(db, chargeStore, dbAddr, event.valueWei, {
|
|
321
|
-
txHash: event.txHash,
|
|
322
|
-
confirmations: event.confirmations,
|
|
323
|
-
confirmationsRequired: event.confirmationsRequired,
|
|
324
|
-
amountReceivedCents: event.amountUsdCents,
|
|
325
|
-
}, log);
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
await watcher.init();
|
|
329
|
-
log(`ETH watcher started (${method.chain}:${method.token})`, { addresses: chainAddresses.length });
|
|
330
|
-
let ethPolling = false;
|
|
331
|
-
timers.push(setInterval(async () => {
|
|
332
|
-
if (ethPolling)
|
|
333
|
-
return;
|
|
334
|
-
ethPolling = true;
|
|
335
|
-
try {
|
|
336
|
-
const fresh = await chargeStore.listActiveDepositAddresses();
|
|
337
|
-
const freshNative = fresh
|
|
338
|
-
.filter((a) => a.chain === method.chain && a.token === method.token)
|
|
339
|
-
.map((a) => a.address);
|
|
340
|
-
watcher.setWatchedAddresses(freshNative.map((a) => toWatcherAddr(a, method)));
|
|
341
|
-
await watcher.poll();
|
|
342
|
-
}
|
|
343
|
-
catch (err) {
|
|
344
|
-
log("ETH poll error", { chain: method.chain, error: String(err) });
|
|
345
|
-
}
|
|
346
|
-
finally {
|
|
347
|
-
ethPolling = false;
|
|
348
|
-
}
|
|
349
|
-
}, pollMs));
|
|
350
|
-
}
|
|
351
|
-
// --- ERC20 Watchers (log-based Transfer event scanning) ---
|
|
352
|
-
for (const method of erc20Methods) {
|
|
353
|
-
if (!method.rpcUrl || !method.contractAddress)
|
|
354
|
-
continue;
|
|
355
|
-
const rpcCall = createRpcCaller(method.rpcUrl, JSON.parse(method.rpcHeaders ?? "{}"));
|
|
356
|
-
let latestBlock;
|
|
357
|
-
try {
|
|
358
|
-
const latestHex = (await rpcCall("eth_blockNumber", []));
|
|
359
|
-
latestBlock = Number.parseInt(latestHex, 16);
|
|
360
|
-
}
|
|
361
|
-
catch (err) {
|
|
362
|
-
log("Skipping EVM watcher — RPC unreachable", { chain: method.chain, token: method.token, error: String(err) });
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
const activeAddresses = await chargeStore.listActiveDepositAddresses();
|
|
366
|
-
const chainAddresses = activeAddresses.filter((a) => a.chain === method.chain).map((a) => a.address);
|
|
367
|
-
const watcher = new EvmWatcher({
|
|
368
|
-
chain: method.chain,
|
|
369
|
-
token: method.token,
|
|
370
|
-
rpcCall,
|
|
371
|
-
fromBlock: latestBlock,
|
|
372
|
-
watchedAddresses: chainAddresses.map((a) => toWatcherAddr(a, method)),
|
|
373
|
-
contractAddress: toWatcherAddr(method.contractAddress, method),
|
|
374
|
-
decimals: method.decimals,
|
|
375
|
-
confirmations: method.confirmations,
|
|
376
|
-
cursorStore,
|
|
377
|
-
onPayment: async (event) => {
|
|
378
|
-
const dbAddr = fromWatcherAddr(event.to, method);
|
|
379
|
-
log("EVM payment", {
|
|
380
|
-
chain: event.chain,
|
|
381
|
-
token: event.token,
|
|
382
|
-
to: dbAddr,
|
|
383
|
-
txHash: event.txHash,
|
|
384
|
-
confirmations: event.confirmations,
|
|
385
|
-
confirmationsRequired: event.confirmationsRequired,
|
|
386
|
-
});
|
|
387
|
-
await handlePayment(db, chargeStore, dbAddr, event.rawAmount, {
|
|
388
|
-
txHash: event.txHash,
|
|
389
|
-
confirmations: event.confirmations,
|
|
390
|
-
confirmationsRequired: event.confirmationsRequired,
|
|
391
|
-
amountReceivedCents: event.amountUsdCents,
|
|
392
|
-
}, log);
|
|
393
|
-
},
|
|
394
|
-
});
|
|
395
|
-
await watcher.init();
|
|
396
|
-
log(`EVM watcher started (${method.chain}:${method.token})`, { addresses: chainAddresses.length });
|
|
397
|
-
let evmPolling = false;
|
|
398
|
-
timers.push(setInterval(async () => {
|
|
399
|
-
if (evmPolling)
|
|
400
|
-
return;
|
|
401
|
-
evmPolling = true;
|
|
402
|
-
try {
|
|
403
|
-
const fresh = await chargeStore.listActiveDepositAddresses();
|
|
404
|
-
const freshChain = fresh.filter((a) => a.chain === method.chain).map((a) => a.address);
|
|
405
|
-
watcher.setWatchedAddresses(freshChain.map((a) => toWatcherAddr(a, method)));
|
|
406
|
-
await watcher.poll();
|
|
407
|
-
}
|
|
408
|
-
catch (err) {
|
|
409
|
-
log("EVM poll error", { chain: method.chain, token: method.token, error: String(err) });
|
|
410
|
-
}
|
|
411
|
-
finally {
|
|
412
|
-
evmPolling = false;
|
|
413
|
-
}
|
|
414
|
-
}, pollMs));
|
|
415
|
-
}
|
|
416
|
-
// --- Webhook delivery outbox processor ---
|
|
417
|
-
timers.push(setInterval(async () => {
|
|
418
|
-
try {
|
|
419
|
-
const count = await processDeliveries(db, allowedPrefixes, log, serviceKey);
|
|
420
|
-
if (count > 0)
|
|
421
|
-
log("Webhooks delivered", { count });
|
|
422
|
-
}
|
|
423
|
-
catch (err) {
|
|
424
|
-
log("Delivery loop error", { error: String(err) });
|
|
425
|
-
}
|
|
426
|
-
}, deliveryMs));
|
|
427
|
-
log("All watchers started", {
|
|
428
|
-
utxo: utxoMethods.length,
|
|
429
|
-
evm: erc20Methods.length,
|
|
430
|
-
eth: nativeEvmMethods.length,
|
|
431
|
-
pollMs,
|
|
432
|
-
deliveryMs,
|
|
433
|
-
});
|
|
434
|
-
return () => {
|
|
435
|
-
for (const t of timers)
|
|
436
|
-
clearInterval(t);
|
|
437
|
-
};
|
|
438
|
-
}
|