@wopr-network/platform-core 1.63.2 → 1.64.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.
@@ -0,0 +1,309 @@
1
+ # Crypto Plugin Architecture
2
+
3
+ **Date:** 2026-03-24
4
+ **Status:** Approved
5
+ **Problem:** Adding new blockchain curves (Ed25519 for Solana/TON) requires hacking into hardcoded secp256k1 assumptions across 6+ files. The system conflates curve, derivation, and encoding into a single `address_type` column.
6
+ **Solution:** Plugin-based architecture where each chain is an independent npm package implementing standard interfaces. Platform-core defines interfaces only.
7
+
8
+ ## Key Derivation Model
9
+
10
+ Two derivation models depending on the curve's capabilities:
11
+
12
+ ### secp256k1 chains (BTC, ETH, DOGE, LTC, TRX, etc.)
13
+ BIP-32 supports public-key-only child derivation. The pay server holds an **xpub** (no private key). Addresses are derived on demand. Mnemonic never touches the server.
14
+
15
+ ### Ed25519 chains (Solana, TON, etc.)
16
+ SLIP-0010 Ed25519 only supports **hardened** derivation — child public keys cannot be derived from a parent public key alone. The mnemonic MUST NOT be on the server.
17
+
18
+ **Solution: Pre-derived address pool.** The sweep CLI (which has the mnemonic) pre-generates N addresses at known derivation indices and uploads them to the key server with their public key as a commitment proof. The key server:
19
+
20
+ 1. Receives `(index, publicKey, address)` tuples from the CLI
21
+ 2. Validates: re-encodes `publicKey` → address using the chain's encoder, confirms it matches
22
+ 3. Stores validated addresses in the pool
23
+ 4. Hands them out to charges on demand, recording which index was assigned
24
+
25
+ The derivation is still **deterministic and provable** — the sweep CLI can always re-derive the private key at any index. The key server can verify any address without the private key.
26
+
27
+ **Pool replenishment:**
28
+ ```bash
29
+ openssl enc ... -d | npx @wopr-network/crypto-sweep replenish --chain solana --count 100
30
+ ```
31
+
32
+ The `key_rings` table stores the derivation mode:
33
+
34
+ | `derivation_mode` | Behavior |
35
+ |---|---|
36
+ | `"on-demand"` | secp256k1 — derive from xpub at request time |
37
+ | `"pool"` | Ed25519 — draw from pre-derived address pool |
38
+
39
+ ## Core Interfaces (platform-core)
40
+
41
+ Platform-core becomes the interface package. It defines what a chain plugin must implement but contains no chain-specific code.
42
+
43
+ ### PaymentEvent
44
+ The contract between plugins and platform-core.
45
+ ```ts
46
+ interface PaymentEvent {
47
+ chain: string;
48
+ token: string;
49
+ from: string;
50
+ to: string;
51
+ rawAmount: string;
52
+ amountUsdCents: number;
53
+ txHash: string;
54
+ blockNumber: number;
55
+ confirmations: number;
56
+ confirmationsRequired: number;
57
+ }
58
+ ```
59
+
60
+ ### ICurveDeriver
61
+ Derives child public keys from key material. Implemented by KeyRing.
62
+ ```ts
63
+ interface ICurveDeriver {
64
+ derivePublicKey(chainIndex: number, addressIndex: number): Uint8Array;
65
+ getCurve(): "secp256k1" | "ed25519";
66
+ }
67
+ ```
68
+
69
+ For secp256k1: implemented using xpub + BIP-32 non-hardened derivation.
70
+ For Ed25519: NOT used on the server. Only used in the sweep CLI which has the mnemonic. The server uses the pre-derived pool instead.
71
+
72
+ ### IAddressEncoder
73
+ Pure function — public key bytes to address string.
74
+ ```ts
75
+ interface IAddressEncoder {
76
+ encode(publicKey: Uint8Array, params: EncodingParams): string;
77
+ encodingType(): string;
78
+ }
79
+
80
+ interface EncodingParams {
81
+ hrp?: string; // bech32: "bc", "ltc", "tb"
82
+ version?: string; // p2pkh/keccak-b58check: "0x1e", "0x41"
83
+ }
84
+ ```
85
+
86
+ ### IChainWatcher
87
+ Detects payments at watched addresses.
88
+ ```ts
89
+ interface IChainWatcher {
90
+ init(): Promise<void>;
91
+ poll(): Promise<PaymentEvent[]>;
92
+ setWatchedAddresses(addresses: string[]): void;
93
+ getCursor(): number;
94
+ stop(): void;
95
+ }
96
+ ```
97
+
98
+ Oracle/pricing: the watcher receives an `IPriceOracle` at construction time (via `WatcherOpts`). The plugin calls `oracle.getPrice(token)` to convert raw amounts to USD cents. Price conversion is the plugin's responsibility — platform-core provides the oracle.
99
+
100
+ ### ISweepStrategy
101
+ Scans balances and broadcasts sweep transactions. **Only used by the sweep CLI, never by the running key server.**
102
+ ```ts
103
+ interface ISweepStrategy {
104
+ scan(keys: KeyPair[], treasury: string): Promise<DepositInfo[]>;
105
+ sweep(keys: KeyPair[], treasury: string, dryRun: boolean): Promise<SweepResult[]>;
106
+ }
107
+ ```
108
+
109
+ ### IChainPlugin
110
+ Bundles everything for a chain. The plugin does not hold a single deriver instance — it receives key rings at watcher/sweeper creation time.
111
+ ```ts
112
+ interface IChainPlugin {
113
+ pluginId: string;
114
+ supportedCurve: "secp256k1" | "ed25519";
115
+ encoders: Record<string, IAddressEncoder>;
116
+ createWatcher(opts: WatcherOpts): IChainWatcher;
117
+ createSweeper(opts: SweeperOpts): ISweepStrategy;
118
+ version: number; // interface version for compatibility
119
+ }
120
+
121
+ interface WatcherOpts {
122
+ rpcUrl: string;
123
+ rpcHeaders: Record<string, string>;
124
+ oracle: IPriceOracle;
125
+ cursorStore: IWatcherCursorStore;
126
+ token: string;
127
+ contractAddress?: string;
128
+ decimals: number;
129
+ confirmations: number;
130
+ }
131
+ ```
132
+
133
+ ## DB Schema
134
+
135
+ ### New: `key_rings` table
136
+
137
+ | Column | Type | Description |
138
+ |--------|------|-------------|
139
+ | `id` | text PK | e.g. `"btc-main"`, `"sol-main"` |
140
+ | `curve` | text | `"secp256k1"`, `"ed25519"` |
141
+ | `derivation_scheme` | text | `"bip32"`, `"slip0010"` |
142
+ | `derivation_mode` | text | `"on-demand"` (xpub) or `"pool"` (pre-derived) |
143
+ | `key_material` | text | JSON: `{ xpub: "xpub6..." }` for on-demand, `{}` for pool |
144
+ | `coin_type` | integer | BIP-44 coin type (0, 2, 3, 60, 195, 501, etc.) |
145
+ | `account_index` | integer | BIP-44 account (usually 0) |
146
+ | `created_at` | text | timestamp |
147
+
148
+ **Unique constraint:** `(coin_type, account_index)` — prevents two key rings from claiming the same derivation path. Replaces `path_allocations`.
149
+
150
+ ### New: `address_pool` table (Ed25519 chains)
151
+
152
+ | Column | Type | Description |
153
+ |--------|------|-------------|
154
+ | `id` | serial PK | auto-increment |
155
+ | `key_ring_id` | text FK | → key_rings.id |
156
+ | `derivation_index` | integer | BIP-44 address index |
157
+ | `public_key` | text | hex-encoded public key (commitment proof) |
158
+ | `address` | text | encoded address string |
159
+ | `assigned_to` | text NULL | charge ID (null = available) |
160
+ | `created_at` | text | timestamp |
161
+
162
+ **Unique constraint:** `(key_ring_id, derivation_index)` — no duplicate indices.
163
+
164
+ ### Modified: `payment_methods`
165
+
166
+ - Drop: `xpub`, `address_type`, `watcher_type`
167
+ - Add: `key_ring_id` (FK → key_rings.id)
168
+ - Add: `encoding` (text — encoder plugin ID, e.g. `"bech32"`, `"base58-solana"`)
169
+ - Add: `plugin_id` (text — chain plugin ID, e.g. `"evm"`, `"solana"`)
170
+ - Keep: `encoding_params` (scoped to encoder only)
171
+ - Keep: `rpc_url`, `rpc_headers`, `contract_address`, `decimals`, `confirmations`, etc.
172
+
173
+ ### Removed: `path_allocations`
174
+
175
+ Replaced by `key_rings` unique constraint on `(coin_type, account_index)`.
176
+
177
+ ## Plugin Packages
178
+
179
+ Each chain is its own npm package:
180
+
181
+ | Package | Curve | Chains |
182
+ |---------|-------|--------|
183
+ | `crypto-plugin-evm` | secp256k1 | ETH, Base, Arbitrum, Polygon, Optimism, Avalanche, BSC |
184
+ | `crypto-plugin-utxo-common` | — | Shared UTXO watcher, bitcoind RPC, sweep logic |
185
+ | `crypto-plugin-bitcoin` | secp256k1 | BTC (depends on utxo-common) |
186
+ | `crypto-plugin-litecoin` | secp256k1 | LTC (depends on utxo-common) |
187
+ | `crypto-plugin-dogecoin` | secp256k1 | DOGE (depends on utxo-common) |
188
+ | `crypto-plugin-tron` | secp256k1 | TRX + TRC-20 (handles T-address ↔ hex conversion internally) |
189
+ | `crypto-plugin-solana` | ed25519 | SOL + SPL tokens |
190
+
191
+ Platform-core exports interfaces as a peer dependency:
192
+ ```
193
+ @wopr-network/platform-core/crypto-plugin
194
+ ```
195
+
196
+ ## Plugin Registry
197
+
198
+ Explicit imports in the key-server entry point:
199
+
200
+ ```ts
201
+ import { evmPlugin } from "@wopr-network/crypto-plugin-evm";
202
+ import { bitcoinPlugin } from "@wopr-network/crypto-plugin-bitcoin";
203
+ import { solanaPlugin } from "@wopr-network/crypto-plugin-solana";
204
+
205
+ const registry = new PluginRegistry();
206
+ registry.register(evmPlugin);
207
+ registry.register(bitcoinPlugin);
208
+ registry.register(solanaPlugin);
209
+ ```
210
+
211
+ ## Startup Flow
212
+
213
+ 1. Read `key_rings` from DB → instantiate `ICurveDeriver` per on-demand ring
214
+ 2. Read `payment_methods` from DB → resolve plugin by `plugin_id`
215
+ 3. For each enabled method, call `plugin.createWatcher(opts)` with key ring's deriver (on-demand) or pool addresses (pool mode) + method's RPC config + oracle
216
+ 4. Start poll loops (watcher service is lifecycle manager: start/stop/poll interval)
217
+
218
+ ## Unified Sweep CLI
219
+
220
+ Package: `@wopr-network/crypto-sweep`
221
+
222
+ ### Sweep mode (default)
223
+ 1. Read mnemonic from stdin
224
+ 2. Fetch enabled payment methods from chain server (`GET /chains`)
225
+ 3. Group by key ring → derive private keys per curve
226
+ 4. For each chain, load sweep plugin, call `scan()`
227
+ 5. Print summary (dry run by default)
228
+ 6. If `SWEEP_DRY_RUN=false`, call `sweep()` for each chain
229
+
230
+ ### Replenish mode (Ed25519 pools)
231
+ 1. Read mnemonic from stdin
232
+ 2. Derive N addresses at next available indices
233
+ 3. Upload `(index, publicKey, address)` tuples to key server
234
+ 4. Key server validates and stores in `address_pool`
235
+
236
+ ```bash
237
+ # Sweep all chains
238
+ openssl enc ... -d | npx @wopr-network/crypto-sweep
239
+
240
+ # Replenish Solana address pool
241
+ openssl enc ... -d | npx @wopr-network/crypto-sweep replenish --chain solana --count 100
242
+ ```
243
+
244
+ No per-chain env vars. RPC URLs and headers come from the chain server.
245
+
246
+ ## Adding a New Chain
247
+
248
+ ### secp256k1 chain (e.g. XRP)
249
+ 1. `npm install @wopr-network/crypto-plugin-xrp`
250
+ 2. Add `registry.register(xrpPlugin)` to entry point
251
+ 3. Insert `key_ring` row (curve: secp256k1, derivation_mode: on-demand, coin_type: 144)
252
+ 4. Insert `payment_method` row (plugin_id: "xrp", encoding: "base58-xrp", key_ring_id: "xrp-main")
253
+ 5. Restart
254
+
255
+ ### Ed25519 chain (e.g. Solana)
256
+ 1. `npm install @wopr-network/crypto-plugin-solana`
257
+ 2. Add `registry.register(solanaPlugin)` to entry point
258
+ 3. Insert `key_ring` row (curve: ed25519, derivation_mode: pool, coin_type: 501)
259
+ 4. Insert `payment_method` row (plugin_id: "solana", encoding: "base58-solana", key_ring_id: "sol-main")
260
+ 5. Replenish pool: `openssl enc ... -d | npx @wopr-network/crypto-sweep replenish --chain solana --count 200`
261
+ 6. Restart
262
+
263
+ No code changes to platform-core for either path.
264
+
265
+ ## Client API
266
+
267
+ **Zero changes.** Existing endpoints unchanged:
268
+
269
+ - `POST /address` — `{ chain: "SOL:solana" }` → `{ address, index }`
270
+ - `POST /charges` — `{ chain: "SOL:solana", amountUsd: 5 }` → `{ chargeId, address }`
271
+ - `GET /chains` — new chains appear automatically
272
+ - Webhooks — `{ chargeId, status, txHash }` — unchanged
273
+
274
+ Only change: admin `POST /admin/chains` takes `key_ring_id` + `encoding` + `plugin_id` instead of `address_type` + `watcher_type` + `xpub`.
275
+
276
+ ## Migration Path
277
+
278
+ ### Phase 1 — Interfaces + registry (platform-core)
279
+ - Define all interfaces (`IChainPlugin`, `ICurveDeriver`, `IAddressEncoder`, `IChainWatcher`, `ISweepStrategy`)
280
+ - Create `PluginRegistry`
281
+ - DB migration 1: add `key_rings` table, add `address_pool` table, add new columns to `payment_methods`
282
+ - Backfill: create key_ring rows from existing xpub + address_type data, set `key_ring_id` + `encoding` + `plugin_id` on existing payment_methods
283
+ - DB migration 2: drop old columns (`xpub`, `address_type`, `watcher_type`), drop `path_allocations`
284
+
285
+ ### Phase 2 — Extract existing chains into plugins
286
+ - `crypto-plugin-evm` — from current `evm/watcher.ts`, `evm/eth-watcher.ts`
287
+ - `crypto-plugin-utxo-common` + bitcoin/litecoin/dogecoin — from current UTXO watcher code
288
+ - `crypto-plugin-tron` — from current tron code (address conversion handled internally by plugin)
289
+ - All existing behavior preserved, just restructured
290
+
291
+ ### Phase 3 — Unified sweep CLI
292
+ - `@wopr-network/crypto-sweep` — sweep + replenish modes
293
+ - Replaces `sweep-stablecoins.ts` and `sweep-tron.ts`
294
+
295
+ ### Phase 4 — New chains
296
+ - `crypto-plugin-solana` — first Ed25519 chain, proves pool model
297
+ - Then TON, XRP, etc.
298
+
299
+ Each phase is independently deployable. Phase 1+2 is a refactor with no behavior change. Phase 3 replaces scripts. Phase 4 is new functionality.
300
+
301
+ ## Testing
302
+
303
+ Every chain plugin must include:
304
+ - **Sweep key parity test** — derived address == mnemonic-derived private key address (for on-demand: xpub test; for pool: index + pubkey → address test)
305
+ - **Known test vector** — at least one address verified against external tooling (e.g. TronLink, Phantom, Electrum)
306
+ - **Watcher unit test** — mock RPC responses, verify `PaymentEvent` output matches expected fields
307
+ - **Encoder unit test** — known pubkey → known address
308
+ - **Integration test** — full pipeline: `PluginRegistry → createWatcher → poll → PaymentEvent → handlePayment` with mock RPC
309
+ - **Pool validation test** (Ed25519 only) — uploaded `(index, pubkey, address)` re-validates correctly; tampered tuples are rejected
@@ -0,0 +1 @@
1
+ ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "rpc_headers" text NOT NULL DEFAULT '{}';
@@ -148,6 +148,20 @@
148
148
  "when": 1743350400000,
149
149
  "tag": "0020_product_config_tables",
150
150
  "breakpoints": true
151
+ },
152
+ {
153
+ "idx": 21,
154
+ "version": "7",
155
+ "when": 1743436800000,
156
+ "tag": "0021_watcher_type_column",
157
+ "breakpoints": true
158
+ },
159
+ {
160
+ "idx": 22,
161
+ "version": "7",
162
+ "when": 1743523200000,
163
+ "tag": "0022_rpc_headers_column",
164
+ "breakpoints": true
151
165
  }
152
166
  ]
153
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.63.2",
3
+ "version": "1.64.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",