@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.
- package/dist/billing/crypto/__tests__/address-gen.test.js +191 -90
- package/dist/billing/crypto/address-gen.js +32 -0
- package/dist/billing/crypto/evm/eth-watcher.js +52 -41
- package/dist/billing/crypto/evm/watcher.js +5 -11
- package/dist/billing/crypto/key-server.js +1 -0
- package/dist/billing/crypto/payment-method-store.d.ts +1 -0
- package/dist/billing/crypto/payment-method-store.js +2 -0
- package/dist/billing/crypto/watcher-service.js +4 -4
- package/dist/db/schema/crypto.d.ts +17 -0
- package/dist/db/schema/crypto.js +1 -0
- package/docs/superpowers/plans/2026-03-24-crypto-plugin-phase1.md +697 -0
- package/docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md +309 -0
- package/drizzle/migrations/0022_rpc_headers_column.sql +1 -0
- package/drizzle/migrations/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/billing/crypto/__tests__/address-gen.test.ts +207 -88
- package/src/billing/crypto/address-gen.ts +31 -0
- package/src/billing/crypto/evm/eth-watcher.ts +64 -47
- package/src/billing/crypto/evm/watcher.ts +8 -9
- package/src/billing/crypto/key-server.ts +2 -0
- package/src/billing/crypto/payment-method-store.ts +3 -0
- package/src/billing/crypto/watcher-service.ts +4 -4
- package/src/db/schema/crypto.ts +1 -0
|
@@ -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
|
}
|