@wopr-network/platform-core 1.64.0 → 1.65.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 (33) 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.js +3 -0
  4. package/dist/billing/crypto/payment-method-store.d.ts +3 -0
  5. package/dist/billing/crypto/payment-method-store.js +9 -0
  6. package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +1 -0
  7. package/dist/billing/crypto/plugin/__tests__/integration.test.js +58 -0
  8. package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +1 -0
  9. package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +46 -0
  10. package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +1 -0
  11. package/dist/billing/crypto/plugin/__tests__/registry.test.js +49 -0
  12. package/dist/billing/crypto/plugin/index.d.ts +2 -0
  13. package/dist/billing/crypto/plugin/index.js +1 -0
  14. package/dist/billing/crypto/plugin/interfaces.d.ts +97 -0
  15. package/dist/billing/crypto/plugin/interfaces.js +2 -0
  16. package/dist/billing/crypto/plugin/registry.d.ts +8 -0
  17. package/dist/billing/crypto/plugin/registry.js +21 -0
  18. package/dist/db/schema/crypto.d.ts +328 -0
  19. package/dist/db/schema/crypto.js +33 -1
  20. package/dist/db/schema/snapshots.d.ts +1 -1
  21. package/drizzle/migrations/0023_key_rings_table.sql +35 -0
  22. package/drizzle/migrations/0024_backfill_key_rings.sql +75 -0
  23. package/package.json +5 -1
  24. package/src/billing/crypto/index.ts +9 -0
  25. package/src/billing/crypto/key-server.ts +3 -0
  26. package/src/billing/crypto/payment-method-store.ts +12 -0
  27. package/src/billing/crypto/plugin/__tests__/integration.test.ts +64 -0
  28. package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +51 -0
  29. package/src/billing/crypto/plugin/__tests__/registry.test.ts +58 -0
  30. package/src/billing/crypto/plugin/index.ts +17 -0
  31. package/src/billing/crypto/plugin/interfaces.ts +106 -0
  32. package/src/billing/crypto/plugin/registry.ts +26 -0
  33. package/src/db/schema/crypto.ts +43 -1
@@ -784,6 +784,57 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
784
784
  identity: undefined;
785
785
  generated: undefined;
786
786
  }, {}, {}>;
787
+ keyRingId: import("drizzle-orm/pg-core").PgColumn<{
788
+ name: "key_ring_id";
789
+ tableName: "payment_methods";
790
+ dataType: "string";
791
+ columnType: "PgText";
792
+ data: string;
793
+ driverParam: string;
794
+ notNull: false;
795
+ hasDefault: false;
796
+ isPrimaryKey: false;
797
+ isAutoincrement: false;
798
+ hasRuntimeDefault: false;
799
+ enumValues: [string, ...string[]];
800
+ baseColumn: never;
801
+ identity: undefined;
802
+ generated: undefined;
803
+ }, {}, {}>;
804
+ encoding: import("drizzle-orm/pg-core").PgColumn<{
805
+ name: "encoding";
806
+ tableName: "payment_methods";
807
+ dataType: "string";
808
+ columnType: "PgText";
809
+ data: string;
810
+ driverParam: string;
811
+ notNull: false;
812
+ hasDefault: false;
813
+ isPrimaryKey: false;
814
+ isAutoincrement: false;
815
+ hasRuntimeDefault: false;
816
+ enumValues: [string, ...string[]];
817
+ baseColumn: never;
818
+ identity: undefined;
819
+ generated: undefined;
820
+ }, {}, {}>;
821
+ pluginId: import("drizzle-orm/pg-core").PgColumn<{
822
+ name: "plugin_id";
823
+ tableName: "payment_methods";
824
+ dataType: "string";
825
+ columnType: "PgText";
826
+ data: string;
827
+ driverParam: string;
828
+ notNull: false;
829
+ hasDefault: false;
830
+ isPrimaryKey: false;
831
+ isAutoincrement: false;
832
+ hasRuntimeDefault: false;
833
+ enumValues: [string, ...string[]];
834
+ baseColumn: never;
835
+ identity: undefined;
836
+ generated: undefined;
837
+ }, {}, {}>;
787
838
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
788
839
  name: "created_at";
789
840
  tableName: "payment_methods";
@@ -1237,3 +1288,280 @@ export declare const watcherProcessed: import("drizzle-orm/pg-core").PgTableWith
1237
1288
  };
1238
1289
  dialect: "pg";
