@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,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
+ ```