@wopr-network/platform-core 1.65.0 → 1.66.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/billing/crypto/key-server-entry.js +37 -12
- package/dist/billing/crypto/key-server.d.ts +12 -0
- package/dist/billing/crypto/key-server.js +21 -4
- package/dist/billing/crypto/plugin-watcher-service.d.ts +32 -0
- package/dist/billing/crypto/plugin-watcher-service.js +113 -0
- package/docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md +48 -34
- package/drizzle/migrations/0000_slippery_mandrill.sql +133 -133
- package/drizzle/migrations/0001_infrastructure_extraction.sql +102 -102
- package/drizzle/migrations/0002_gateway_service_keys.sql +4 -4
- package/drizzle/migrations/0003_double_entry_ledger.sql +15 -15
- package/drizzle/migrations/0005_stablecoin_columns.sql +7 -7
- package/drizzle/migrations/0006_invite_acceptance.sql +2 -2
- package/drizzle/migrations/0010_oracle_address.sql +2 -2
- package/drizzle/migrations/0011_notification_templates.sql +1 -1
- package/drizzle/migrations/0014_crypto_key_server.sql +2 -2
- package/drizzle/migrations/0015_callback_url.sql +3 -3
- package/drizzle/migrations/0016_charge_progress_columns.sql +4 -4
- package/drizzle/migrations/0020_encoding_params_column.sql +1 -1
- package/drizzle/migrations/0021_watcher_type_column.sql +1 -1
- package/drizzle/migrations/0022_oracle_asset_id_column.sql +1 -1
- package/package.json +2 -1
- package/src/billing/crypto/key-server-entry.ts +48 -12
- package/src/billing/crypto/key-server.ts +25 -3
- package/src/billing/crypto/plugin-watcher-service.ts +148 -0
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
-- Adds atomic derivation counter + path registry + address log.
|
|
3
3
|
|
|
4
4
|
-- 1. Add network column to payment_methods (parallel to chain)
|
|
5
|
-
ALTER TABLE "payment_methods" ADD COLUMN "network" text NOT NULL DEFAULT 'mainnet';
|
|
5
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "network" text NOT NULL DEFAULT 'mainnet';
|
|
6
6
|
--> statement-breakpoint
|
|
7
7
|
|
|
8
8
|
-- 2. Add next_index atomic counter to payment_methods
|
|
9
|
-
ALTER TABLE "payment_methods" ADD COLUMN "next_index" integer NOT NULL DEFAULT 0;
|
|
9
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "next_index" integer NOT NULL DEFAULT 0;
|
|
10
10
|
--> statement-breakpoint
|
|
11
11
|
|
|
12
12
|
-- 3. BIP-44 path allocation registry
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
-- Watcher service schema additions: webhook outbox + charge amount tracking.
|
|
2
2
|
|
|
3
3
|
-- 1. callback_url for webhook delivery
|
|
4
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "callback_url" text;
|
|
4
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "callback_url" text;
|
|
5
5
|
--> statement-breakpoint
|
|
6
6
|
|
|
7
7
|
-- 2. Expected crypto amount in native base units (locked at charge creation)
|
|
8
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "expected_amount" text;
|
|
8
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "expected_amount" text;
|
|
9
9
|
--> statement-breakpoint
|
|
10
10
|
|
|
11
11
|
-- 3. Running total of received crypto in native base units (partial payments)
|
|
12
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "received_amount" text;
|
|
12
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "received_amount" text;
|
|
13
13
|
--> statement-breakpoint
|
|
14
14
|
|
|
15
15
|
-- 4. Webhook delivery outbox — durable retry for payment callbacks
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "confirmations" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
2
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "confirmations_required" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
|
3
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "tx_hash" text;--> statement-breakpoint
|
|
4
|
-
ALTER TABLE "crypto_charges" ADD COLUMN "amount_received_cents" integer DEFAULT 0 NOT NULL;
|
|
1
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "confirmations" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
2
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "confirmations_required" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "tx_hash" text;--> statement-breakpoint
|
|
4
|
+
ALTER TABLE "crypto_charges" ADD COLUMN IF NOT EXISTS "amount_received_cents" integer DEFAULT 0 NOT NULL;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ALTER TABLE "payment_methods" ADD COLUMN "encoding_params" text DEFAULT '{}' NOT NULL;
|
|
1
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "encoding_params" text DEFAULT '{}' NOT NULL;
|
|
2
2
|
--> statement-breakpoint
|
|
3
3
|
UPDATE "payment_methods" SET "encoding_params" = '{"hrp":"bc"}' WHERE "address_type" = 'bech32' AND "chain" = 'bitcoin';
|
|
4
4
|
--> statement-breakpoint
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
ALTER TABLE "payment_methods" ADD COLUMN "watcher_type" text DEFAULT 'evm' NOT NULL;
|
|
1
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "watcher_type" text DEFAULT 'evm' NOT NULL;
|
|
2
2
|
--> statement-breakpoint
|
|
3
3
|
UPDATE "payment_methods" SET "watcher_type" = 'utxo' WHERE "chain" IN ('bitcoin', 'litecoin', 'dogecoin');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ALTER TABLE "payment_methods" ADD COLUMN "oracle_asset_id" text;
|
|
1
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "oracle_asset_id" text;
|
|
2
2
|
--> statement-breakpoint
|
|
3
3
|
UPDATE "payment_methods" SET "oracle_asset_id" = 'bitcoin' WHERE "token" = 'BTC';
|
|
4
4
|
--> statement-breakpoint
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wopr-network/platform-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.66.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -140,6 +140,7 @@
|
|
|
140
140
|
"@scure/base": "^2.0.0",
|
|
141
141
|
"@scure/bip32": "^2.0.1",
|
|
142
142
|
"@scure/bip39": "^2.0.1",
|
|
143
|
+
"@wopr-network/crypto-plugins": "^1.0.1",
|
|
143
144
|
"handlebars": "^4.7.8",
|
|
144
145
|
"js-yaml": "^4.1.1",
|
|
145
146
|
"postmark": "^4.0.7",
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/* biome-ignore-all lint/suspicious/noConsole: standalone entry point */
|
|
10
10
|
import { serve } from "@hono/node-server";
|
|
11
|
+
import {
|
|
12
|
+
bitcoinPlugin,
|
|
13
|
+
dogecoinPlugin,
|
|
14
|
+
evmPlugin,
|
|
15
|
+
litecoinPlugin,
|
|
16
|
+
solanaPlugin,
|
|
17
|
+
tronPlugin,
|
|
18
|
+
} from "@wopr-network/crypto-plugins";
|
|
11
19
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
12
20
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
13
21
|
import pg from "pg";
|
|
@@ -21,6 +29,8 @@ import { CoinGeckoOracle } from "./oracle/coingecko.js";
|
|
|
21
29
|
import { CompositeOracle } from "./oracle/composite.js";
|
|
22
30
|
import { FixedPriceOracle } from "./oracle/fixed.js";
|
|
23
31
|
import { DrizzlePaymentMethodStore } from "./payment-method-store.js";
|
|
32
|
+
import { PluginRegistry } from "./plugin/registry.js";
|
|
33
|
+
import { startPluginWatchers } from "./plugin-watcher-service.js";
|
|
24
34
|
import { startWatchers } from "./watcher-service.js";
|
|
25
35
|
|
|
26
36
|
const PORT = Number(process.env.PORT ?? "3100");
|
|
@@ -64,6 +74,19 @@ async function main(): Promise<void> {
|
|
|
64
74
|
const coingecko = new CoinGeckoOracle({ tokenIds: dbTokenIds });
|
|
65
75
|
const oracle = new CompositeOracle(chainlink, coingecko);
|
|
66
76
|
|
|
77
|
+
// Build plugin registry — one plugin per chain family
|
|
78
|
+
const registry = new PluginRegistry();
|
|
79
|
+
registry.register(bitcoinPlugin);
|
|
80
|
+
registry.register(litecoinPlugin);
|
|
81
|
+
registry.register(dogecoinPlugin);
|
|
82
|
+
registry.register(evmPlugin);
|
|
83
|
+
registry.register(tronPlugin);
|
|
84
|
+
registry.register(solanaPlugin);
|
|
85
|
+
console.log(
|
|
86
|
+
`[crypto-key-server] Registered ${registry.list().length} chain plugins:`,
|
|
87
|
+
registry.list().map((p) => p.pluginId),
|
|
88
|
+
);
|
|
89
|
+
|
|
67
90
|
const app = createKeyServerApp({
|
|
68
91
|
db,
|
|
69
92
|
chargeStore,
|
|
@@ -71,21 +94,34 @@ async function main(): Promise<void> {
|
|
|
71
94
|
oracle,
|
|
72
95
|
serviceKey: SERVICE_KEY,
|
|
73
96
|
adminToken: ADMIN_TOKEN,
|
|
97
|
+
registry,
|
|
74
98
|
});
|
|
75
99
|
|
|
76
|
-
// Boot watchers
|
|
100
|
+
// Boot plugin-driven watchers — polls for payments, sends webhooks.
|
|
101
|
+
// Falls back to legacy startWatchers() if USE_LEGACY_WATCHERS=1 is set.
|
|
77
102
|
const cursorStore = new DrizzleWatcherCursorStore(db);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
const useLegacy = process.env.USE_LEGACY_WATCHERS === "1";
|
|
104
|
+
const stopWatchers = useLegacy
|
|
105
|
+
? await startWatchers({
|
|
106
|
+
db,
|
|
107
|
+
chargeStore,
|
|
108
|
+
methodStore,
|
|
109
|
+
cursorStore,
|
|
110
|
+
oracle,
|
|
111
|
+
bitcoindUser: BITCOIND_USER,
|
|
112
|
+
bitcoindPassword: BITCOIND_PASSWORD,
|
|
113
|
+
serviceKey: SERVICE_KEY,
|
|
114
|
+
log: (msg, meta) => console.log(`[watcher] ${msg}`, meta ?? ""),
|
|
115
|
+
})
|
|
116
|
+
: await startPluginWatchers({
|
|
117
|
+
db,
|
|
118
|
+
chargeStore,
|
|
119
|
+
methodStore,
|
|
120
|
+
cursorStore,
|
|
121
|
+
oracle,
|
|
122
|
+
registry,
|
|
123
|
+
log: (msg, meta) => console.log(`[watcher] ${msg}`, meta ?? ""),
|
|
124
|
+
});
|
|
89
125
|
|
|
90
126
|
const server = serve({ fetch: app.fetch, port: PORT });
|
|
91
127
|
console.log(`[crypto-key-server] Listening on :${PORT}`);
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ~200 lines of new code wrapping platform-core's existing crypto modules.
|
|
9
9
|
*/
|
|
10
|
+
|
|
11
|
+
import { HDKey } from "@scure/bip32";
|
|
10
12
|
import { eq, sql } from "drizzle-orm";
|
|
11
13
|
import { Hono } from "hono";
|
|
12
14
|
import type { DrizzleDb } from "../../db/index.js";
|
|
@@ -18,6 +20,7 @@ import { centsToNative } from "./oracle/convert.js";
|
|
|
18
20
|
import type { IPriceOracle } from "./oracle/types.js";
|
|
19
21
|
import { AssetNotSupportedError } from "./oracle/types.js";
|
|
20
22
|
import type { IPaymentMethodStore } from "./payment-method-store.js";
|
|
23
|
+
import type { PluginRegistry } from "./plugin/registry.js";
|
|
21
24
|
|
|
22
25
|
export interface KeyServerDeps {
|
|
23
26
|
db: DrizzleDb;
|
|
@@ -28,6 +31,8 @@ export interface KeyServerDeps {
|
|
|
28
31
|
serviceKey?: string;
|
|
29
32
|
/** Bearer token for admin routes. If unset, admin routes are disabled. */
|
|
30
33
|
adminToken?: string;
|
|
34
|
+
/** Plugin registry for address encoding. Falls back to address-gen.ts when absent. */
|
|
35
|
+
registry?: PluginRegistry;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/**
|
|
@@ -42,6 +47,7 @@ async function deriveNextAddress(
|
|
|
42
47
|
db: DrizzleDb,
|
|
43
48
|
chainId: string,
|
|
44
49
|
tenantId?: string,
|
|
50
|
+
registry?: PluginRegistry,
|
|
45
51
|
): Promise<{ address: string; index: number; chain: string; token: string }> {
|
|
46
52
|
const maxRetries = 10;
|
|
47
53
|
const dbWithTx = db as unknown as { transaction: (fn: (tx: DrizzleDb) => Promise<unknown>) => Promise<unknown> };
|
|
@@ -68,7 +74,23 @@ async function deriveNextAddress(
|
|
|
68
74
|
} catch {
|
|
69
75
|
throw new Error(`Invalid encoding_params JSON for chain ${chainId}: ${method.encodingParams}`);
|
|
70
76
|
}
|
|
71
|
-
|
|
77
|
+
|
|
78
|
+
// Plugin-driven encoding: look up the plugin, use its encoder.
|
|
79
|
+
// Falls back to legacy deriveAddress() when no registry or no matching plugin.
|
|
80
|
+
let address: string;
|
|
81
|
+
const pluginId = method.pluginId ?? (method.watcherType === "utxo" ? "bitcoin" : method.watcherType);
|
|
82
|
+
const plugin = registry?.get(pluginId ?? "");
|
|
83
|
+
const encodingKey = method.encoding ?? method.addressType;
|
|
84
|
+
const encoder = plugin?.encoders[encodingKey];
|
|
85
|
+
|
|
86
|
+
if (encoder) {
|
|
87
|
+
const master = HDKey.fromExtendedKey(method.xpub);
|
|
88
|
+
const child = master.deriveChild(0).deriveChild(index);
|
|
89
|
+
if (!child.publicKey) throw new Error("Failed to derive public key");
|
|
90
|
+
address = encoder.encode(child.publicKey, encodingParams as Record<string, string | undefined>);
|
|
91
|
+
} else {
|
|
92
|
+
address = deriveAddress(method.xpub, index, method.addressType, encodingParams);
|
|
93
|
+
}
|
|
72
94
|
|
|
73
95
|
// Step 2: Record in immutable log. If this address was already derived by a
|
|
74
96
|
// sibling chain (shared xpub), the unique constraint fires and we retry
|
|
@@ -146,7 +168,7 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
|
|
|
146
168
|
if (!body.chain) return c.json({ error: "chain is required" }, 400);
|
|
147
169
|
|
|
148
170
|
const tenantId = c.req.header("X-Tenant-Id");
|
|
149
|
-
const result = await deriveNextAddress(deps.db, body.chain, tenantId ?? undefined);
|
|
171
|
+
const result = await deriveNextAddress(deps.db, body.chain, tenantId ?? undefined, deps.registry);
|
|
150
172
|
return c.json(result, 201);
|
|
151
173
|
});
|
|
152
174
|
|
|
@@ -164,7 +186,7 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
|
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
const tenantId = c.req.header("X-Tenant-Id") ?? "unknown";
|
|
167
|
-
const { address, index, chain, token } = await deriveNextAddress(deps.db, body.chain, tenantId);
|
|
189
|
+
const { address, index, chain, token } = await deriveNextAddress(deps.db, body.chain, tenantId, deps.registry);
|
|
168
190
|
|
|
169
191
|
// Look up payment method for decimals + oracle config
|
|
170
192
|
const method = await deps.methodStore.getById(body.chain);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-driven watcher service — replaces the hardcoded watcher-service.ts.
|
|
3
|
+
*
|
|
4
|
+
* Instead of importing BtcWatcher/EvmWatcher/EthWatcher directly,
|
|
5
|
+
* this delegates to IChainPlugin.createWatcher() from the plugin registry.
|
|
6
|
+
* Adding a new chain = register a plugin + INSERT a payment_methods row.
|
|
7
|
+
*
|
|
8
|
+
* Payment flow is unchanged:
|
|
9
|
+
* plugin.poll() -> PaymentEvent[] -> handlePayment() -> credit + webhook
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DrizzleDb } from "../../db/index.js";
|
|
13
|
+
import type { ICryptoChargeRepository } from "./charge-store.js";
|
|
14
|
+
import type { IWatcherCursorStore } from "./cursor-store.js";
|
|
15
|
+
import type { IPriceOracle } from "./oracle/types.js";
|
|
16
|
+
import type { IPaymentMethodStore, PaymentMethodRecord } from "./payment-method-store.js";
|
|
17
|
+
import type { IChainPlugin, IChainWatcher } from "./plugin/interfaces.js";
|
|
18
|
+
import type { PluginRegistry } from "./plugin/registry.js";
|
|
19
|
+
import { handlePayment } from "./watcher-service.js";
|
|
20
|
+
|
|
21
|
+
export interface PluginWatcherServiceOpts {
|
|
22
|
+
db: DrizzleDb;
|
|
23
|
+
chargeStore: ICryptoChargeRepository;
|
|
24
|
+
methodStore: IPaymentMethodStore;
|
|
25
|
+
cursorStore: IWatcherCursorStore;
|
|
26
|
+
oracle: IPriceOracle;
|
|
27
|
+
registry: PluginRegistry;
|
|
28
|
+
pollIntervalMs?: number;
|
|
29
|
+
log?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Map legacy watcher_type values to plugin IDs for backward compatibility. */
|
|
33
|
+
const WATCHER_TYPE_TO_PLUGIN: Record<string, string> = {
|
|
34
|
+
utxo: "bitcoin",
|
|
35
|
+
evm: "evm",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function resolvePlugin(registry: PluginRegistry, method: PaymentMethodRecord): IChainPlugin | undefined {
|
|
39
|
+
// Prefer explicit plugin_id, fall back to watcher_type mapping
|
|
40
|
+
const id = method.pluginId ?? WATCHER_TYPE_TO_PLUGIN[method.watcherType];
|
|
41
|
+
return id ? registry.get(id) : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Boot plugin-driven watchers for all enabled payment methods.
|
|
46
|
+
*
|
|
47
|
+
* Returns a cleanup function that stops all poll timers and watchers.
|
|
48
|
+
*/
|
|
49
|
+
export async function startPluginWatchers(opts: PluginWatcherServiceOpts): Promise<() => void> {
|
|
50
|
+
const { db, chargeStore, methodStore, cursorStore, oracle, registry } = opts;
|
|
51
|
+
const pollMs = opts.pollIntervalMs ?? 15_000;
|
|
52
|
+
const log = opts.log ?? (() => {});
|
|
53
|
+
|
|
54
|
+
const methods = await methodStore.listEnabled();
|
|
55
|
+
const timers: ReturnType<typeof setInterval>[] = [];
|
|
56
|
+
const watchers: IChainWatcher[] = [];
|
|
57
|
+
|
|
58
|
+
for (const method of methods) {
|
|
59
|
+
if (!method.rpcUrl) continue;
|
|
60
|
+
|
|
61
|
+
const plugin = resolvePlugin(registry, method);
|
|
62
|
+
if (!plugin) {
|
|
63
|
+
log("No plugin found, skipping method", { id: method.id, chain: method.chain, watcherType: method.watcherType });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const watcher = plugin.createWatcher({
|
|
68
|
+
rpcUrl: method.rpcUrl,
|
|
69
|
+
rpcHeaders: JSON.parse(method.rpcHeaders ?? "{}"),
|
|
70
|
+
oracle,
|
|
71
|
+
cursorStore,
|
|
72
|
+
token: method.token,
|
|
73
|
+
chain: method.chain,
|
|
74
|
+
contractAddress: method.contractAddress ?? undefined,
|
|
75
|
+
decimals: method.decimals,
|
|
76
|
+
confirmations: method.confirmations,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await watcher.init();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
log("Watcher init failed, skipping", { chain: method.chain, token: method.token, error: String(err) });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Seed watched addresses from active charges
|
|
87
|
+
const active = await chargeStore.listActiveDepositAddresses();
|
|
88
|
+
const addrs = active.filter((a) => a.chain === method.chain && a.token === method.token).map((a) => a.address);
|
|
89
|
+
watcher.setWatchedAddresses(addrs);
|
|
90
|
+
|
|
91
|
+
watchers.push(watcher);
|
|
92
|
+
log(`Plugin watcher started (${method.chain}:${method.token})`, {
|
|
93
|
+
plugin: plugin.pluginId,
|
|
94
|
+
addresses: addrs.length,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
let polling = false;
|
|
98
|
+
timers.push(
|
|
99
|
+
setInterval(async () => {
|
|
100
|
+
if (polling) return;
|
|
101
|
+
polling = true;
|
|
102
|
+
try {
|
|
103
|
+
// Refresh watched addresses each cycle
|
|
104
|
+
const fresh = await chargeStore.listActiveDepositAddresses();
|
|
105
|
+
const freshAddrs = fresh
|
|
106
|
+
.filter((a) => a.chain === method.chain && a.token === method.token)
|
|
107
|
+
.map((a) => a.address);
|
|
108
|
+
watcher.setWatchedAddresses(freshAddrs);
|
|
109
|
+
|
|
110
|
+
const events = await watcher.poll();
|
|
111
|
+
for (const ev of events) {
|
|
112
|
+
log("Plugin payment", {
|
|
113
|
+
chain: ev.chain,
|
|
114
|
+
token: ev.token,
|
|
115
|
+
to: ev.to,
|
|
116
|
+
txHash: ev.txHash,
|
|
117
|
+
confirmations: ev.confirmations,
|
|
118
|
+
});
|
|
119
|
+
await handlePayment(
|
|
120
|
+
db,
|
|
121
|
+
chargeStore,
|
|
122
|
+
ev.to,
|
|
123
|
+
ev.rawAmount,
|
|
124
|
+
{
|
|
125
|
+
txHash: ev.txHash,
|
|
126
|
+
confirmations: ev.confirmations,
|
|
127
|
+
confirmationsRequired: ev.confirmationsRequired,
|
|
128
|
+
amountReceivedCents: ev.amountUsdCents,
|
|
129
|
+
},
|
|
130
|
+
log,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
log("Plugin poll error", { chain: method.chain, token: method.token, error: String(err) });
|
|
135
|
+
} finally {
|
|
136
|
+
polling = false;
|
|
137
|
+
}
|
|
138
|
+
}, pollMs),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
log("All plugin watchers started", { count: watchers.length, pollMs });
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
for (const t of timers) clearInterval(t);
|
|
146
|
+
for (const w of watchers) w.stop();
|
|
147
|
+
};
|
|
148
|
+
}
|