@wopr-network/platform-core 1.63.1 → 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/__tests__/key-server.test.js +3 -0
- 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-entry.js +8 -1
- package/dist/billing/crypto/key-server.js +19 -14
- package/dist/billing/crypto/oracle/coingecko.js +3 -0
- package/dist/billing/crypto/payment-method-store.d.ts +2 -0
- package/dist/billing/crypto/payment-method-store.js +5 -0
- package/dist/billing/crypto/tron/address-convert.js +15 -5
- package/dist/billing/crypto/watcher-service.js +9 -9
- package/dist/db/schema/crypto.d.ts +34 -0
- package/dist/db/schema/crypto.js +3 -1
- 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_oracle_asset_id_column.sql +23 -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/__tests__/key-server.test.ts +3 -0
- 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-entry.ts +7 -1
- package/src/billing/crypto/key-server.ts +26 -19
- package/src/billing/crypto/oracle/coingecko.ts +3 -0
- package/src/billing/crypto/payment-method-store.ts +7 -0
- package/src/billing/crypto/tron/address-convert.ts +13 -4
- package/src/billing/crypto/watcher-service.ts +12 -11
- package/src/db/schema/crypto.ts +3 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
# Crypto Plugin Architecture — Phase 1: Interfaces + DB + Registry
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Define plugin interfaces, migrate DB schema from hardcoded address_type/watcher_type to key_rings + plugin_id, create plugin registry. Zero behavior change — existing chains continue working.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Platform-core defines `IChainPlugin`, `ICurveDeriver`, `IAddressEncoder`, `IChainWatcher`, `ISweepStrategy` interfaces. New `key_rings` table decouples key material from payment methods. `PluginRegistry` maps plugin IDs to implementations. Existing chain code is NOT extracted yet (Phase 2).
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Drizzle ORM, PostgreSQL, Vitest
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### Task 1: Define Core Interfaces
|
|
16
|
+
|
|
17
|
+
**Files:**
|
|
18
|
+
- Create: `src/billing/crypto/plugin/interfaces.ts`
|
|
19
|
+
- Test: `src/billing/crypto/plugin/__tests__/interfaces.test.ts`
|
|
20
|
+
|
|
21
|
+
- [ ] **Step 1: Write interface type tests**
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/billing/crypto/plugin/__tests__/interfaces.test.ts
|
|
25
|
+
import { describe, expect, it } from "vitest";
|
|
26
|
+
import type {
|
|
27
|
+
EncodingParams,
|
|
28
|
+
IAddressEncoder,
|
|
29
|
+
IChainPlugin,
|
|
30
|
+
IChainWatcher,
|
|
31
|
+
ICurveDeriver,
|
|
32
|
+
ISweepStrategy,
|
|
33
|
+
PaymentEvent,
|
|
34
|
+
WatcherOpts,
|
|
35
|
+
} from "../interfaces.js";
|
|
36
|
+
|
|
37
|
+
describe("plugin interfaces — type contracts", () => {
|
|
38
|
+
it("PaymentEvent has required fields", () => {
|
|
39
|
+
const event: PaymentEvent = {
|
|
40
|
+
chain: "ethereum",
|
|
41
|
+
token: "ETH",
|
|
42
|
+
from: "0xabc",
|
|
43
|
+
to: "0xdef",
|
|
44
|
+
rawAmount: "1000000000000000000",
|
|
45
|
+
amountUsdCents: 350000,
|
|
46
|
+
txHash: "0x123",
|
|
47
|
+
blockNumber: 100,
|
|
48
|
+
confirmations: 6,
|
|
49
|
+
confirmationsRequired: 6,
|
|
50
|
+
};
|
|
51
|
+
expect(event.chain).toBe("ethereum");
|
|
52
|
+
expect(event.amountUsdCents).toBe(350000);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ICurveDeriver contract is satisfiable", () => {
|
|
56
|
+
const deriver: ICurveDeriver = {
|
|
57
|
+
derivePublicKey: (_chain: number, _index: number) => new Uint8Array(33),
|
|
58
|
+
getCurve: () => "secp256k1",
|
|
59
|
+
};
|
|
60
|
+
expect(deriver.getCurve()).toBe("secp256k1");
|
|
61
|
+
expect(deriver.derivePublicKey(0, 0)).toBeInstanceOf(Uint8Array);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("IAddressEncoder contract is satisfiable", () => {
|
|
65
|
+
const encoder: IAddressEncoder = {
|
|
66
|
+
encode: (_pk: Uint8Array, _params: EncodingParams) => "bc1qtest",
|
|
67
|
+
encodingType: () => "bech32",
|
|
68
|
+
};
|
|
69
|
+
expect(encoder.encodingType()).toBe("bech32");
|
|
70
|
+
expect(encoder.encode(new Uint8Array(33), { hrp: "bc" })).toBe("bc1qtest");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("IChainWatcher contract is satisfiable", () => {
|
|
74
|
+
const watcher: IChainWatcher = {
|
|
75
|
+
init: async () => {},
|
|
76
|
+
poll: async () => [],
|
|
77
|
+
setWatchedAddresses: () => {},
|
|
78
|
+
getCursor: () => 0,
|
|
79
|
+
stop: () => {},
|
|
80
|
+
};
|
|
81
|
+
expect(watcher.getCursor()).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
87
|
+
|
|
88
|
+
Run: `npx vitest run src/billing/crypto/plugin/__tests__/interfaces.test.ts`
|
|
89
|
+
Expected: FAIL — module not found
|
|
90
|
+
|
|
91
|
+
- [ ] **Step 3: Write interfaces**
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// src/billing/crypto/plugin/interfaces.ts
|
|
95
|
+
|
|
96
|
+
export interface PaymentEvent {
|
|
97
|
+
chain: string;
|
|
98
|
+
token: string;
|
|
99
|
+
from: string;
|
|
100
|
+
to: string;
|
|
101
|
+
rawAmount: string;
|
|
102
|
+
amountUsdCents: number;
|
|
103
|
+
txHash: string;
|
|
104
|
+
blockNumber: number;
|
|
105
|
+
confirmations: number;
|
|
106
|
+
confirmationsRequired: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ICurveDeriver {
|
|
110
|
+
derivePublicKey(chainIndex: number, addressIndex: number): Uint8Array;
|
|
111
|
+
getCurve(): "secp256k1" | "ed25519";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface EncodingParams {
|
|
115
|
+
hrp?: string;
|
|
116
|
+
version?: string;
|
|
117
|
+
[key: string]: string | undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface IAddressEncoder {
|
|
121
|
+
encode(publicKey: Uint8Array, params: EncodingParams): string;
|
|
122
|
+
encodingType(): string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface KeyPair {
|
|
126
|
+
privateKey: Uint8Array;
|
|
127
|
+
publicKey: Uint8Array;
|
|
128
|
+
address: string;
|
|
129
|
+
index: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface DepositInfo {
|
|
133
|
+
index: number;
|
|
134
|
+
address: string;
|
|
135
|
+
nativeBalance: bigint;
|
|
136
|
+
tokenBalances: Array<{ token: string; balance: bigint; decimals: number }>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface SweepResult {
|
|
140
|
+
index: number;
|
|
141
|
+
address: string;
|
|
142
|
+
token: string;
|
|
143
|
+
amount: string;
|
|
144
|
+
txHash: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ISweepStrategy {
|
|
148
|
+
scan(keys: KeyPair[], treasury: string): Promise<DepositInfo[]>;
|
|
149
|
+
sweep(keys: KeyPair[], treasury: string, dryRun: boolean): Promise<SweepResult[]>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface IPriceOracle {
|
|
153
|
+
getPrice(token: string, feedAddress?: string): Promise<{ priceMicros: number }>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface IWatcherCursorStore {
|
|
157
|
+
get(watcherId: string): Promise<number | null>;
|
|
158
|
+
save(watcherId: string, cursor: number): Promise<void>;
|
|
159
|
+
getConfirmationCount(watcherId: string, txKey: string): Promise<number | null>;
|
|
160
|
+
saveConfirmationCount(watcherId: string, txKey: string, count: number): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface WatcherOpts {
|
|
164
|
+
rpcUrl: string;
|
|
165
|
+
rpcHeaders: Record<string, string>;
|
|
166
|
+
oracle: IPriceOracle;
|
|
167
|
+
cursorStore: IWatcherCursorStore;
|
|
168
|
+
token: string;
|
|
169
|
+
chain: string;
|
|
170
|
+
contractAddress?: string;
|
|
171
|
+
decimals: number;
|
|
172
|
+
confirmations: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface SweeperOpts {
|
|
176
|
+
rpcUrl: string;
|
|
177
|
+
rpcHeaders: Record<string, string>;
|
|
178
|
+
token: string;
|
|
179
|
+
chain: string;
|
|
180
|
+
contractAddress?: string;
|
|
181
|
+
decimals: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface IChainWatcher {
|
|
185
|
+
init(): Promise<void>;
|
|
186
|
+
poll(): Promise<PaymentEvent[]>;
|
|
187
|
+
setWatchedAddresses(addresses: string[]): void;
|
|
188
|
+
getCursor(): number;
|
|
189
|
+
stop(): void;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface IChainPlugin {
|
|
193
|
+
pluginId: string;
|
|
194
|
+
supportedCurve: "secp256k1" | "ed25519";
|
|
195
|
+
encoders: Record<string, IAddressEncoder>;
|
|
196
|
+
createWatcher(opts: WatcherOpts): IChainWatcher;
|
|
197
|
+
createSweeper(opts: SweeperOpts): ISweepStrategy;
|
|
198
|
+
version: number;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
203
|
+
|
|
204
|
+
Run: `npx vitest run src/billing/crypto/plugin/__tests__/interfaces.test.ts`
|
|
205
|
+
Expected: PASS
|
|
206
|
+
|
|
207
|
+
- [ ] **Step 5: Lint and commit**
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npx biome check --write src/billing/crypto/plugin/
|
|
211
|
+
git add src/billing/crypto/plugin/
|
|
212
|
+
git commit -m "feat: define crypto plugin interfaces (IChainPlugin, ICurveDeriver, IAddressEncoder, IChainWatcher, ISweepStrategy)"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Task 2: Create Plugin Registry
|
|
218
|
+
|
|
219
|
+
**Files:**
|
|
220
|
+
- Create: `src/billing/crypto/plugin/registry.ts`
|
|
221
|
+
- Test: `src/billing/crypto/plugin/__tests__/registry.test.ts`
|
|
222
|
+
|
|
223
|
+
- [ ] **Step 1: Write failing test**
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// src/billing/crypto/plugin/__tests__/registry.test.ts
|
|
227
|
+
import { describe, expect, it } from "vitest";
|
|
228
|
+
import { PluginRegistry } from "../registry.js";
|
|
229
|
+
import type { IChainPlugin } from "../interfaces.js";
|
|
230
|
+
|
|
231
|
+
function mockPlugin(id: string, curve: "secp256k1" | "ed25519" = "secp256k1"): IChainPlugin {
|
|
232
|
+
return {
|
|
233
|
+
pluginId: id,
|
|
234
|
+
supportedCurve: curve,
|
|
235
|
+
encoders: {},
|
|
236
|
+
createWatcher: () => ({ init: async () => {}, poll: async () => [], setWatchedAddresses: () => {}, getCursor: () => 0, stop: () => {} }),
|
|
237
|
+
createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
|
|
238
|
+
version: 1,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
describe("PluginRegistry", () => {
|
|
243
|
+
it("registers and retrieves a plugin", () => {
|
|
244
|
+
const reg = new PluginRegistry();
|
|
245
|
+
reg.register(mockPlugin("evm"));
|
|
246
|
+
expect(reg.get("evm")).toBeDefined();
|
|
247
|
+
expect(reg.get("evm")?.pluginId).toBe("evm");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("throws on duplicate registration", () => {
|
|
251
|
+
const reg = new PluginRegistry();
|
|
252
|
+
reg.register(mockPlugin("evm"));
|
|
253
|
+
expect(() => reg.register(mockPlugin("evm"))).toThrow("already registered");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("returns undefined for unknown plugin", () => {
|
|
257
|
+
const reg = new PluginRegistry();
|
|
258
|
+
expect(reg.get("unknown")).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("lists all registered plugins", () => {
|
|
262
|
+
const reg = new PluginRegistry();
|
|
263
|
+
reg.register(mockPlugin("evm"));
|
|
264
|
+
reg.register(mockPlugin("solana", "ed25519"));
|
|
265
|
+
expect(reg.list()).toHaveLength(2);
|
|
266
|
+
expect(reg.list().map((p) => p.pluginId).sort()).toEqual(["evm", "solana"]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("getOrThrow throws for unknown plugin", () => {
|
|
270
|
+
const reg = new PluginRegistry();
|
|
271
|
+
expect(() => reg.getOrThrow("nope")).toThrow("not registered");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
277
|
+
|
|
278
|
+
Run: `npx vitest run src/billing/crypto/plugin/__tests__/registry.test.ts`
|
|
279
|
+
Expected: FAIL — module not found
|
|
280
|
+
|
|
281
|
+
- [ ] **Step 3: Write implementation**
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
// src/billing/crypto/plugin/registry.ts
|
|
285
|
+
import type { IChainPlugin } from "./interfaces.js";
|
|
286
|
+
|
|
287
|
+
export class PluginRegistry {
|
|
288
|
+
private plugins = new Map<string, IChainPlugin>();
|
|
289
|
+
|
|
290
|
+
register(plugin: IChainPlugin): void {
|
|
291
|
+
if (this.plugins.has(plugin.pluginId)) {
|
|
292
|
+
throw new Error(`Plugin "${plugin.pluginId}" is already registered`);
|
|
293
|
+
}
|
|
294
|
+
this.plugins.set(plugin.pluginId, plugin);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
get(pluginId: string): IChainPlugin | undefined {
|
|
298
|
+
return this.plugins.get(pluginId);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getOrThrow(pluginId: string): IChainPlugin {
|
|
302
|
+
const plugin = this.plugins.get(pluginId);
|
|
303
|
+
if (!plugin) throw new Error(`Plugin "${pluginId}" is not registered`);
|
|
304
|
+
return plugin;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
list(): IChainPlugin[] {
|
|
308
|
+
return [...this.plugins.values()];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
314
|
+
|
|
315
|
+
Run: `npx vitest run src/billing/crypto/plugin/__tests__/registry.test.ts`
|
|
316
|
+
Expected: PASS
|
|
317
|
+
|
|
318
|
+
- [ ] **Step 5: Lint and commit**
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
npx biome check --write src/billing/crypto/plugin/
|
|
322
|
+
git add src/billing/crypto/plugin/
|
|
323
|
+
git commit -m "feat: add PluginRegistry for chain plugin management"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### Task 3: DB Migration — Add key_rings + address_pool tables
|
|
329
|
+
|
|
330
|
+
**Files:**
|
|
331
|
+
- Create: `drizzle/migrations/0023_key_rings_table.sql`
|
|
332
|
+
- Modify: `src/db/schema/crypto.ts`
|
|
333
|
+
|
|
334
|
+
- [ ] **Step 1: Write migration SQL**
|
|
335
|
+
|
|
336
|
+
```sql
|
|
337
|
+
-- drizzle/migrations/0023_key_rings_table.sql
|
|
338
|
+
|
|
339
|
+
-- Key rings: decouples key material from payment methods
|
|
340
|
+
CREATE TABLE IF NOT EXISTS "key_rings" (
|
|
341
|
+
"id" text PRIMARY KEY,
|
|
342
|
+
"curve" text NOT NULL,
|
|
343
|
+
"derivation_scheme" text NOT NULL,
|
|
344
|
+
"derivation_mode" text NOT NULL DEFAULT 'on-demand',
|
|
345
|
+
"key_material" text NOT NULL DEFAULT '{}',
|
|
346
|
+
"coin_type" integer NOT NULL,
|
|
347
|
+
"account_index" integer NOT NULL DEFAULT 0,
|
|
348
|
+
"created_at" text NOT NULL DEFAULT (now())
|
|
349
|
+
);
|
|
350
|
+
--> statement-breakpoint
|
|
351
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "key_rings_path_unique" ON "key_rings" ("coin_type", "account_index");
|
|
352
|
+
--> statement-breakpoint
|
|
353
|
+
|
|
354
|
+
-- Pre-derived address pool (for Ed25519 chains)
|
|
355
|
+
CREATE TABLE IF NOT EXISTS "address_pool" (
|
|
356
|
+
"id" serial PRIMARY KEY,
|
|
357
|
+
"key_ring_id" text NOT NULL REFERENCES "key_rings"("id"),
|
|
358
|
+
"derivation_index" integer NOT NULL,
|
|
359
|
+
"public_key" text NOT NULL,
|
|
360
|
+
"address" text NOT NULL,
|
|
361
|
+
"assigned_to" text,
|
|
362
|
+
"created_at" text NOT NULL DEFAULT (now())
|
|
363
|
+
);
|
|
364
|
+
--> statement-breakpoint
|
|
365
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "address_pool_ring_index" ON "address_pool" ("key_ring_id", "derivation_index");
|
|
366
|
+
--> statement-breakpoint
|
|
367
|
+
|
|
368
|
+
-- Add new columns to payment_methods
|
|
369
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "key_ring_id" text REFERENCES "key_rings"("id");
|
|
370
|
+
--> statement-breakpoint
|
|
371
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "encoding" text;
|
|
372
|
+
--> statement-breakpoint
|
|
373
|
+
ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "plugin_id" text;
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
- [ ] **Step 2: Update Drizzle schema**
|
|
377
|
+
|
|
378
|
+
Add `keyRings` and `addressPool` table definitions to `src/db/schema/crypto.ts`.
|
|
379
|
+
Add `keyRingId`, `encoding`, `pluginId` columns to `paymentMethods`.
|
|
380
|
+
|
|
381
|
+
- [ ] **Step 3: Run migration locally to verify**
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
# Start the key server locally or run against test DB
|
|
385
|
+
npx vitest run src/billing/crypto/__tests__/address-gen.test.ts
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- [ ] **Step 4: Lint and commit**
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
npx biome check --write src/db/schema/crypto.ts
|
|
392
|
+
git add drizzle/migrations/0023_key_rings_table.sql src/db/schema/crypto.ts
|
|
393
|
+
git commit -m "feat: add key_rings + address_pool tables, new columns on payment_methods"
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
### Task 4: Backfill Migration — Populate key_rings from existing data
|
|
399
|
+
|
|
400
|
+
**Files:**
|
|
401
|
+
- Create: `drizzle/migrations/0024_backfill_key_rings.sql`
|
|
402
|
+
|
|
403
|
+
- [ ] **Step 1: Write backfill migration**
|
|
404
|
+
|
|
405
|
+
```sql
|
|
406
|
+
-- drizzle/migrations/0024_backfill_key_rings.sql
|
|
407
|
+
|
|
408
|
+
-- Create key rings from existing payment method xpubs
|
|
409
|
+
-- Each unique (coin_type via path_allocations) gets a key ring
|
|
410
|
+
|
|
411
|
+
-- EVM chains (coin type 60)
|
|
412
|
+
INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
|
|
413
|
+
SELECT DISTINCT 'evm-main', 'secp256k1', 'bip32', 'on-demand',
|
|
414
|
+
json_build_object('xpub', pm.xpub)::text,
|
|
415
|
+
pa.coin_type, pa.account_index
|
|
416
|
+
FROM path_allocations pa
|
|
417
|
+
JOIN payment_methods pm ON pm.id = pa.chain_id
|
|
418
|
+
WHERE pa.coin_type = 60
|
|
419
|
+
LIMIT 1
|
|
420
|
+
ON CONFLICT DO NOTHING;
|
|
421
|
+
--> statement-breakpoint
|
|
422
|
+
|
|
423
|
+
-- BTC (coin type 0)
|
|
424
|
+
INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
|
|
425
|
+
SELECT DISTINCT 'btc-main', 'secp256k1', 'bip32', 'on-demand',
|
|
426
|
+
json_build_object('xpub', pm.xpub)::text,
|
|
427
|
+
pa.coin_type, pa.account_index
|
|
428
|
+
FROM path_allocations pa
|
|
429
|
+
JOIN payment_methods pm ON pm.id = pa.chain_id
|
|
430
|
+
WHERE pa.coin_type = 0
|
|
431
|
+
LIMIT 1
|
|
432
|
+
ON CONFLICT DO NOTHING;
|
|
433
|
+
--> statement-breakpoint
|
|
434
|
+
|
|
435
|
+
-- LTC (coin type 2)
|
|
436
|
+
INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
|
|
437
|
+
SELECT DISTINCT 'ltc-main', 'secp256k1', 'bip32', 'on-demand',
|
|
438
|
+
json_build_object('xpub', pm.xpub)::text,
|
|
439
|
+
pa.coin_type, pa.account_index
|
|
440
|
+
FROM path_allocations pa
|
|
441
|
+
JOIN payment_methods pm ON pm.id = pa.chain_id
|
|
442
|
+
WHERE pa.coin_type = 2
|
|
443
|
+
LIMIT 1
|
|
444
|
+
ON CONFLICT DO NOTHING;
|
|
445
|
+
--> statement-breakpoint
|
|
446
|
+
|
|
447
|
+
-- DOGE (coin type 3)
|
|
448
|
+
INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
|
|
449
|
+
SELECT DISTINCT 'doge-main', 'secp256k1', 'bip32', 'on-demand',
|
|
450
|
+
json_build_object('xpub', pm.xpub)::text,
|
|
451
|
+
pa.coin_type, pa.account_index
|
|
452
|
+
FROM path_allocations pa
|
|
453
|
+
JOIN payment_methods pm ON pm.id = pa.chain_id
|
|
454
|
+
WHERE pa.coin_type = 3
|
|
455
|
+
LIMIT 1
|
|
456
|
+
ON CONFLICT DO NOTHING;
|
|
457
|
+
--> statement-breakpoint
|
|
458
|
+
|
|
459
|
+
-- TRON (coin type 195)
|
|
460
|
+
INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
|
|
461
|
+
SELECT DISTINCT 'tron-main', 'secp256k1', 'bip32', 'on-demand',
|
|
462
|
+
json_build_object('xpub', pm.xpub)::text,
|
|
463
|
+
pa.coin_type, pa.account_index
|
|
464
|
+
FROM path_allocations pa
|
|
465
|
+
JOIN payment_methods pm ON pm.id = pa.chain_id
|
|
466
|
+
WHERE pa.coin_type = 195
|
|
467
|
+
LIMIT 1
|
|
468
|
+
ON CONFLICT DO NOTHING;
|
|
469
|
+
--> statement-breakpoint
|
|
470
|
+
|
|
471
|
+
-- Backfill payment_methods with key_ring_id, encoding, plugin_id
|
|
472
|
+
UPDATE payment_methods SET
|
|
473
|
+
key_ring_id = CASE
|
|
474
|
+
WHEN chain IN ('arbitrum','avalanche','base','base-sepolia','bsc','optimism','polygon','sepolia') THEN 'evm-main'
|
|
475
|
+
WHEN chain = 'bitcoin' THEN 'btc-main'
|
|
476
|
+
WHEN chain = 'litecoin' THEN 'ltc-main'
|
|
477
|
+
WHEN chain = 'dogecoin' THEN 'doge-main'
|
|
478
|
+
WHEN chain = 'tron' THEN 'tron-main'
|
|
479
|
+
END,
|
|
480
|
+
encoding = address_type,
|
|
481
|
+
plugin_id = watcher_type
|
|
482
|
+
WHERE key_ring_id IS NULL;
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
- [ ] **Step 2: Commit**
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
git add drizzle/migrations/0024_backfill_key_rings.sql
|
|
489
|
+
git commit -m "feat: backfill key_rings from existing path_allocations + payment_methods"
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
### Task 5: Update PaymentMethodStore to include new fields
|
|
495
|
+
|
|
496
|
+
**Files:**
|
|
497
|
+
- Modify: `src/billing/crypto/payment-method-store.ts`
|
|
498
|
+
- Test: existing tests should still pass
|
|
499
|
+
|
|
500
|
+
- [ ] **Step 1: Add new fields to PaymentMethodRecord**
|
|
501
|
+
|
|
502
|
+
Add `keyRingId`, `encoding`, `pluginId` to the `PaymentMethodRecord` type and all mapping functions in `payment-method-store.ts`.
|
|
503
|
+
|
|
504
|
+
- [ ] **Step 2: Run existing tests**
|
|
505
|
+
|
|
506
|
+
Run: `npx vitest run`
|
|
507
|
+
Expected: All existing tests still pass (new fields are nullable during transition)
|
|
508
|
+
|
|
509
|
+
- [ ] **Step 3: Lint and commit**
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
npx biome check --write src/billing/crypto/payment-method-store.ts
|
|
513
|
+
git add src/billing/crypto/payment-method-store.ts
|
|
514
|
+
git commit -m "feat: add keyRingId, encoding, pluginId to PaymentMethodRecord"
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
### Task 6: Export plugin interfaces from platform-core
|
|
520
|
+
|
|
521
|
+
**Files:**
|
|
522
|
+
- Create: `src/billing/crypto/plugin/index.ts`
|
|
523
|
+
- Modify: `src/billing/crypto/index.ts`
|
|
524
|
+
- Modify: `package.json` (add subpath export)
|
|
525
|
+
|
|
526
|
+
- [ ] **Step 1: Create plugin barrel export**
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
// src/billing/crypto/plugin/index.ts
|
|
530
|
+
export type {
|
|
531
|
+
DepositInfo,
|
|
532
|
+
EncodingParams,
|
|
533
|
+
IAddressEncoder,
|
|
534
|
+
IChainPlugin,
|
|
535
|
+
IChainWatcher,
|
|
536
|
+
ICurveDeriver,
|
|
537
|
+
IPriceOracle,
|
|
538
|
+
ISweepStrategy,
|
|
539
|
+
IWatcherCursorStore,
|
|
540
|
+
KeyPair,
|
|
541
|
+
PaymentEvent,
|
|
542
|
+
SweepResult,
|
|
543
|
+
SweeperOpts,
|
|
544
|
+
WatcherOpts,
|
|
545
|
+
} from "./interfaces.js";
|
|
546
|
+
export { PluginRegistry } from "./registry.js";
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
- [ ] **Step 2: Add subpath export to package.json**
|
|
550
|
+
|
|
551
|
+
Add to `exports` field:
|
|
552
|
+
```json
|
|
553
|
+
"./crypto-plugin": {
|
|
554
|
+
"import": "./dist/billing/crypto/plugin/index.js",
|
|
555
|
+
"types": "./dist/billing/crypto/plugin/index.d.ts"
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
- [ ] **Step 3: Re-export from main crypto index**
|
|
560
|
+
|
|
561
|
+
Add to `src/billing/crypto/index.ts`:
|
|
562
|
+
```ts
|
|
563
|
+
export { PluginRegistry } from "./plugin/index.js";
|
|
564
|
+
export type { IChainPlugin, ICurveDeriver, IAddressEncoder, IChainWatcher, ISweepStrategy, PaymentEvent } from "./plugin/index.js";
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
- [ ] **Step 4: Build to verify exports work**
|
|
568
|
+
|
|
569
|
+
Run: `pnpm build`
|
|
570
|
+
Expected: Clean build, `dist/billing/crypto/plugin/` exists
|
|
571
|
+
|
|
572
|
+
- [ ] **Step 5: Lint and commit**
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
npx biome check --write src/billing/crypto/plugin/ src/billing/crypto/index.ts
|
|
576
|
+
git add src/billing/crypto/plugin/index.ts src/billing/crypto/index.ts package.json
|
|
577
|
+
git commit -m "feat: export plugin interfaces from platform-core/crypto-plugin"
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
### Task 7: Integration test — registry + interfaces end-to-end
|
|
583
|
+
|
|
584
|
+
**Files:**
|
|
585
|
+
- Create: `src/billing/crypto/plugin/__tests__/integration.test.ts`
|
|
586
|
+
|
|
587
|
+
- [ ] **Step 1: Write integration test**
|
|
588
|
+
|
|
589
|
+
Test that a mock plugin can be registered, watcher created, and poll returns events:
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
import { describe, expect, it } from "vitest";
|
|
593
|
+
import { PluginRegistry } from "../registry.js";
|
|
594
|
+
import type { IChainPlugin, PaymentEvent, WatcherOpts } from "../interfaces.js";
|
|
595
|
+
|
|
596
|
+
describe("plugin integration — registry → watcher → events", () => {
|
|
597
|
+
it("full lifecycle: register → create watcher → poll → events", async () => {
|
|
598
|
+
const mockEvent: PaymentEvent = {
|
|
599
|
+
chain: "test",
|
|
600
|
+
token: "TEST",
|
|
601
|
+
from: "0xsender",
|
|
602
|
+
to: "0xreceiver",
|
|
603
|
+
rawAmount: "1000",
|
|
604
|
+
amountUsdCents: 100,
|
|
605
|
+
txHash: "0xhash",
|
|
606
|
+
blockNumber: 42,
|
|
607
|
+
confirmations: 6,
|
|
608
|
+
confirmationsRequired: 6,
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const plugin: IChainPlugin = {
|
|
612
|
+
pluginId: "test",
|
|
613
|
+
supportedCurve: "secp256k1",
|
|
614
|
+
encoders: {},
|
|
615
|
+
createWatcher: (_opts: WatcherOpts) => ({
|
|
616
|
+
init: async () => {},
|
|
617
|
+
poll: async () => [mockEvent],
|
|
618
|
+
setWatchedAddresses: () => {},
|
|
619
|
+
getCursor: () => 42,
|
|
620
|
+
stop: () => {},
|
|
621
|
+
}),
|
|
622
|
+
createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
|
|
623
|
+
version: 1,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const registry = new PluginRegistry();
|
|
627
|
+
registry.register(plugin);
|
|
628
|
+
|
|
629
|
+
const resolved = registry.getOrThrow("test");
|
|
630
|
+
const watcher = resolved.createWatcher({
|
|
631
|
+
rpcUrl: "http://localhost:8545",
|
|
632
|
+
rpcHeaders: {},
|
|
633
|
+
oracle: { getPrice: async () => ({ priceMicros: 3500_000000 }) },
|
|
634
|
+
cursorStore: { get: async () => null, save: async () => {}, getConfirmationCount: async () => null, saveConfirmationCount: async () => {} },
|
|
635
|
+
token: "TEST",
|
|
636
|
+
chain: "test",
|
|
637
|
+
decimals: 18,
|
|
638
|
+
confirmations: 6,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
await watcher.init();
|
|
642
|
+
const events = await watcher.poll();
|
|
643
|
+
expect(events).toHaveLength(1);
|
|
644
|
+
expect(events[0].txHash).toBe("0xhash");
|
|
645
|
+
expect(watcher.getCursor()).toBe(42);
|
|
646
|
+
watcher.stop();
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
- [ ] **Step 2: Run test**
|
|
652
|
+
|
|
653
|
+
Run: `npx vitest run src/billing/crypto/plugin/__tests__/integration.test.ts`
|
|
654
|
+
Expected: PASS
|
|
655
|
+
|
|
656
|
+
- [ ] **Step 3: Lint and commit**
|
|
657
|
+
|
|
658
|
+
```bash
|
|
659
|
+
npx biome check --write src/billing/crypto/plugin/__tests__/
|
|
660
|
+
git add src/billing/crypto/plugin/__tests__/integration.test.ts
|
|
661
|
+
git commit -m "test: plugin registry integration test — full lifecycle"
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
### Task 8: Final verification
|
|
667
|
+
|
|
668
|
+
- [ ] **Step 1: Run full test suite**
|
|
669
|
+
|
|
670
|
+
```bash
|
|
671
|
+
npx vitest run
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Expected: All tests pass (existing + new plugin tests)
|
|
675
|
+
|
|
676
|
+
- [ ] **Step 2: Build**
|
|
677
|
+
|
|
678
|
+
```bash
|
|
679
|
+
pnpm build
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Expected: Clean build
|
|
683
|
+
|
|
684
|
+
- [ ] **Step 3: Lint**
|
|
685
|
+
|
|
686
|
+
```bash
|
|
687
|
+
npx biome check src/
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Expected: No errors
|
|
691
|
+
|
|
692
|
+
- [ ] **Step 4: Create PR**
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
git push origin feat/crypto-plugin-phase1
|
|
696
|
+
gh pr create --title "feat: crypto plugin architecture — Phase 1 (interfaces + DB + registry)" --body "..."
|
|
697
|
+
```
|