1239
1290
  }>;
1291
+ /**
1292
+ * Key rings — decouples key material (xpub/seed) from payment methods.
1293
+ * Each key ring maps to a BIP-44 coin type + account index.
1294
+ */
1295
+ export declare const keyRings: import("drizzle-orm/pg-core").PgTableWithColumns<{
1296
+ name: "key_rings";
1297
+ schema: undefined;
1298
+ columns: {
1299
+ id: import("drizzle-orm/pg-core").PgColumn<{
1300
+ name: "id";
1301
+ tableName: "key_rings";
1302
+ dataType: "string";
1303
+ columnType: "PgText";
1304
+ data: string;
1305
+ driverParam: string;
1306
+ notNull: true;
1307
+ hasDefault: false;
1308
+ isPrimaryKey: true;
1309
+ isAutoincrement: false;
1310
+ hasRuntimeDefault: false;
1311
+ enumValues: [string, ...string[]];
1312
+ baseColumn: never;
1313
+ identity: undefined;
1314
+ generated: undefined;
1315
+ }, {}, {}>;
1316
+ curve: import("drizzle-orm/pg-core").PgColumn<{
1317
+ name: "curve";
1318
+ tableName: "key_rings";
1319
+ dataType: "string";
1320
+ columnType: "PgText";
1321
+ data: string;
1322
+ driverParam: string;
1323
+ notNull: true;
1324
+ hasDefault: false;
1325
+ isPrimaryKey: false;
1326
+ isAutoincrement: false;
1327
+ hasRuntimeDefault: false;
1328
+ enumValues: [string, ...string[]];
1329
+ baseColumn: never;
1330
+ identity: undefined;
1331
+ generated: undefined;
1332
+ }, {}, {}>;
1333
+ derivationScheme: import("drizzle-orm/pg-core").PgColumn<{
1334
+ name: "derivation_scheme";
1335
+ tableName: "key_rings";
1336
+ dataType: "string";
1337
+ columnType: "PgText";
1338
+ data: string;
1339
+ driverParam: string;
1340
+ notNull: true;
1341
+ hasDefault: false;
1342
+ isPrimaryKey: false;
1343
+ isAutoincrement: false;
1344
+ hasRuntimeDefault: false;
1345
+ enumValues: [string, ...string[]];
1346
+ baseColumn: never;
1347
+ identity: undefined;
1348
+ generated: undefined;
1349
+ }, {}, {}>;
1350
+ derivationMode: import("drizzle-orm/pg-core").PgColumn<{
1351
+ name: "derivation_mode";
1352
+ tableName: "key_rings";
1353
+ dataType: "string";
1354
+ columnType: "PgText";
1355
+ data: string;
1356
+ driverParam: string;
1357
+ notNull: true;
1358
+ hasDefault: true;
1359
+ isPrimaryKey: false;
1360
+ isAutoincrement: false;
1361
+ hasRuntimeDefault: false;
1362
+ enumValues: [string, ...string[]];
1363
+ baseColumn: never;
1364
+ identity: undefined;
1365
+ generated: undefined;
1366
+ }, {}, {}>;
1367
+ keyMaterial: import("drizzle-orm/pg-core").PgColumn<{
1368
+ name: "key_material";
1369
+ tableName: "key_rings";
1370
+ dataType: "string";
1371
+ columnType: "PgText";
1372
+ data: string;
1373
+ driverParam: string;
1374
+ notNull: true;
1375
+ hasDefault: true;
1376
+ isPrimaryKey: false;
1377
+ isAutoincrement: false;
1378
+ hasRuntimeDefault: false;
1379
+ enumValues: [string, ...string[]];
1380
+ baseColumn: never;
1381
+ identity: undefined;
1382
+ generated: undefined;
1383
+ }, {}, {}>;
1384
+ coinType: import("drizzle-orm/pg-core").PgColumn<{
1385
+ name: "coin_type";
1386
+ tableName: "key_rings";
1387
+ dataType: "number";
1388
+ columnType: "PgInteger";
1389
+ data: number;
1390
+ driverParam: string | number;
1391
+ notNull: true;
1392
+ hasDefault: false;
1393
+ isPrimaryKey: false;
1394
+ isAutoincrement: false;
1395
+ hasRuntimeDefault: false;
1396
+ enumValues: undefined;
1397
+ baseColumn: never;
1398
+ identity: undefined;
1399
+ generated: undefined;
1400
+ }, {}, {}>;
1401
+ accountIndex: import("drizzle-orm/pg-core").PgColumn<{
1402
+ name: "account_index";
1403
+ tableName: "key_rings";
1404
+ dataType: "number";
1405
+ columnType: "PgInteger";
1406
+ data: number;
1407
+ driverParam: string | number;
1408
+ notNull: true;
1409
+ hasDefault: true;
1410
+ isPrimaryKey: false;
1411
+ isAutoincrement: false;
1412
+ hasRuntimeDefault: false;
1413
+ enumValues: undefined;
1414
+ baseColumn: never;
1415
+ identity: undefined;
1416
+ generated: undefined;
1417
+ }, {}, {}>;
1418
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
1419
+ name: "created_at";
1420
+ tableName: "key_rings";
1421
+ dataType: "string";
1422
+ columnType: "PgText";
1423
+ data: string;
1424
+ driverParam: string;
1425
+ notNull: true;
1426
+ hasDefault: true;
1427
+ isPrimaryKey: false;
1428
+ isAutoincrement: false;
1429
+ hasRuntimeDefault: false;
1430
+ enumValues: [string, ...string[]];
1431
+ baseColumn: never;
1432
+ identity: undefined;
1433
+ generated: undefined;
1434
+ }, {}, {}>;
1435
+ };
1436
+ dialect: "pg";
1437
+ }>;
1438
+ /**
1439
+ * Pre-derived address pool — for Ed25519 chains that need offline derivation.
1440
+ * Addresses are derived in batches and assigned on demand.
1441
+ */
1442
+ export declare const addressPool: import("drizzle-orm/pg-core").PgTableWithColumns<{
1443
+ name: "address_pool";
1444
+ schema: undefined;
1445
+ columns: {
1446
+ id: import("drizzle-orm/pg-core").PgColumn<{
1447
+ name: "id";
1448
+ tableName: "address_pool";
1449
+ dataType: "number";
1450
+ columnType: "PgSerial";
1451
+ data: number;
1452
+ driverParam: number;
1453
+ notNull: true;
1454
+ hasDefault: true;
1455
+ isPrimaryKey: true;
1456
+ isAutoincrement: false;
1457
+ hasRuntimeDefault: false;
1458
+ enumValues: undefined;
1459
+ baseColumn: never;
1460
+ identity: undefined;
1461
+ generated: undefined;
1462
+ }, {}, {}>;
1463
+ keyRingId: import("drizzle-orm/pg-core").PgColumn<{
1464
+ name: "key_ring_id";
1465
+ tableName: "address_pool";
1466
+ dataType: "string";
1467
+ columnType: "PgText";
1468
+ data: string;
1469
+ driverParam: string;
1470
+ notNull: true;
1471
+ hasDefault: false;
1472
+ isPrimaryKey: false;
1473
+ isAutoincrement: false;
1474
+ hasRuntimeDefault: false;
1475
+ enumValues: [string, ...string[]];
1476
+ baseColumn: never;
1477
+ identity: undefined;
1478
+ generated: undefined;
1479
+ }, {}, {}>;
1480
+ derivationIndex: import("drizzle-orm/pg-core").PgColumn<{
1481
+ name: "derivation_index";
1482
+ tableName: "address_pool";
1483
+ dataType: "number";
1484
+ columnType: "PgInteger";
1485
+ data: number;
1486
+ driverParam: string | number;
1487
+ notNull: true;
1488
+ hasDefault: false;
1489
+ isPrimaryKey: false;
1490
+ isAutoincrement: false;
1491
+ hasRuntimeDefault: false;
1492
+ enumValues: undefined;
1493
+ baseColumn: never;
1494
+ identity: undefined;
1495
+ generated: undefined;
1496
+ }, {}, {}>;
1497
+ publicKey: import("drizzle-orm/pg-core").PgColumn<{
1498
+ name: "public_key";
1499
+ tableName: "address_pool";
1500
+ dataType: "string";
1501
+ columnType: "PgText";
1502
+ data: string;
1503
+ driverParam: string;
1504
+ notNull: true;
1505
+ hasDefault: false;
1506
+ isPrimaryKey: false;
1507
+ isAutoincrement: false;
1508
+ hasRuntimeDefault: false;
1509
+ enumValues: [string, ...string[]];
1510
+ baseColumn: never;
1511
+ identity: undefined;
1512
+ generated: undefined;
1513
+ }, {}, {}>;
1514
+ address: import("drizzle-orm/pg-core").PgColumn<{
1515
+ name: "address";
1516
+ tableName: "address_pool";
1517
+ dataType: "string";
1518
+ columnType: "PgText";
1519
+ data: string;
1520
+ driverParam: string;
1521
+ notNull: true;
1522
+ hasDefault: false;
1523
+ isPrimaryKey: false;
1524
+ isAutoincrement: false;
1525
+ hasRuntimeDefault: false;
1526
+ enumValues: [string, ...string[]];
1527
+ baseColumn: never;
1528
+ identity: undefined;
1529
+ generated: undefined;
1530
+ }, {}, {}>;
1531
+ assignedTo: import("drizzle-orm/pg-core").PgColumn<{
1532
+ name: "assigned_to";
1533
+ tableName: "address_pool";
1534
+ dataType: "string";
1535
+ columnType: "PgText";
1536
+ data: string;
1537
+ driverParam: string;
1538
+ notNull: false;
1539
+ hasDefault: false;
1540
+ isPrimaryKey: false;
1541
+ isAutoincrement: false;
1542
+ hasRuntimeDefault: false;
1543
+ enumValues: [string, ...string[]];
1544
+ baseColumn: never;
1545
+ identity: undefined;
1546
+ generated: undefined;
1547
+ }, {}, {}>;
1548
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
1549
+ name: "created_at";
1550
+ tableName: "address_pool";
1551
+ dataType: "string";
1552
+ columnType: "PgText";
1553
+ data: string;
1554
+ driverParam: string;
1555
+ notNull: true;
1556
+ hasDefault: true;
1557
+ isPrimaryKey: false;
1558
+ isAutoincrement: false;
1559
+ hasRuntimeDefault: false;
1560
+ enumValues: [string, ...string[]];
1561
+ baseColumn: never;
1562
+ identity: undefined;
1563
+ generated: undefined;
1564
+ }, {}, {}>;
1565
+ };
1566
+ dialect: "pg";
1567
+ }>;
@@ -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
  * Crypto payment charges — tracks the lifecycle of each payment.
