@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
|
@@ -13,6 +13,8 @@ export { handleKeyServerWebhook, handleKeyServerWebhook as handleCryptoWebhook,
|
|
|
13
13
|
export * from "./oracle/index.js";
|
|
14
14
|
export type { IPaymentMethodStore, PaymentMethodRecord } from "./payment-method-store.js";
|
|
15
15
|
export { DrizzlePaymentMethodStore } from "./payment-method-store.js";
|
|
16
|
+
export type { IAddressEncoder, IChainPlugin, IChainWatcher, ICurveDeriver, ISweepStrategy, PaymentEvent, } from "./plugin/index.js";
|
|
17
|
+
export { PluginRegistry } from "./plugin/index.js";
|
|
16
18
|
export type { CryptoCharge, CryptoChargeStatus, CryptoPaymentState } from "./types.js";
|
|
17
19
|
export type { UnifiedCheckoutDeps, UnifiedCheckoutResult } from "./unified-checkout.js";
|
|
18
20
|
export { createUnifiedCheckout, MIN_CHECKOUT_USD as MIN_PAYMENT_USD, MIN_CHECKOUT_USD } from "./unified-checkout.js";
|
|
@@ -7,4 +7,5 @@ export { createKeyServerApp } from "./key-server.js";
|
|
|
7
7
|
export { handleKeyServerWebhook, handleKeyServerWebhook as handleCryptoWebhook, normalizeStatus, } from "./key-server-webhook.js";
|
|
8
8
|
export * from "./oracle/index.js";
|
|
9
9
|
export { DrizzlePaymentMethodStore } from "./payment-method-store.js";
|
|
10
|
+
export { PluginRegistry } from "./plugin/index.js";
|
|
10
11
|
export { createUnifiedCheckout, MIN_CHECKOUT_USD as MIN_PAYMENT_USD, MIN_CHECKOUT_USD } from "./unified-checkout.js";
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/* biome-ignore-all lint/suspicious/noConsole: standalone entry point */
|
|
10
10
|
import { serve } from "@hono/node-server";
|
|
11
|
+
import { bitcoinPlugin, dogecoinPlugin, evmPlugin, litecoinPlugin, solanaPlugin, tronPlugin, } from "@wopr-network/crypto-plugins";
|
|
11
12
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
12
13
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
13
14
|
import pg from "pg";
|
|
@@ -21,6 +22,8 @@ import { CoinGeckoOracle } from "./oracle/coingecko.js";
|
|
|
21
22
|
import { CompositeOracle } from "./oracle/composite.js";
|
|
22
23
|
import { FixedPriceOracle } from "./oracle/fixed.js";
|
|
23
24
|
import { DrizzlePaymentMethodStore } from "./payment-method-store.js";
|
|
25
|
+
import { PluginRegistry } from "./plugin/registry.js";
|
|
26
|
+
import { startPluginWatchers } from "./plugin-watcher-service.js";
|
|
24
27
|
import { startWatchers } from "./watcher-service.js";
|
|
25
28
|
const PORT = Number(process.env.PORT ?? "3100");
|
|
26
29
|
const DATABASE_URL = process.env.DATABASE_URL;
|
|
@@ -57,6 +60,15 @@ async function main() {
|
|
|
57
60
|
}
|
|
58
61
|
const coingecko = new CoinGeckoOracle({ tokenIds: dbTokenIds });
|
|
59
62
|
const oracle = new CompositeOracle(chainlink, coingecko);
|
|
63
|
+
// Build plugin registry — one plugin per chain family
|
|
64
|
+
const registry = new PluginRegistry();
|
|
65
|
+
registry.register(bitcoinPlugin);
|
|
66
|
+
registry.register(litecoinPlugin);
|
|
67
|
+
registry.register(dogecoinPlugin);
|
|
68
|
+
registry.register(evmPlugin);
|
|
69
|
+
registry.register(tronPlugin);
|
|
70
|
+
registry.register(solanaPlugin);
|
|
71
|
+
console.log(`[crypto-key-server] Registered ${registry.list().length} chain plugins:`, registry.list().map((p) => p.pluginId));
|
|
60
72
|
const app = createKeyServerApp({
|
|
61
73
|
db,
|
|
62
74
|
chargeStore,
|
|
@@ -64,20 +76,33 @@ async function main() {
|
|
|
64
76
|
oracle,
|
|
65
77
|
serviceKey: SERVICE_KEY,
|
|
66
78
|
adminToken: ADMIN_TOKEN,
|
|
79
|
+
registry,
|
|
67
80
|
});
|
|
68
|
-
// Boot watchers
|
|
81
|
+
// Boot plugin-driven watchers — polls for payments, sends webhooks.
|
|
82
|
+
// Falls back to legacy startWatchers() if USE_LEGACY_WATCHERS=1 is set.
|
|
69
83
|
const cursorStore = new DrizzleWatcherCursorStore(db);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
const useLegacy = process.env.USE_LEGACY_WATCHERS === "1";
|
|
85
|
+
const stopWatchers = useLegacy
|
|
86
|
+
? await startWatchers({
|
|
87
|
+
db,
|
|
88
|
+
chargeStore,
|
|
89
|
+
methodStore,
|
|
90
|
+
cursorStore,
|
|
91
|
+
oracle,
|
|
92
|
+
bitcoindUser: BITCOIND_USER,
|
|
93
|
+
bitcoindPassword: BITCOIND_PASSWORD,
|
|
94
|
+
serviceKey: SERVICE_KEY,
|
|
95
|
+
log: (msg, meta) => console.log(`[watcher] ${msg}`, meta ?? ""),
|
|
96
|
+
})
|
|
97
|
+
: await startPluginWatchers({
|
|
98
|
+
db,
|
|
99
|
+
chargeStore,
|
|
100
|
+
methodStore,
|
|
101
|
+
cursorStore,
|
|
102
|
+
oracle,
|
|
103
|
+
registry,
|
|
104
|
+
log: (msg, meta) => console.log(`[watcher] ${msg}`, meta ?? ""),
|
|
105
|
+
});
|
|
81
106
|
const server = serve({ fetch: app.fetch, port: PORT });
|
|
82
107
|
console.log(`[crypto-key-server] Listening on :${PORT}`);
|
|
83
108
|
// Graceful shutdown — stop accepting requests, drain watchers, close pool
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto Key Server — shared address derivation + charge management.
|
|
3
|
+
*
|
|
4
|
+
* Deploys on the chain server (pay.wopr.bot) alongside bitcoind.
|
|
5
|
+
* Products don't run watchers or hold xpubs. They request addresses
|
|
6
|
+
* and receive webhooks.
|
|
7
|
+
*
|
|
8
|
+
* ~200 lines of new code wrapping platform-core's existing crypto modules.
|
|
9
|
+
*/
|
|
1
10
|
import { Hono } from "hono";
|
|
2
11
|
import type { DrizzleDb } from "../../db/index.js";
|
|
3
12
|
import type { ICryptoChargeRepository } from "./charge-store.js";
|
|
4
13
|
import type { IPriceOracle } from "./oracle/types.js";
|
|
5
14
|
import type { IPaymentMethodStore } from "./payment-method-store.js";
|
|
15
|
+
import type { PluginRegistry } from "./plugin/registry.js";
|
|
6
16
|
export interface KeyServerDeps {
|
|
7
17
|
db: DrizzleDb;
|
|
8
18
|
chargeStore: ICryptoChargeRepository;
|
|
@@ -12,6 +22,8 @@ export interface KeyServerDeps {
|
|
|
12
22
|
serviceKey?: string;
|
|
13
23
|
/** Bearer token for admin routes. If unset, admin routes are disabled. */
|
|
14
24
|
adminToken?: string;
|
|
25
|
+
/** Plugin registry for address encoding. Falls back to address-gen.ts when absent. */
|
|
26
|
+
registry?: PluginRegistry;
|
|
15
27
|
}
|
|
16
28
|
/**
|
|
17
29
|
* Create the Hono app for the crypto key server.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ~200 lines of new code wrapping platform-core's existing crypto modules.
|
|
9
9
|
*/
|
|
10
|
+
import { HDKey } from "@scure/bip32";
|
|
10
11
|
import { eq, sql } from "drizzle-orm";
|
|
11
12
|
import { Hono } from "hono";
|
|
12
13
|
import { derivedAddresses, pathAllocations, paymentMethods } from "../../db/schema/crypto.js";
|
|
@@ -21,7 +22,7 @@ import { AssetNotSupportedError } from "./oracle/types.js";
|
|
|
21
22
|
* address. The unique constraint on derived_addresses.address prevents reuse.
|
|
22
23
|
* On collision, we skip the index and retry (up to maxRetries).
|
|
23
24
|
*/
|
|
24
|
-
async function deriveNextAddress(db, chainId, tenantId) {
|
|
25
|
+
async function deriveNextAddress(db, chainId, tenantId, registry) {
|
|
25
26
|
const maxRetries = 10;
|
|
26
27
|
const dbWithTx = db;
|
|
27
28
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -46,7 +47,23 @@ async function deriveNextAddress(db, chainId, tenantId) {
|
|
|
46
47
|
catch {
|
|
47
48
|
throw new Error(`Invalid encoding_params JSON for chain ${chainId}: ${method.encodingParams}`);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
// Plugin-driven encoding: look up the plugin, use its encoder.
|
|
51
|
+
// Falls back to legacy deriveAddress() when no registry or no matching plugin.
|
|
52
|
+
let address;
|
|
53
|
+
const pluginId = method.pluginId ?? (method.watcherType === "utxo" ? "bitcoin" : method.watcherType);
|
|
54
|
+
const plugin = registry?.get(pluginId ?? "");
|
|
55
|
+
const encodingKey = method.encoding ?? method.addressType;
|
|
56
|
+
const encoder = plugin?.encoders[encodingKey];
|
|
57
|
+
if (encoder) {
|
|
58
|
+
const master = HDKey.fromExtendedKey(method.xpub);
|
|
59
|
+
const child = master.deriveChild(0).deriveChild(index);
|
|
60
|
+
if (!child.publicKey)
|
|
61
|
+
throw new Error("Failed to derive public key");
|
|
62
|
+
address = encoder.encode(child.publicKey, encodingParams);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
address = deriveAddress(method.xpub, index, method.addressType, encodingParams);
|
|
66
|
+
}
|
|
50
67
|
// Step 2: Record in immutable log. If this address was already derived by a
|
|
51
68
|
// sibling chain (shared xpub), the unique constraint fires and we retry
|
|
52
69
|
// with the next index (which is already incremented above).
|
|
@@ -121,7 +138,7 @@ export function createKeyServerApp(deps) {
|
|
|
121
138
|
if (!body.chain)
|
|
122
139
|
return c.json({ error: "chain is required" }, 400);
|
|
123
140
|
const tenantId = c.req.header("X-Tenant-Id");
|
|
124
|
-
const result = await deriveNextAddress(deps.db, body.chain, tenantId ?? undefined);
|
|
141
|
+
const result = await deriveNextAddress(deps.db, body.chain, tenantId ?? undefined, deps.registry);
|
|
125
142
|
return c.json(result, 201);
|
|
126
143
|
});
|
|
127
144
|
/** POST /charges — create charge + derive address + start watching */
|
|
@@ -131,7 +148,7 @@ export function createKeyServerApp(deps) {
|
|
|
131
148
|
return c.json({ error: "chain is required and amountUsd must be a positive finite number" }, 400);
|
|
132
149
|
}
|
|
133
150
|
const tenantId = c.req.header("X-Tenant-Id") ?? "unknown";
|
|
134
|
-
const { address, index, chain, token } = await deriveNextAddress(deps.db, body.chain, tenantId);
|
|
151
|
+
const { address, index, chain, token } = await deriveNextAddress(deps.db, body.chain, tenantId, deps.registry);
|
|
135
152
|
// Look up payment method for decimals + oracle config
|
|
136
153
|
const method = await deps.methodStore.getById(body.chain);
|
|
137
154
|
if (!method)
|
|
@@ -287,6 +304,9 @@ export function createKeyServerApp(deps) {
|
|
|
287
304
|
watcherType: body.watcher_type ?? "evm",
|
|
288
305
|
oracleAssetId: body.oracle_asset_id ?? null,
|
|
289
306
|
confirmations: body.confirmations ?? 6,
|
|
307
|
+
keyRingId: null,
|
|
308
|
+
encoding: null,
|
|
309
|
+
pluginId: null,
|
|
290
310
|
});
|
|
291
311
|
// Record the path allocation (idempotent — ignore if already exists)
|
|
292
312
|
const inserted = (await deps.db
|
|
@@ -19,6 +19,9 @@ export interface PaymentMethodRecord {
|
|
|
19
19
|
watcherType: string;
|
|
20
20
|
oracleAssetId: string | null;
|
|
21
21
|
confirmations: number;
|
|
22
|
+
keyRingId: string | null;
|
|
23
|
+
encoding: string | null;
|
|
24
|
+
pluginId: string | null;
|
|
22
25
|
}
|
|
23
26
|
export interface IPaymentMethodStore {
|
|
24
27
|
/** List all enabled payment methods, ordered by displayOrder. */
|
|
@@ -52,6 +52,9 @@ export class DrizzlePaymentMethodStore {
|
|
|
52
52
|
watcherType: method.watcherType,
|
|
53
53
|
oracleAssetId: method.oracleAssetId,
|
|
54
54
|
confirmations: method.confirmations,
|
|
55
|
+
keyRingId: method.keyRingId,
|
|
56
|
+
encoding: method.encoding,
|
|
57
|
+
pluginId: method.pluginId,
|
|
55
58
|
})
|
|
56
59
|
.onConflictDoUpdate({
|
|
57
60
|
target: paymentMethods.id,
|
|
@@ -73,6 +76,9 @@ export class DrizzlePaymentMethodStore {
|
|
|
73
76
|
watcherType: method.watcherType,
|
|
74
77
|
oracleAssetId: method.oracleAssetId,
|
|
75
78
|
confirmations: method.confirmations,
|
|
79
|
+
keyRingId: method.keyRingId,
|
|
80
|
+
encoding: method.encoding,
|
|
81
|
+
pluginId: method.pluginId,
|
|
76
82
|
},
|
|
77
83
|
});
|
|
78
84
|
}
|
|
@@ -114,5 +120,8 @@ function toRecord(row) {
|
|
|
114
120
|
watcherType: row.watcherType,
|
|
115
121
|
oracleAssetId: row.oracleAssetId,
|
|
116
122
|
confirmations: row.confirmations,
|
|
123
|
+
keyRingId: row.keyRingId,
|
|
124
|
+
encoding: row.encoding,
|
|
125
|
+
pluginId: row.pluginId,
|
|
117
126
|
};
|
|
118
127
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PluginRegistry } from "../registry.js";
|
|
3
|
+
describe("plugin integration — registry → watcher → events", () => {
|
|
4
|
+
it("full lifecycle: register → create watcher → poll → events", async () => {
|
|
5
|
+
const mockEvent = {
|
|
6
|
+
chain: "test",
|
|
7
|
+
token: "TEST",
|
|
8
|
+
from: "0xsender",
|
|
9
|
+
to: "0xreceiver",
|
|
10
|
+
rawAmount: "1000",
|
|
11
|
+
amountUsdCents: 100,
|
|
12
|
+
txHash: "0xhash",
|
|
13
|
+
blockNumber: 42,
|
|
14
|
+
confirmations: 6,
|
|
15
|
+
confirmationsRequired: 6,
|
|
16
|
+
};
|
|
17
|
+
const plugin = {
|
|
18
|
+
pluginId: "test",
|
|
19
|
+
supportedCurve: "secp256k1",
|
|
20
|
+
encoders: {},
|
|
21
|
+
createWatcher: (_opts) => ({
|
|
22
|
+
init: async () => { },
|
|
23
|
+
poll: async () => [mockEvent],
|
|
24
|
+
setWatchedAddresses: () => { },
|
|
25
|
+
getCursor: () => 42,
|
|
26
|
+
stop: () => { },
|
|
27
|
+
}),
|
|
28
|
+
createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
|
|
29
|
+
version: 1,
|
|
30
|
+
};
|
|
31
|
+
const registry = new PluginRegistry();
|
|
32
|
+
registry.register(plugin);
|
|
33
|
+
const resolved = registry.getOrThrow("test");
|
|
34
|
+
const watcher = resolved.createWatcher({
|
|
35
|
+
rpcUrl: "http://localhost:8545",
|
|
36
|
+
rpcHeaders: {},
|
|
37
|
+
oracle: {
|
|
38
|
+
getPrice: async () => ({ priceMicros: 3500_000000 }),
|
|
39
|
+
},
|
|
40
|
+
cursorStore: {
|
|
41
|
+
get: async () => null,
|
|
42
|
+
save: async () => { },
|
|
43
|
+
getConfirmationCount: async () => null,
|
|
44
|
+
saveConfirmationCount: async () => { },
|
|
45
|
+
},
|
|
46
|
+
token: "TEST",
|
|
47
|
+
chain: "test",
|
|
48
|
+
decimals: 18,
|
|
49
|
+
confirmations: 6,
|
|
50
|
+
});
|
|
51
|
+
await watcher.init();
|
|
52
|
+
const events = await watcher.poll();
|
|
53
|
+
expect(events).toHaveLength(1);
|
|
54
|
+
expect(events[0].txHash).toBe("0xhash");
|
|
55
|
+
expect(watcher.getCursor()).toBe(42);
|
|
56
|
+
watcher.stop();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/billing/crypto/plugin/__tests__/interfaces.test.ts
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
describe("plugin interfaces — type contracts", () => {
|
|
4
|
+
it("PaymentEvent has required fields", () => {
|
|
5
|
+
const event = {
|
|
6
|
+
chain: "ethereum",
|
|
7
|
+
token: "ETH",
|
|
8
|
+
from: "0xabc",
|
|
9
|
+
to: "0xdef",
|
|
10
|
+
rawAmount: "1000000000000000000",
|
|
11
|
+
amountUsdCents: 350000,
|
|
12
|
+
txHash: "0x123",
|
|
13
|
+
blockNumber: 100,
|
|
14
|
+
confirmations: 6,
|
|
15
|
+
confirmationsRequired: 6,
|
|
16
|
+
};
|
|
17
|
+
expect(event.chain).toBe("ethereum");
|
|
18
|
+
expect(event.amountUsdCents).toBe(350000);
|
|
19
|
+
});
|
|
20
|
+
it("ICurveDeriver contract is satisfiable", () => {
|
|
21
|
+
const deriver = {
|
|
22
|
+
derivePublicKey: (_chain, _index) => new Uint8Array(33),
|
|
23
|
+
getCurve: () => "secp256k1",
|
|
24
|
+
};
|
|
25
|
+
expect(deriver.getCurve()).toBe("secp256k1");
|
|
26
|
+
expect(deriver.derivePublicKey(0, 0)).toBeInstanceOf(Uint8Array);
|
|
27
|
+
});
|
|
28
|
+
it("IAddressEncoder contract is satisfiable", () => {
|
|
29
|
+
const encoder = {
|
|
30
|
+
encode: (_pk, _params) => "bc1qtest",
|
|
31
|
+
encodingType: () => "bech32",
|
|
32
|
+
};
|
|
33
|
+
expect(encoder.encodingType()).toBe("bech32");
|
|
34
|
+
expect(encoder.encode(new Uint8Array(33), { hrp: "bc" })).toBe("bc1qtest");
|
|
35
|
+
});
|
|
36
|
+
it("IChainWatcher contract is satisfiable", () => {
|
|
37
|
+
const watcher = {
|
|
38
|
+
init: async () => { },
|
|
39
|
+
poll: async () => [],
|
|
40
|
+
setWatchedAddresses: () => { },
|
|
41
|
+
getCursor: () => 0,
|
|
42
|
+
stop: () => { },
|
|
43
|
+
};
|
|
44
|
+
expect(watcher.getCursor()).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PluginRegistry } from "../registry.js";
|
|
3
|
+
function mockPlugin(id, curve = "secp256k1") {
|
|
4
|
+
return {
|
|
5
|
+
pluginId: id,
|
|
6
|
+
supportedCurve: curve,
|
|
7
|
+
encoders: {},
|
|
8
|
+
createWatcher: () => ({
|
|
9
|
+
init: async () => { },
|
|
10
|
+
poll: async () => [],
|
|
11
|
+
setWatchedAddresses: () => { },
|
|
12
|
+
getCursor: () => 0,
|
|
13
|
+
stop: () => { },
|
|
14
|
+
}),
|
|
15
|
+
createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
|
|
16
|
+
version: 1,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe("PluginRegistry", () => {
|
|
20
|
+
it("registers and retrieves a plugin", () => {
|
|
21
|
+
const reg = new PluginRegistry();
|
|
22
|
+
reg.register(mockPlugin("evm"));
|
|
23
|
+
expect(reg.get("evm")).toBeDefined();
|
|
24
|
+
expect(reg.get("evm")?.pluginId).toBe("evm");
|
|
25
|
+
});
|
|
26
|
+
it("throws on duplicate registration", () => {
|
|
27
|
+
const reg = new PluginRegistry();
|
|
28
|
+
reg.register(mockPlugin("evm"));
|
|
29
|
+
expect(() => reg.register(mockPlugin("evm"))).toThrow("already registered");
|
|
30
|
+
});
|
|
31
|
+
it("returns undefined for unknown plugin", () => {
|
|
32
|
+
const reg = new PluginRegistry();
|
|
33
|
+
expect(reg.get("unknown")).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
it("lists all registered plugins", () => {
|
|
36
|
+
const reg = new PluginRegistry();
|
|
37
|
+
reg.register(mockPlugin("evm"));
|
|
38
|
+
reg.register(mockPlugin("solana", "ed25519"));
|
|
39
|
+
expect(reg.list()).toHaveLength(2);
|
|
40
|
+
expect(reg
|
|
41
|
+
.list()
|
|
42
|
+
.map((p) => p.pluginId)
|
|
43
|
+
.sort()).toEqual(["evm", "solana"]);
|
|
44
|
+
});
|
|
45
|
+
it("getOrThrow throws for unknown plugin", () => {
|
|
46
|
+
const reg = new PluginRegistry();
|
|
47
|
+
expect(() => reg.getOrThrow("nope")).toThrow("not registered");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export type { DepositInfo, EncodingParams, IAddressEncoder, IChainPlugin, IChainWatcher, ICurveDeriver, IPriceOracle, ISweepStrategy, IWatcherCursorStore, KeyPair, PaymentEvent, SweeperOpts, SweepResult, WatcherOpts, } from "./interfaces.js";
|
|
2
|
+
export { PluginRegistry } from "./registry.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PluginRegistry } from "./registry.js";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export interface PaymentEvent {
|
|
2
|
+
chain: string;
|
|
3
|
+
token: string;
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
rawAmount: string;
|
|
7
|
+
amountUsdCents: number;
|
|
8
|
+
txHash: string;
|
|
9
|
+
blockNumber: number;
|
|
10
|
+
confirmations: number;
|
|
11
|
+
confirmationsRequired: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ICurveDeriver {
|
|
14
|
+
derivePublicKey(chainIndex: number, addressIndex: number): Uint8Array;
|
|
15
|
+
getCurve(): "secp256k1" | "ed25519";
|
|
16
|
+
}
|
|
17
|
+
export interface EncodingParams {
|
|
18
|
+
hrp?: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
[key: string]: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
export interface IAddressEncoder {
|
|
23
|
+
encode(publicKey: Uint8Array, params: EncodingParams): string;
|
|
24
|
+
encodingType(): string;
|
|
25
|
+
}
|
|
26
|
+
export interface KeyPair {
|
|
27
|
+
privateKey: Uint8Array;
|
|
28
|
+
publicKey: Uint8Array;
|
|
29
|
+
address: string;
|
|
30
|
+
index: number;
|
|
31
|
+
}
|
|
32
|
+
export interface DepositInfo {
|
|
33
|
+
index: number;
|
|
34
|
+
address: string;
|
|
35
|
+
nativeBalance: bigint;
|
|
36
|
+
tokenBalances: Array<{
|
|
37
|
+
token: string;
|
|
38
|
+
balance: bigint;
|
|
39
|
+
decimals: number;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
export interface SweepResult {
|
|
43
|
+
index: number;
|
|
44
|
+
address: string;
|
|
45
|
+
token: string;
|
|
46
|
+
amount: string;
|
|
47
|
+
txHash: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ISweepStrategy {
|
|
50
|
+
scan(keys: KeyPair[], treasury: string): Promise<DepositInfo[]>;
|
|
51
|
+
sweep(keys: KeyPair[], treasury: string, dryRun: boolean): Promise<SweepResult[]>;
|
|
52
|
+
}
|
|
53
|
+
export interface IPriceOracle {
|
|
54
|
+
getPrice(token: string, feedAddress?: string): Promise<{
|
|
55
|
+
priceMicros: number;
|
|
56
|
+
}>;
|
|
57
|
+
}
|
|
58
|
+
export interface IWatcherCursorStore {
|
|
59
|
+
get(watcherId: string): Promise<number | null>;
|
|
60
|
+
save(watcherId: string, cursor: number): Promise<void>;
|
|
61
|
+
getConfirmationCount(watcherId: string, txKey: string): Promise<number | null>;
|
|
62
|
+
saveConfirmationCount(watcherId: string, txKey: string, count: number): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
export interface WatcherOpts {
|
|
65
|
+
rpcUrl: string;
|
|
66
|
+
rpcHeaders: Record<string, string>;
|
|
67
|
+
oracle: IPriceOracle;
|
|
68
|
+
cursorStore: IWatcherCursorStore;
|
|
69
|
+
token: string;
|
|
70
|
+
chain: string;
|
|
71
|
+
contractAddress?: string;
|
|
72
|
+
decimals: number;
|
|
73
|
+
confirmations: number;
|
|
74
|
+
}
|
|
75
|
+
export interface SweeperOpts {
|
|
76
|
+
rpcUrl: string;
|
|
77
|
+
rpcHeaders: Record<string, string>;
|
|
78
|
+
token: string;
|
|
79
|
+
chain: string;
|
|
80
|
+
contractAddress?: string;
|
|
81
|
+
decimals: number;
|
|
82
|
+
}
|
|
83
|
+
export interface IChainWatcher {
|
|
84
|
+
init(): Promise<void>;
|
|
85
|
+
poll(): Promise<PaymentEvent[]>;
|
|
86
|
+
setWatchedAddresses(addresses: string[]): void;
|
|
87
|
+
getCursor(): number;
|
|
88
|
+
stop(): void;
|
|
89
|
+
}
|
|
90
|
+
export interface IChainPlugin {
|
|
91
|
+
pluginId: string;
|
|
92
|
+
supportedCurve: "secp256k1" | "ed25519";
|
|
93
|
+
encoders: Record<string, IAddressEncoder>;
|
|
94
|
+
createWatcher(opts: WatcherOpts): IChainWatcher;
|
|
95
|
+
createSweeper(opts: SweeperOpts): ISweepStrategy;
|
|
96
|
+
version: number;
|
|
97
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IChainPlugin } from "./interfaces.js";
|
|
2
|
+
export declare class PluginRegistry {
|
|
3
|
+
private plugins;
|
|
4
|
+
register(plugin: IChainPlugin): void;
|
|
5
|
+
get(pluginId: string): IChainPlugin | undefined;
|
|
6
|
+
getOrThrow(pluginId: string): IChainPlugin;
|
|
7
|
+
list(): IChainPlugin[];
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class PluginRegistry {
|
|
2
|
+
plugins = new Map();
|
|
3
|
+
register(plugin) {
|
|
4
|
+
if (this.plugins.has(plugin.pluginId)) {
|
|
5
|
+
throw new Error(`Plugin "${plugin.pluginId}" is already registered`);
|
|
6
|
+
}
|
|
7
|
+
this.plugins.set(plugin.pluginId, plugin);
|
|
8
|
+
}
|
|
9
|
+
get(pluginId) {
|
|
10
|
+
return this.plugins.get(pluginId);
|
|
11
|
+
}
|
|
12
|
+
getOrThrow(pluginId) {
|
|
13
|
+
const plugin = this.plugins.get(pluginId);
|
|
14
|
+
if (!plugin)
|
|
15
|
+
throw new Error(`Plugin "${pluginId}" is not registered`);
|
|
16
|
+
return plugin;
|
|
17
|
+
}
|
|
18
|
+
list() {
|
|
19
|
+
return [...this.plugins.values()];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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 type { DrizzleDb } from "../../db/index.js";
|
|
12
|
+
import type { ICryptoChargeRepository } from "./charge-store.js";
|
|
13
|
+
import type { IWatcherCursorStore } from "./cursor-store.js";
|
|
14
|
+
import type { IPriceOracle } from "./oracle/types.js";
|
|
15
|
+
import type { IPaymentMethodStore } from "./payment-method-store.js";
|
|
16
|
+
import type { PluginRegistry } from "./plugin/registry.js";
|
|
17
|
+
export interface PluginWatcherServiceOpts {
|
|
18
|
+
db: DrizzleDb;
|
|
19
|
+
chargeStore: ICryptoChargeRepository;
|
|
20
|
+
methodStore: IPaymentMethodStore;
|
|
21
|
+
cursorStore: IWatcherCursorStore;
|
|
22
|
+
oracle: IPriceOracle;
|
|
23
|
+
registry: PluginRegistry;
|
|
24
|
+
pollIntervalMs?: number;
|
|
25
|
+
log?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Boot plugin-driven watchers for all enabled payment methods.
|
|
29
|
+
*
|
|
30
|
+
* Returns a cleanup function that stops all poll timers and watchers.
|
|
31
|
+
*/
|
|
32
|
+
export declare function startPluginWatchers(opts: PluginWatcherServiceOpts): Promise<() => void>;
|