@wopr-network/platform-core 1.64.0 → 1.66.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.
Files changed (40) hide show
  1. package/dist/billing/crypto/index.d.ts +2 -0
  2. package/dist/billing/crypto/index.js +1 -0
  3. package/dist/billing/crypto/key-server-entry.js +37 -12
  4. package/dist/billing/crypto/key-server.d.ts +12 -0
  5. package/dist/billing/crypto/key-server.js +24 -4
  6. package/dist/billing/crypto/payment-method-store.d.ts +3 -0
  7. package/dist/billing/crypto/payment-method-store.js +9 -0
  8. package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +1 -0
  9. package/dist/billing/crypto/plugin/__tests__/integration.test.js +58 -0
  10. package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +1 -0
  11. package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +46 -0
  12. package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +1 -0
  13. package/dist/billing/crypto/plugin/__tests__/registry.test.js +49 -0
  14. package/dist/billing/crypto/plugin/index.d.ts +2 -0
  15. package/dist/billing/crypto/plugin/index.js +1 -0
  16. package/dist/billing/crypto/plugin/interfaces.d.ts +97 -0
  17. package/dist/billing/crypto/plugin/interfaces.js +2 -0
  18. package/dist/billing/crypto/plugin/registry.d.ts +8 -0
  19. package/dist/billing/crypto/plugin/registry.js +21 -0
  20. package/dist/billing/crypto/plugin-watcher-service.d.ts +32 -0
  21. package/dist/billing/crypto/plugin-watcher-service.js +113 -0
  22. package/dist/db/schema/crypto.d.ts +328 -0
  23. package/dist/db/schema/crypto.js +33 -1
  24. package/dist/db/schema/snapshots.d.ts +1 -1
  25. package/docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md +48 -34
  26. package/drizzle/migrations/0023_key_rings_table.sql +35 -0
  27. package/drizzle/migrations/0024_backfill_key_rings.sql +75 -0
  28. package/package.json +6 -1
  29. package/src/billing/crypto/index.ts +9 -0
  30. package/src/billing/crypto/key-server-entry.ts +48 -12
  31. package/src/billing/crypto/key-server.ts +28 -3
  32. package/src/billing/crypto/payment-method-store.ts +12 -0
  33. package/src/billing/crypto/plugin/__tests__/integration.test.ts +64 -0
  34. package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +51 -0
  35. package/src/billing/crypto/plugin/__tests__/registry.test.ts +58 -0
  36. package/src/billing/crypto/plugin/index.ts +17 -0
  37. package/src/billing/crypto/plugin/interfaces.ts +106 -0
  38. package/src/billing/crypto/plugin/registry.ts +26 -0
  39. package/src/billing/crypto/plugin-watcher-service.ts +148 -0
  40. package/src/db/schema/crypto.ts +43 -1
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { sql } from "drizzle-orm";
2
- import { boolean, index, integer, pgTable, primaryKey, text } from "drizzle-orm/pg-core";
2
+ import { boolean, index, integer, pgTable, primaryKey, serial, text, uniqueIndex } from "drizzle-orm/pg-core";
3
3
 
4
4
  /**
5
5
  * Crypto payment charges — tracks the lifecycle of each payment.
@@ -89,6 +89,9 @@ export const paymentMethods = pgTable("payment_methods", {
89
89
  oracleAssetId: text("oracle_asset_id"), // CoinGecko slug (e.g. "bitcoin", "tron"). Null = stablecoin (1:1 USD) or use token symbol fallback.
90
90
  confirmations: integer("confirmations").notNull().default(1),
91
91
  nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
92
+ keyRingId: text("key_ring_id"), // FK to key_rings.id (nullable during migration)
93
+ encoding: text("encoding"), // address encoding override (e.g. "bech32", "p2pkh", "evm")
94
+ pluginId: text("plugin_id"), // plugin identifier (e.g. "evm", "utxo", "solana")
92
95
  createdAt: text("created_at").notNull().default(sql`(now())`),
93
96
  });
94
97
 
@@ -161,3 +164,42 @@ export const watcherProcessed = pgTable(
161
164
  },
162
165
  (table) => [primaryKey({ columns: [table.watcherId, table.txId] })],
163
166
  );
167
+
168
+ /**
169
+ * Key rings — decouples key material (xpub/seed) from payment methods.
170
+ * Each key ring maps to a BIP-44 coin type + account index.
171
+ */
172
+ export const keyRings = pgTable(
173
+ "key_rings",
174
+ {
175
+ id: text("id").primaryKey(),
176
+ curve: text("curve").notNull(), // "secp256k1" | "ed25519"
177
+ derivationScheme: text("derivation_scheme").notNull(), // "bip32" | "slip10" | "ed25519-hd"
178
+ derivationMode: text("derivation_mode").notNull().default("on-demand"), // "on-demand" | "pre-derived"
179
+ keyMaterial: text("key_material").notNull().default("{}"), // JSON: { xpub: "..." }
180
+ coinType: integer("coin_type").notNull(), // BIP-44 coin type
181
+ accountIndex: integer("account_index").notNull().default(0),
182
+ createdAt: text("created_at").notNull().default(sql`(now())`),
183
+ },
184
+ (table) => [uniqueIndex("key_rings_path_unique").on(table.coinType, table.accountIndex)],
185
+ );
186
+
187
+ /**
188
+ * Pre-derived address pool — for Ed25519 chains that need offline derivation.
189
+ * Addresses are derived in batches and assigned on demand.
190
+ */
191
+ export const addressPool = pgTable(
192
+ "address_pool",
193
+ {
194
+ id: serial("id").primaryKey(),
195
+ keyRingId: text("key_ring_id")
196
+ .notNull()
197
+ .references(() => keyRings.id),
198
+ derivationIndex: integer("derivation_index").notNull(),
199
+ publicKey: text("public_key").notNull(),
200
+ address: text("address").notNull(),
201
+ assignedTo: text("assigned_to"), // charge reference or tenant ID
202
+ createdAt: text("created_at").notNull().default(sql`(now())`),
203
+ },
204
+ (table) => [uniqueIndex("address_pool_ring_index").on(table.keyRingId, table.derivationIndex)],
205
+ );