5
5
  * reference_id is the charge ID (e.g. "btc:bc1q...").
@@ -82,6 +82,9 @@ export const paymentMethods = pgTable("payment_methods", {
82
82
  oracleAssetId: text("oracle_asset_id"), // CoinGecko slug (e.g. "bitcoin", "tron"). Null = stablecoin (1:1 USD) or use token symbol fallback.
83
83
  confirmations: integer("confirmations").notNull().default(1),
84
84
  nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
85
+ keyRingId: text("key_ring_id"), // FK to key_rings.id (nullable during migration)
86
+ encoding: text("encoding"), // address encoding override (e.g. "bech32", "p2pkh", "evm")
87
+ pluginId: text("plugin_id"), // plugin identifier (e.g. "evm", "utxo", "solana")
85
88
  createdAt: text("created_at").notNull().default(sql `(now())`),
86
89
  });
87
90
  /**
@@ -134,3 +137,32 @@ export const watcherProcessed = pgTable("watcher_processed", {
134
137
  txId: text("tx_id").notNull(),
135
138
  processedAt: text("processed_at").notNull().default(sql `(now())`),
136
139
  }, (table) => [primaryKey({ columns: [table.watcherId, table.txId] })]);
140
+ /**
141
+ * Key rings — decouples key material (xpub/seed) from payment methods.
142
+ * Each key ring maps to a BIP-44 coin type + account index.
143
+ */
144
+ export const keyRings = pgTable("key_rings", {
145
+ id: text("id").primaryKey(),
146
+ curve: text("curve").notNull(), // "secp256k1" | "ed25519"
147
+ derivationScheme: text("derivation_scheme").notNull(), // "bip32" | "slip10" | "ed25519-hd"
148
+ derivationMode: text("derivation_mode").notNull().default("on-demand"), // "on-demand" | "pre-derived"
149
+ keyMaterial: text("key_material").notNull().default("{}"), // JSON: { xpub: "..." }
150
+ coinType: integer("coin_type").notNull(), // BIP-44 coin type
151
+ accountIndex: integer("account_index").notNull().default(0),
152
+ createdAt: text("created_at").notNull().default(sql `(now())`),
153
+ }, (table) => [uniqueIndex("key_rings_path_unique").on(table.coinType, table.accountIndex)]);
154
+ /**
155
+ * Pre-derived address pool — for Ed25519 chains that need offline derivation.
156
+ * Addresses are derived in batches and assigned on demand.
157
+ */
158
+ export const addressPool = pgTable("address_pool", {
159
+ id: serial("id").primaryKey(),
160
+ keyRingId: text("key_ring_id")
161
+ .notNull()
162
+ .references(() => keyRings.id),
163
+ derivationIndex: integer("derivation_index").notNull(),
164
+ publicKey: text("public_key").notNull(),
165
+ address: text("address").notNull(),
166
+ assignedTo: text("assigned_to"), // charge reference or tenant ID
167
+ createdAt: text("created_at").notNull().default(sql `(now())`),
168
+ }, (table) => [uniqueIndex("address_pool_ring_index").on(table.keyRingId, table.derivationIndex)]);
@@ -92,7 +92,7 @@ export declare const snapshots: import("drizzle-orm/pg-core").PgTableWithColumns
92
92
  tableName: "snapshots";
