@wopr-network/platform-core 1.64.0 → 1.66.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/index.d.ts +2 -0
- package/dist/billing/crypto/index.js +1 -0
- package/dist/billing/crypto/key-server-entry.js +37 -12
- package/dist/billing/crypto/key-server.d.ts +12 -0
- package/dist/billing/crypto/key-server.js +24 -4
- package/dist/billing/crypto/payment-method-store.d.ts +3 -0
- package/dist/billing/crypto/payment-method-store.js +9 -0
- package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +1 -0
- package/dist/billing/crypto/plugin/__tests__/integration.test.js +58 -0
- package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +1 -0
- package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +46 -0
- package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +1 -0
- package/dist/billing/crypto/plugin/__tests__/registry.test.js +49 -0
- package/dist/billing/crypto/plugin/index.d.ts +2 -0
- package/dist/billing/crypto/plugin/index.js +1 -0
- package/dist/billing/crypto/plugin/interfaces.d.ts +97 -0
- package/dist/billing/crypto/plugin/interfaces.js +2 -0
- package/dist/billing/crypto/plugin/registry.d.ts +8 -0
- package/dist/billing/crypto/plugin/registry.js +21 -0
- package/dist/billing/crypto/plugin-watcher-service.d.ts +32 -0
- package/dist/billing/crypto/plugin-watcher-service.js +113 -0
- package/dist/db/schema/crypto.d.ts +328 -0
- package/dist/db/schema/crypto.js +33 -1
- package/dist/db/schema/snapshots.d.ts +1 -1
- package/docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md +48 -34
- package/drizzle/migrations/0023_key_rings_table.sql +35 -0
- package/drizzle/migrations/0024_backfill_key_rings.sql +75 -0
- package/package.json +6 -1
- package/src/billing/crypto/index.ts +9 -0
- package/src/billing/crypto/key-server-entry.ts +48 -12
- package/src/billing/crypto/key-server.ts +28 -3
- package/src/billing/crypto/payment-method-store.ts +12 -0
- package/src/billing/crypto/plugin/__tests__/integration.test.ts +64 -0
- package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +51 -0
- package/src/billing/crypto/plugin/__tests__/registry.test.ts +58 -0
- package/src/billing/crypto/plugin/index.ts +17 -0
- package/src/billing/crypto/plugin/interfaces.ts +106 -0
- package/src/billing/crypto/plugin/registry.ts +26 -0
- package/src/billing/crypto/plugin-watcher-service.ts +148 -0
- package/src/db/schema/crypto.ts +43 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-driven watcher service — replaces the hardcoded watcher-service.ts.
|
|
3
|
+
*
|
|
4
|
+
* Instead of importing BtcWatcher/EvmWatcher/EthWatcher directly,
|
|
5
|
+
* this delegates to IChainPlugin.createWatcher() from the plugin registry.
|
|
6
|
+
* Adding a new chain = register a plugin + INSERT a payment_methods row.
|
|
7
|
+
*
|
|
8
|
+
* Payment flow is unchanged:
|
|
9
|
+
* plugin.poll() -> PaymentEvent[] -> handlePayment() -> credit + webhook
|
|
10
|
+
*/
|
|
11
|
+
import { handlePayment } from "./watcher-service.js";
|
|
12
|
+
/** Map legacy watcher_type values to plugin IDs for backward compatibility. */
|
|
13
|
+
const WATCHER_TYPE_TO_PLUGIN = {
|
|
14
|
+
utxo: "bitcoin",
|
|
15
|
+
evm: "evm",
|
|
16
|
+
};
|
|
17
|
+
function resolvePlugin(registry, method) {
|
|
18
|
+
// Prefer explicit plugin_id, fall back to watcher_type mapping
|
|
19
|
+
const id = method.pluginId ?? WATCHER_TYPE_TO_PLUGIN[method.watcherType];
|
|
20
|
+
return id ? registry.get(id) : undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Boot plugin-driven watchers for all enabled payment methods.
|
|
24
|
+
*
|
|
25
|
+
* Returns a cleanup function that stops all poll timers and watchers.
|
|
26
|
+
*/
|
|
27
|
+
export async function startPluginWatchers(opts) {
|
|
28
|
+
const { db, chargeStore, methodStore, cursorStore, oracle, registry } = opts;
|
|
29
|
+
const pollMs = opts.pollIntervalMs ?? 15_000;
|
|
30
|
+
const log = opts.log ?? (() => { });
|
|
31
|
+
const methods = await methodStore.listEnabled();
|
|
32
|
+
const timers = [];
|
|
33
|
+
const watchers = [];
|
|
34
|
+
for (const method of methods) {
|
|
35
|
+
if (!method.rpcUrl)
|
|
36
|
+
continue;
|
|
37
|
+
const plugin = resolvePlugin(registry, method);
|
|
38
|
+
if (!plugin) {
|
|
39
|
+
log("No plugin found, skipping method", { id: method.id, chain: method.chain, watcherType: method.watcherType });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const watcher = plugin.createWatcher({
|
|
43
|
+
rpcUrl: method.rpcUrl,
|
|
44
|
+
rpcHeaders: JSON.parse(method.rpcHeaders ?? "{}"),
|
|
45
|
+
oracle,
|
|
46
|
+
cursorStore,
|
|
47
|
+
token: method.token,
|
|
48
|
+
chain: method.chain,
|
|
49
|
+
contractAddress: method.contractAddress ?? undefined,
|
|
50
|
+
decimals: method.decimals,
|
|
51
|
+
confirmations: method.confirmations,
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
await watcher.init();
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
log("Watcher init failed, skipping", { chain: method.chain, token: method.token, error: String(err) });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Seed watched addresses from active charges
|
|
61
|
+
const active = await chargeStore.listActiveDepositAddresses();
|
|
62
|
+
const addrs = active.filter((a) => a.chain === method.chain && a.token === method.token).map((a) => a.address);
|
|
63
|
+
watcher.setWatchedAddresses(addrs);
|
|
64
|
+
watchers.push(watcher);
|
|
65
|
+
log(`Plugin watcher started (${method.chain}:${method.token})`, {
|
|
66
|
+
plugin: plugin.pluginId,
|
|
67
|
+
addresses: addrs.length,
|
|
68
|
+
});
|
|
69
|
+
let polling = false;
|
|
70
|
+
timers.push(setInterval(async () => {
|
|
71
|
+
if (polling)
|
|
72
|
+
return;
|
|
73
|
+
polling = true;
|
|
74
|
+
try {
|
|
75
|
+
// Refresh watched addresses each cycle
|
|
76
|
+
const fresh = await chargeStore.listActiveDepositAddresses();
|
|
77
|
+
const freshAddrs = fresh
|
|
78
|
+
.filter((a) => a.chain === method.chain && a.token === method.token)
|
|
79
|
+
.map((a) => a.address);
|
|
80
|
+
watcher.setWatchedAddresses(freshAddrs);
|
|
81
|
+
const events = await watcher.poll();
|
|
82
|
+
for (const ev of events) {
|
|
83
|
+
log("Plugin payment", {
|
|
84
|
+
chain: ev.chain,
|
|
85
|
+
token: ev.token,
|
|
86
|
+
to: ev.to,
|
|
87
|
+
txHash: ev.txHash,
|
|
88
|
+
confirmations: ev.confirmations,
|
|
89
|
+
});
|
|
90
|
+
await handlePayment(db, chargeStore, ev.to, ev.rawAmount, {
|
|
91
|
+
txHash: ev.txHash,
|
|
92
|
+
confirmations: ev.confirmations,
|
|
93
|
+
confirmationsRequired: ev.confirmationsRequired,
|
|
94
|
+
amountReceivedCents: ev.amountUsdCents,
|
|
95
|
+
}, log);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
log("Plugin poll error", { chain: method.chain, token: method.token, error: String(err) });
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
polling = false;
|
|
103
|
+
}
|
|
104
|
+
}, pollMs));
|
|
105
|
+
}
|
|
106
|
+
log("All plugin watchers started", { count: watchers.length, pollMs });
|
|
107
|
+
return () => {
|
|
108
|
+
for (const t of timers)
|
|
109
|
+
clearInterval(t);
|
|
110
|
+
for (const w of watchers)
|
|
111
|
+
w.stop();
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -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
|
+
}>;
|
package/dist/db/schema/crypto.js
CHANGED
|
@@ -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: "
|
|
95
|
+
data: "on-demand" | "nightly" | "pre-restore";
|
|
96
96
|
driverParam: string;
|
|
97
97
|
notNull: true;
|
|
98
98
|
hasDefault: true;
|
|
@@ -174,19 +174,32 @@ interface WatcherOpts {
|
|
|
174
174
|
|
|
175
175
|
Replaced by `key_rings` unique constraint on `(coin_type, account_index)`.
|
|
176
176
|
|
|
177
|
-
## Plugin
|
|
177
|
+
## Plugin Monorepo
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
Single npm package: `@wopr-network/crypto-plugins`. One version, one CI, one install.
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
181
|
+
```
|
|
182
|
+
crypto-plugins/
|
|
183
|
+
src/
|
|
184
|
+
evm/ # ETH, Base, Arbitrum, Polygon, Optimism, Avalanche, BSC
|
|
185
|
+
bitcoin/ # BTC (uses shared/utxo)
|
|
186
|
+
litecoin/ # LTC (uses shared/utxo)
|
|
187
|
+
dogecoin/ # DOGE (uses shared/utxo)
|
|
188
|
+
tron/ # TRX + TRC-20 (T-address ↔ hex conversion)
|
|
189
|
+
solana/ # SOL + SPL tokens (Ed25519)
|
|
190
|
+
shared/ # UTXO common (bitcoind RPC, watcher, sweep), test helpers
|
|
191
|
+
index.ts # barrel export of all plugins
|
|
192
|
+
package.json # peer dep on @wopr-network/platform-core
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| Plugin | Curve | Subpath | Chains |
|
|
196
|
+
|--------|-------|---------|--------|
|
|
197
|
+
| `evm` | secp256k1 | `@wopr-network/crypto-plugins/evm` | ETH + all EVM L1/L2s |
|
|
198
|
+
| `bitcoin` | secp256k1 | `@wopr-network/crypto-plugins/bitcoin` | BTC |
|
|
199
|
+
| `litecoin` | secp256k1 | `@wopr-network/crypto-plugins/litecoin` | LTC |
|
|
200
|
+
| `dogecoin` | secp256k1 | `@wopr-network/crypto-plugins/dogecoin` | DOGE |
|
|
201
|
+
| `tron` | secp256k1 | `@wopr-network/crypto-plugins/tron` | TRX + TRC-20 |
|
|
202
|
+
| `solana` | ed25519 | `@wopr-network/crypto-plugins/solana` | SOL + SPL |
|
|
190
203
|
|
|
191
204
|
Platform-core exports interfaces as a peer dependency:
|
|
192
205
|
```
|
|
@@ -198,9 +211,9 @@ Platform-core exports interfaces as a peer dependency:
|
|
|
198
211
|
Explicit imports in the key-server entry point:
|
|
199
212
|
|
|
200
213
|
```ts
|
|
201
|
-
import { evmPlugin } from "@wopr-network/crypto-
|
|
202
|
-
import { bitcoinPlugin } from "@wopr-network/crypto-
|
|
203
|
-
import { solanaPlugin } from "@wopr-network/crypto-
|
|
214
|
+
import { evmPlugin } from "@wopr-network/crypto-plugins/evm";
|
|
215
|
+
import { bitcoinPlugin } from "@wopr-network/crypto-plugins/bitcoin";
|
|
216
|
+
import { solanaPlugin } from "@wopr-network/crypto-plugins/solana";
|
|
204
217
|
|
|
205
218
|
const registry = new PluginRegistry();
|
|
206
219
|
registry.register(evmPlugin);
|
|
@@ -245,21 +258,18 @@ No per-chain env vars. RPC URLs and headers come from the chain server.
|
|
|
245
258
|
|
|
246
259
|
## Adding a New Chain
|
|
247
260
|
|
|
248
|
-
###
|
|
249
|
-
1. `
|
|
250
|
-
2. Add
|
|
251
|
-
3.
|
|
252
|
-
4.
|
|
253
|
-
5.
|
|
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
|
+
### Existing chain type (e.g. XRP — secp256k1)
|
|
262
|
+
1. Add `src/xrp/` to `@wopr-network/crypto-plugins` with encoder + watcher + sweeper
|
|
263
|
+
2. Add subpath export, bump version, publish
|
|
264
|
+
3. Add `registry.register(xrpPlugin)` to key-server entry point
|
|
265
|
+
4. `npm update @wopr-network/crypto-plugins`
|
|
266
|
+
5. Insert `key_ring` row + `payment_method` row in DB
|
|
261
267
|
6. Restart
|
|
262
268
|
|
|
269
|
+
### New curve (e.g. Solana — Ed25519)
|
|
270
|
+
Same as above, plus:
|
|
271
|
+
- Replenish pool: `openssl enc ... -d | npx @wopr-network/crypto-sweep replenish --chain solana --count 200`
|
|
272
|
+
|
|
263
273
|
No code changes to platform-core for either path.
|
|
264
274
|
|
|
265
275
|
## Client API
|
|
@@ -282,19 +292,23 @@ Only change: admin `POST /admin/chains` takes `key_ring_id` + `encoding` + `plug
|
|
|
282
292
|
- Backfill: create key_ring rows from existing xpub + address_type data, set `key_ring_id` + `encoding` + `plugin_id` on existing payment_methods
|
|
283
293
|
- DB migration 2: drop old columns (`xpub`, `address_type`, `watcher_type`), drop `path_allocations`
|
|
284
294
|
|
|
285
|
-
### Phase 2 — Extract existing chains into plugins
|
|
286
|
-
-
|
|
287
|
-
- `
|
|
288
|
-
- `
|
|
295
|
+
### Phase 2 — Extract existing chains into `@wopr-network/crypto-plugins` monorepo
|
|
296
|
+
- New repo: `wopr-network/crypto-plugins`
|
|
297
|
+
- `src/evm/` — from current `evm/watcher.ts`, `evm/eth-watcher.ts`
|
|
298
|
+
- `src/shared/utxo/` — shared bitcoind RPC, UTXO watcher, sweep base
|
|
299
|
+
- `src/bitcoin/`, `src/litecoin/`, `src/dogecoin/` — thin configs on shared/utxo
|
|
300
|
+
- `src/tron/` — from current tron code (address conversion internal)
|
|
301
|
+
- `src/shared/test-helpers/` — mock RPC, test fixtures
|
|
289
302
|
- All existing behavior preserved, just restructured
|
|
303
|
+
- One npm package, one version, subpath exports per chain
|
|
290
304
|
|
|
291
305
|
### Phase 3 — Unified sweep CLI
|
|
292
|
-
- `@wopr-network/crypto-sweep`
|
|
306
|
+
- Sweep + replenish modes live in `@wopr-network/crypto-plugins/sweep` (same monorepo, separate entrypoint)
|
|
293
307
|
- Replaces `sweep-stablecoins.ts` and `sweep-tron.ts`
|
|
294
308
|
|
|
295
309
|
### Phase 4 — New chains
|
|
296
|
-
- `crypto-
|
|
297
|
-
- Then TON, XRP, etc.
|
|
310
|
+
- `src/solana/` in crypto-plugins — first Ed25519 chain, proves pool model
|
|
311
|
+
- Then TON, XRP, etc. — each is a new directory + subpath export + version bump
|
|
298
312
|
|
|
299
313
|
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
314
|
|
|
@@ -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;
|