@wopr-network/platform-core 1.63.2 → 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 (47) hide show
  1. package/dist/billing/crypto/__tests__/address-gen.test.js +191 -90
  2. package/dist/billing/crypto/address-gen.js +32 -0
  3. package/dist/billing/crypto/evm/eth-watcher.js +52 -41
  4. package/dist/billing/crypto/evm/watcher.js +5 -11
  5. package/dist/billing/crypto/index.d.ts +2 -0
  6. package/dist/billing/crypto/index.js +1 -0
  7. package/dist/billing/crypto/key-server.js +4 -0
  8. package/dist/billing/crypto/payment-method-store.d.ts +4 -0
  9. package/dist/billing/crypto/payment-method-store.js +11 -0
  10. package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +1 -0
  11. package/dist/billing/crypto/plugin/__tests__/integration.test.js +58 -0
  12. package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +1 -0
  13. package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +46 -0
  14. package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +1 -0
  15. package/dist/billing/crypto/plugin/__tests__/registry.test.js +49 -0
  16. package/dist/billing/crypto/plugin/index.d.ts +2 -0
  17. package/dist/billing/crypto/plugin/index.js +1 -0
  18. package/dist/billing/crypto/plugin/interfaces.d.ts +97 -0
  19. package/dist/billing/crypto/plugin/interfaces.js +2 -0
  20. package/dist/billing/crypto/plugin/registry.d.ts +8 -0
  21. package/dist/billing/crypto/plugin/registry.js +21 -0
  22. package/dist/billing/crypto/watcher-service.js +4 -4
  23. package/dist/db/schema/crypto.d.ts +345 -0
  24. package/dist/db/schema/crypto.js +34 -1
  25. package/dist/db/schema/snapshots.d.ts +1 -1
  26. package/docs/superpowers/plans/2026-03-24-crypto-plugin-phase1.md +697 -0
  27. package/docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md +309 -0
  28. package/drizzle/migrations/0022_rpc_headers_column.sql +1 -0
  29. package/drizzle/migrations/0023_key_rings_table.sql +35 -0
  30. package/drizzle/migrations/0024_backfill_key_rings.sql +75 -0
  31. package/drizzle/migrations/meta/_journal.json +14 -0
  32. package/package.json +5 -1
  33. package/src/billing/crypto/__tests__/address-gen.test.ts +207 -88
  34. package/src/billing/crypto/address-gen.ts +31 -0
  35. package/src/billing/crypto/evm/eth-watcher.ts +64 -47
  36. package/src/billing/crypto/evm/watcher.ts +8 -9
  37. package/src/billing/crypto/index.ts +9 -0
  38. package/src/billing/crypto/key-server.ts +5 -0
  39. package/src/billing/crypto/payment-method-store.ts +15 -0
  40. package/src/billing/crypto/plugin/__tests__/integration.test.ts +64 -0
  41. package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +51 -0
  42. package/src/billing/crypto/plugin/__tests__/registry.test.ts +58 -0
  43. package/src/billing/crypto/plugin/index.ts +17 -0
  44. package/src/billing/crypto/plugin/interfaces.ts +106 -0
  45. package/src/billing/crypto/plugin/registry.ts +26 -0
  46. package/src/billing/crypto/watcher-service.ts +4 -4
  47. package/src/db/schema/crypto.ts +44 -1
@@ -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,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,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,2 @@
1
+ // src/billing/crypto/plugin/interfaces.ts
2
+ export {};
@@ -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
+ }
@@ -277,13 +277,13 @@ export async function startWatchers(opts) {
277
277
  // Address conversion for EVM-watched chains with non-0x address formats (Tron T...).
278
278
  // Only applies to chains routed through the EVM watcher but storing non-hex addresses.
279
279
  // UTXO chains (DOGE p2pkh) never enter this path — they use the UTXO watcher.
280
- const isTronMethod = (method) => method.addressType === "p2pkh" && method.chain === "tron";
280
+ const isTronMethod = (method) => (method.addressType === "p2pkh" || method.addressType === "keccak-b58check") && method.chain === "tron";
281
281
  const toWatcherAddr = (addr, method) => isTronMethod(method) && isTronAddress(addr) ? tronToHex(addr) : addr;
282
282
  const fromWatcherAddr = (addr, method) => isTronMethod(method) ? hexToTron(addr) : addr;
283
283
  for (const method of nativeEvmMethods) {
284
284
  if (!method.rpcUrl)
285
285
  continue;
286
- const rpcCall = createRpcCaller(method.rpcUrl);
286
+ const rpcCall = createRpcCaller(method.rpcUrl, JSON.parse(method.rpcHeaders ?? "{}"));
287
287
  let latestBlock;
288
288
  try {
289
289
  const latestHex = (await rpcCall("eth_blockNumber", []));
@@ -352,7 +352,7 @@ export async function startWatchers(opts) {
352
352
  for (const method of erc20Methods) {
353
353
  if (!method.rpcUrl || !method.contractAddress)
354
354
  continue;
355
- const rpcCall = createRpcCaller(method.rpcUrl);
355
+ const rpcCall = createRpcCaller(method.rpcUrl, JSON.parse(method.rpcHeaders ?? "{}"));
356
356
  let latestBlock;
357
357
  try {
358
358
  const latestHex = (await rpcCall("eth_blockNumber", []));
@@ -370,7 +370,7 @@ export async function startWatchers(opts) {
370
370
  rpcCall,
371
371
  fromBlock: latestBlock,
372
372
  watchedAddresses: chainAddresses.map((a) => toWatcherAddr(a, method)),
373
- contractAddress: method.contractAddress,
373
+ contractAddress: toWatcherAddr(method.contractAddress, method),
374
374
  decimals: method.decimals,
375
375
  confirmations: method.confirmations,
376
376
  cursorStore,