93
93
  dataType: "string";
94
94
  columnType: "PgText";
95
- data: "nightly" | "on-demand" | "pre-restore";
95
+ data: "on-demand" | "nightly" | "pre-restore";
96
96
  driverParam: string;
97
97
  notNull: true;
98
98
  hasDefault: true;
@@ -0,0 +1,35 @@
1
+ -- Key rings: decouples key material from payment methods
2
+ CREATE TABLE IF NOT EXISTS "key_rings" (
3
+ "id" text PRIMARY KEY,
4
+ "curve" text NOT NULL,
5
+ "derivation_scheme" text NOT NULL,
6
+ "derivation_mode" text NOT NULL DEFAULT 'on-demand',
7
+ "key_material" text NOT NULL DEFAULT '{}',
8
+ "coin_type" integer NOT NULL,
9
+ "account_index" integer NOT NULL DEFAULT 0,
10
+ "created_at" text NOT NULL DEFAULT (now())
11
+ );
12
+ --> statement-breakpoint
13
+ CREATE UNIQUE INDEX IF NOT EXISTS "key_rings_path_unique" ON "key_rings" ("coin_type", "account_index");
14
+ --> statement-breakpoint
15
+
16
+ -- Pre-derived address pool (for Ed25519 chains)
17
+ CREATE TABLE IF NOT EXISTS "address_pool" (
18
+ "id" serial PRIMARY KEY,
19
+ "key_ring_id" text NOT NULL REFERENCES "key_rings"("id"),
20
+ "derivation_index" integer NOT NULL,
21
+ "public_key" text NOT NULL,
22
+ "address" text NOT NULL,
23
+ "assigned_to" text,
24
+ "created_at" text NOT NULL DEFAULT (now())
25
+ );
26
+ --> statement-breakpoint
27
+ CREATE UNIQUE INDEX IF NOT EXISTS "address_pool_ring_index" ON "address_pool" ("key_ring_id", "derivation_index");
28
+ --> statement-breakpoint
29
+
30
+ -- Add new columns to payment_methods
31
+ ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "key_ring_id" text REFERENCES "key_rings"("id");
32
+ --> statement-breakpoint
33
+ ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "encoding" text;
34
+ --> statement-breakpoint
35
+ ALTER TABLE "payment_methods" ADD COLUMN IF NOT EXISTS "plugin_id" text;
@@ -0,0 +1,75 @@
1
+ -- Create key rings from existing payment method xpubs
2
+ -- Each unique (coin_type via path_allocations) gets a key ring
3
+
4
+ -- EVM chains (coin type 60)
5
+ INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
6
+ SELECT DISTINCT 'evm-main', 'secp256k1', 'bip32', 'on-demand',
7
+ json_build_object('xpub', pm.xpub)::text,
8
+ pa.coin_type, pa.account_index
9
+ FROM path_allocations pa
10
+ JOIN payment_methods pm ON pm.id = pa.chain_id
11
+ WHERE pa.coin_type = 60
12
+ LIMIT 1
13
+ ON CONFLICT DO NOTHING;
14
+ --> statement-breakpoint
15
+
16
+ -- BTC (coin type 0)
17
+ INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
18
+ SELECT DISTINCT 'btc-main', 'secp256k1', 'bip32', 'on-demand',
19
+ json_build_object('xpub', pm.xpub)::text,
20
+ pa.coin_type, pa.account_index
21
+ FROM path_allocations pa
22
+ JOIN payment_methods pm ON pm.id = pa.chain_id
23
+ WHERE pa.coin_type = 0
24
+ LIMIT 1
25
+ ON CONFLICT DO NOTHING;
26
+ --> statement-breakpoint
27
+
28
+ -- LTC (coin type 2)
29
+ INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
30
+ SELECT DISTINCT 'ltc-main', 'secp256k1', 'bip32', 'on-demand',
31
+ json_build_object('xpub', pm.xpub)::text,
32
+ pa.coin_type, pa.account_index
33
+ FROM path_allocations pa
34
+ JOIN payment_methods pm ON pm.id = pa.chain_id
35
+ WHERE pa.coin_type = 2
36
+ LIMIT 1
37
+ ON CONFLICT DO NOTHING;
38
+ --> statement-breakpoint
39
+
40
+ -- DOGE (coin type 3)
41
+ INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
42
+ SELECT DISTINCT 'doge-main', 'secp256k1', 'bip32', 'on-demand',
43
+ json_build_object('xpub', pm.xpub)::text,
44
+ pa.coin_type, pa.account_index
45
+ FROM path_allocations pa
46
+ JOIN payment_methods pm ON pm.id = pa.chain_id
47
+ WHERE pa.coin_type = 3
48
+ LIMIT 1
49
+ ON CONFLICT DO NOTHING;
50
+ --> statement-breakpoint
51
+
52
+ -- TRON (coin type 195)
53
+ INSERT INTO "key_rings" ("id", "curve", "derivation_scheme", "derivation_mode", "key_material", "coin_type", "account_index")
54
+ SELECT DISTINCT 'tron-main', 'secp256k1', 'bip32', 'on-demand',
55
+ json_build_object('xpub', pm.xpub)::text,
56
+ pa.coin_type, pa.account_index
57
+ FROM path_allocations pa
58
+ JOIN payment_methods pm ON pm.id = pa.chain_id
59
+ WHERE pa.coin_type = 195
60
+ LIMIT 1
61
+ ON CONFLICT DO NOTHING;
62
+ --> statement-breakpoint
63
+
64
+ -- Backfill payment_methods with key_ring_id, encoding, plugin_id
65
+ UPDATE payment_methods SET
66
+ key_ring_id = CASE
67
+ WHEN chain IN ('arbitrum','avalanche','base','base-sepolia','bsc','optimism','polygon','sepolia') THEN 'evm-main'
68
+ WHEN chain = 'bitcoin' THEN 'btc-main'
69
+ WHEN chain = 'litecoin' THEN 'ltc-main'
70
+ WHEN chain = 'dogecoin' THEN 'doge-main'
71
+ WHEN chain = 'tron' THEN 'tron-main'
72
+ END,
73
+ encoding = address_type,
74
+ plugin_id = watcher_type
75
+ WHERE key_ring_id IS NULL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.64.0",
3
+ "version": "1.65.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -72,6 +72,10 @@
72
72
  "./api/routes/verify-email": "./dist/api/routes/verify-email.js",
73
73
  "./api/routes/ws-auth": "./dist/api/routes/ws-auth.js",
74
74
  "./trpc": "./dist/trpc/index.js",
75
+ "./crypto-plugin": {
76
+ "import": "./dist/billing/crypto/plugin/index.js",
77
+ "types": "./dist/billing/crypto/plugin/index.d.ts"
78
+ },
75
79
  "./*": "./dist/*.js"
76
80
  },
77
81
  "scripts": {
@@ -33,6 +33,15 @@ export {
33
33
  export * from "./oracle/index.js";
34
34
  export type { IPaymentMethodStore, PaymentMethodRecord } from "./payment-method-store.js";
35
35
  export { DrizzlePaymentMethodStore } from "./payment-method-store.js";
36
+ export type {
37
+ IAddressEncoder,
38
+ IChainPlugin,
39
+ IChainWatcher,
40
+ ICurveDeriver,
41
+ ISweepStrategy,
42
+ PaymentEvent,
43
+ } from "./plugin/index.js";
44
+ export { PluginRegistry } from "./plugin/index.js";
36
45
  export type { CryptoCharge, CryptoChargeStatus, CryptoPaymentState } from "./types.js";
37
46
  export type { UnifiedCheckoutDeps, UnifiedCheckoutResult } from "./unified-checkout.js";
38
47
  export { createUnifiedCheckout, MIN_CHECKOUT_USD as MIN_PAYMENT_USD, MIN_CHECKOUT_USD } from "./unified-checkout.js";
@@ -364,6 +364,9 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
364
364
  watcherType: body.watcher_type ?? "evm",
365
365
  oracleAssetId: body.oracle_asset_id ?? null,
366
366
  confirmations: body.confirmations ?? 6,
367
+ keyRingId: null,
368
+ encoding: null,
369
+ pluginId: null,
367
370
  });
368
371
 
369
372
  // Record the path allocation (idempotent — ignore if already exists)
@@ -22,6 +22,9 @@ export interface PaymentMethodRecord {
22
22
  watcherType: string;
23
23
  oracleAssetId: string | null;
24
24
  confirmations: number;
25
+ keyRingId: string | null;
26
+ encoding: string | null;
27
+ pluginId: string | null;
25
28
  }
26
29
 
27
30
  export interface IPaymentMethodStore {
@@ -98,6 +101,9 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
98
101
  watcherType: method.watcherType,
99
102
  oracleAssetId: method.oracleAssetId,
100
103
  confirmations: method.confirmations,
104
+ keyRingId: method.keyRingId,
105
+ encoding: method.encoding,
106
+ pluginId: method.pluginId,
101
107
  })
102
108
  .onConflictDoUpdate({
103
109
  target: paymentMethods.id,
@@ -119,6 +125,9 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
119
125
  watcherType: method.watcherType,
120
126
  oracleAssetId: method.oracleAssetId,
121
127
  confirmations: method.confirmations,
128
+ keyRingId: method.keyRingId,
129
+ encoding: method.encoding,
130
+ pluginId: method.pluginId,
122
131
  },
123
132
  });
124
133
  }
@@ -164,5 +173,8 @@ function toRecord(row: typeof paymentMethods.$inferSelect): PaymentMethodRecord
164
173
  watcherType: row.watcherType,
165
174
  oracleAssetId: row.oracleAssetId,
166
175
  confirmations: row.confirmations,
176
+ keyRingId: row.keyRingId,
177
+ encoding: row.encoding,
178
+ pluginId: row.pluginId,
167
179
  };
168
180
  }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { IChainPlugin, PaymentEvent, WatcherOpts } from "../interfaces.js";
3
+ import { PluginRegistry } from "../registry.js";
4
+
5
+ describe("plugin integration — registry → watcher → events", () => {
6
+ it("full lifecycle: register → create watcher → poll → events", async () => {
7
+ const mockEvent: PaymentEvent = {
8
+ chain: "test",
9
+ token: "TEST",
10
+ from: "0xsender",
11
+ to: "0xreceiver",
12
+ rawAmount: "1000",
13
+ amountUsdCents: 100,
14
+ txHash: "0xhash",
15
+ blockNumber: 42,
16
+ confirmations: 6,
17
+ confirmationsRequired: 6,
18
+ };
19
+
20
+ const plugin: IChainPlugin = {
21
+ pluginId: "test",
22
+ supportedCurve: "secp256k1",
23
+ encoders: {},
24
+ createWatcher: (_opts: WatcherOpts) => ({
25
+ init: async () => {},
26
+ poll: async () => [mockEvent],
27
+ setWatchedAddresses: () => {},
28
+ getCursor: () => 42,
29
+ stop: () => {},
30
+ }),
31
+ createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
32
+ version: 1,
33
+ };
34
+
35
+ const registry = new PluginRegistry();
36
+ registry.register(plugin);
37
+
38
+ const resolved = registry.getOrThrow("test");
39
+ const watcher = resolved.createWatcher({
40
+ rpcUrl: "http://localhost:8545",
41
+ rpcHeaders: {},
42
+ oracle: {
43
+ getPrice: async () => ({ priceMicros: 3500_000000 }),
44
+ },
45
+ cursorStore: {
46
+ get: async () => null,
47
+ save: async () => {},
48
+ getConfirmationCount: async () => null,
49
+ saveConfirmationCount: async () => {},
50
+ },
51
+ token: "TEST",
52
+ chain: "test",
53
+ decimals: 18,
54
+ confirmations: 6,
55
+ });
56
+
57
+ await watcher.init();
58
+ const events = await watcher.poll();
59
+ expect(events).toHaveLength(1);
60
+ expect(events[0].txHash).toBe("0xhash");
61
+ expect(watcher.getCursor()).toBe(42);
62
+ watcher.stop();
63
+ });
64
+ });