@wopr-network/platform-core 1.63.1 → 1.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/billing/crypto/__tests__/address-gen.test.js +191 -90
  2. package/dist/billing/crypto/__tests__/key-server.test.js +3 -0
  3. package/dist/billing/crypto/address-gen.js +32 -0
  4. package/dist/billing/crypto/evm/eth-watcher.js +52 -41
  5. package/dist/billing/crypto/evm/watcher.js +5 -11
  6. package/dist/billing/crypto/key-server-entry.js +8 -1
  7. package/dist/billing/crypto/key-server.js +19 -14
  8. package/dist/billing/crypto/oracle/coingecko.js +3 -0
  9. package/dist/billing/crypto/payment-method-store.d.ts +2 -0
  10. package/dist/billing/crypto/payment-method-store.js +5 -0
  11. package/dist/billing/crypto/tron/address-convert.js +15 -5
  12. package/dist/billing/crypto/watcher-service.js +9 -9
  13. package/dist/db/schema/crypto.d.ts +34 -0
  14. package/dist/db/schema/crypto.js +3 -1
  15. package/docs/superpowers/plans/2026-03-24-crypto-plugin-phase1.md +697 -0
  16. package/docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md +309 -0
  17. package/drizzle/migrations/0022_oracle_asset_id_column.sql +23 -0
  18. package/drizzle/migrations/0022_rpc_headers_column.sql +1 -0
  19. package/drizzle/migrations/meta/_journal.json +14 -0
  20. package/package.json +1 -1
  21. package/src/billing/crypto/__tests__/address-gen.test.ts +207 -88
  22. package/src/billing/crypto/__tests__/key-server.test.ts +3 -0
  23. package/src/billing/crypto/address-gen.ts +31 -0
  24. package/src/billing/crypto/evm/eth-watcher.ts +64 -47
  25. package/src/billing/crypto/evm/watcher.ts +8 -9
  26. package/src/billing/crypto/key-server-entry.ts +7 -1
  27. package/src/billing/crypto/key-server.ts +26 -19
  28. package/src/billing/crypto/oracle/coingecko.ts +3 -0
  29. package/src/billing/crypto/payment-method-store.ts +7 -0
  30. package/src/billing/crypto/tron/address-convert.ts +13 -4
  31. package/src/billing/crypto/watcher-service.ts +12 -11
  32. package/src/db/schema/crypto.ts +3 -1
@@ -317,12 +317,14 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
317
317
  decimals: number;
318
318
  xpub: string;
319
319
  rpc_url: string;
320
+ rpc_headers?: Record<string, string>;
320
321
  confirmations?: number;
321
322
  display_name?: string;
322
323
  oracle_address?: string;
323
324
  address_type?: string;
324
325
  encoding_params?: Record<string, string>;
325
326
  watcher_type?: string;
327
+ oracle_asset_id?: string;
326
328
  icon_url?: string;
327
329
  display_order?: number;
328
330
  }>();
@@ -331,24 +333,6 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
331
333
  return c.json({ error: "id, xpub, and token are required" }, 400);
332
334
  }
333
335
 
334
- // Record the path allocation (idempotent — ignore if already exists)
335
- const inserted = (await deps.db
336
- .insert(pathAllocations)
337
- .values({
338
- coinType: body.coin_type,
339
- accountIndex: body.account_index,
340
- chainId: body.id,
341
- xpub: body.xpub,
342
- })
343
- .onConflictDoNothing()) as { rowCount: number };
344
-
345
- if (inserted.rowCount === 0) {
346
- return c.json(
347
- { error: "Path allocation already exists", path: `m/44'/${body.coin_type}'/${body.account_index}'` },
348
- 409,
349
- );
350
- }
351
-
352
336
  // Validate encoding_params match address_type requirements
353
337
  const addrType = body.address_type ?? "evm";
354
338
  const encParams = body.encoding_params ?? {};
@@ -359,7 +343,7 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
359
343
  return c.json({ error: "p2pkh address_type requires encoding_params.version" }, 400);
360
344
  }
361
345
 
362
- // Upsert the payment method
346
+ // Upsert payment method FIRST (path_allocations has FK to payment_methods.id)
363
347
  await deps.methodStore.upsert({
364
348
  id: body.id,
365
349
  type: body.type ?? "native",
@@ -372,14 +356,37 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
372
356
  displayOrder: body.display_order ?? 0,
373
357
  iconUrl: body.icon_url ?? null,
374
358
  rpcUrl: body.rpc_url,
359
+ rpcHeaders: JSON.stringify(body.rpc_headers ?? {}),
375
360
  oracleAddress: body.oracle_address ?? null,
376
361
  xpub: body.xpub,
377
362
  addressType: body.address_type ?? "evm",
378
363
  encodingParams: JSON.stringify(body.encoding_params ?? {}),
379
364
  watcherType: body.watcher_type ?? "evm",
365
+ oracleAssetId: body.oracle_asset_id ?? null,
380
366
  confirmations: body.confirmations ?? 6,
381
367
  });
382
368
 
369
+ // Record the path allocation (idempotent — ignore if already exists)
370
+ const inserted = (await deps.db
371
+ .insert(pathAllocations)
372
+ .values({
373
+ coinType: body.coin_type,
374
+ accountIndex: body.account_index,
375
+ chainId: body.id,
376
+ xpub: body.xpub,
377
+ })
378
+ .onConflictDoNothing()) as { rowCount: number };
379
+
380
+ if (inserted.rowCount === 0) {
381
+ return c.json(
382
+ {
383
+ message: "Path allocation already exists, payment method updated",
384
+ path: `m/44'/${body.coin_type}'/${body.account_index}'`,
385
+ },
386
+ 200,
387
+ );
388
+ }
389
+
383
390
  return c.json({ id: body.id, path: `m/44'/${body.coin_type}'/${body.account_index}'` }, 201);
384
391
  });
385
392
 
@@ -15,6 +15,9 @@ const COINGECKO_IDS: Record<string, string> = {
15
15
  UNI: "uniswap",
16
16
  AERO: "aerodrome-finance",
17
17
  TRX: "tron",
18
+ BNB: "binancecoin",
19
+ POL: "matic-network",
20
+ AVAX: "avalanche-2",
18
21
  };
19
22
 
20
23
  /** Default cache TTL: 60 seconds. CoinGecko free tier allows 10-30 req/min. */
@@ -14,11 +14,13 @@ export interface PaymentMethodRecord {
14
14
  displayOrder: number;
15
15
  iconUrl: string | null;
16
16
  rpcUrl: string | null;
17
+ rpcHeaders: string;
17
18
  oracleAddress: string | null;
18
19
  xpub: string | null;
19
20
  addressType: string;
20
21
  encodingParams: string;
21
22
  watcherType: string;
23
+ oracleAssetId: string | null;
22
24
  confirmations: number;
23
25
  }
24
26
 
@@ -88,11 +90,13 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
88
90
  displayOrder: method.displayOrder,
89
91
  iconUrl: method.iconUrl,
90
92
  rpcUrl: method.rpcUrl,
93
+ rpcHeaders: method.rpcHeaders ?? "{}",
91
94
  oracleAddress: method.oracleAddress,
92
95
  xpub: method.xpub,
93
96
  addressType: method.addressType,
94
97
  encodingParams: method.encodingParams,
95
98
  watcherType: method.watcherType,
99
+ oracleAssetId: method.oracleAssetId,
96
100
  confirmations: method.confirmations,
97
101
  })
98
102
  .onConflictDoUpdate({
@@ -113,6 +117,7 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
113
117
  addressType: method.addressType,
114
118
  encodingParams: method.encodingParams,
115
119
  watcherType: method.watcherType,
120
+ oracleAssetId: method.oracleAssetId,
116
121
  confirmations: method.confirmations,
117
122
  },
118
123
  });
@@ -151,11 +156,13 @@ function toRecord(row: typeof paymentMethods.$inferSelect): PaymentMethodRecord
151
156
  displayOrder: row.displayOrder,
152
157
  iconUrl: row.iconUrl,
153
158
  rpcUrl: row.rpcUrl,
159
+ rpcHeaders: row.rpcHeaders ?? "{}",
154
160
  oracleAddress: row.oracleAddress,
155
161
  xpub: row.xpub,
156
162
  addressType: row.addressType,
157
163
  encodingParams: row.encodingParams,
158
164
  watcherType: row.watcherType,
165
+ oracleAssetId: row.oracleAssetId,
159
166
  confirmations: row.confirmations,
160
167
  };
161
168
  }
@@ -10,16 +10,25 @@ import { sha256 } from "@noble/hashes/sha2.js";
10
10
  const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
11
11
 
12
12
  function base58decode(s: string): Uint8Array {
13
+ // Count leading '1' characters (each represents a 0x00 byte)
14
+ let leadingZeros = 0;
15
+ for (const ch of s) {
16
+ if (ch !== "1") break;
17
+ leadingZeros++;
18
+ }
13
19
  let num = 0n;
14
20
  for (const ch of s) {
15
21
  const idx = BASE58_ALPHABET.indexOf(ch);
16
22
  if (idx < 0) throw new Error(`Invalid base58 character: ${ch}`);
17
23
  num = num * 58n + BigInt(idx);
18
24
  }
19
- const hex = num.toString(16).padStart(50, "0"); // 25 bytes = 50 hex chars
20
- const bytes = new Uint8Array(25);
21
- for (let i = 0; i < 25; i++) bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
22
- return bytes;
25
+ const hex = num.toString(16).padStart(2, "0");
26
+ const dataBytes = new Uint8Array(hex.length / 2);
27
+ for (let i = 0; i < dataBytes.length; i++) dataBytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
28
+ // Prepend leading zero bytes
29
+ const result = new Uint8Array(leadingZeros + dataBytes.length);
30
+ result.set(dataBytes, leadingZeros);
31
+ return result;
23
32
  }
24
33
 
25
34
  /**
@@ -360,19 +360,20 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
360
360
 
361
361
  const BACKFILL_BLOCKS = 1000; // Scan ~30min of blocks on first deploy to catch missed deposits
362
362
 
363
- // Address conversion helpers for chains with non-EVM address formats (e.g. Tron T...).
364
- // The EVM watcher uses 0x hex addresses; the DB stores native format (T... for Tron).
365
- // Determined by addressType from the DBnot by inspecting addresses at runtime.
366
- const needsAddrConvert = (method: { addressType: string }): boolean => method.addressType === "p2pkh";
367
- const toWatcherAddr = (addr: string, method: { addressType: string }): string =>
368
- needsAddrConvert(method) && isTronAddress(addr) ? tronToHex(addr) : addr;
369
- const fromWatcherAddr = (addr: string, method: { addressType: string }): string =>
370
- needsAddrConvert(method) ? hexToTron(addr) : addr;
363
+ // Address conversion for EVM-watched chains with non-0x address formats (Tron T...).
364
+ // Only applies to chains routed through the EVM watcher but storing non-hex addresses.
365
+ // UTXO chains (DOGE p2pkh) never enter this path they use the UTXO watcher.
366
+ const isTronMethod = (method: { addressType: string; chain: string }): boolean =>
367
+ (method.addressType === "p2pkh" || method.addressType === "keccak-b58check") && method.chain === "tron";
368
+ const toWatcherAddr = (addr: string, method: { addressType: string; chain: string }): string =>
369
+ isTronMethod(method) && isTronAddress(addr) ? tronToHex(addr) : addr;
370
+ const fromWatcherAddr = (addr: string, method: { addressType: string; chain: string }): string =>
371
+ isTronMethod(method) ? hexToTron(addr) : addr;
371
372
 
372
373
  for (const method of nativeEvmMethods) {
373
374
  if (!method.rpcUrl) continue;
374
375
 
375
- const rpcCall = createRpcCaller(method.rpcUrl);
376
+ const rpcCall = createRpcCaller(method.rpcUrl, JSON.parse(method.rpcHeaders ?? "{}"));
376
377
  let latestBlock: number;
377
378
  try {
378
379
  const latestHex = (await rpcCall("eth_blockNumber", [])) as string;
@@ -451,7 +452,7 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
451
452
  for (const method of erc20Methods) {
452
453
  if (!method.rpcUrl || !method.contractAddress) continue;
453
454
 
454
- const rpcCall = createRpcCaller(method.rpcUrl);
455
+ const rpcCall = createRpcCaller(method.rpcUrl, JSON.parse(method.rpcHeaders ?? "{}"));
455
456
  let latestBlock: number;
456
457
  try {
457
458
  const latestHex = (await rpcCall("eth_blockNumber", [])) as string;
@@ -470,7 +471,7 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
470
471
  rpcCall,
471
472
  fromBlock: latestBlock,
472
473
  watchedAddresses: chainAddresses.map((a) => toWatcherAddr(a, method)),
473
- contractAddress: method.contractAddress,
474
+ contractAddress: toWatcherAddr(method.contractAddress, method),
474
475
  decimals: method.decimals,
475
476
  confirmations: method.confirmations,
476
477
  cursorStore,
@@ -80,11 +80,13 @@ export const paymentMethods = pgTable("payment_methods", {
80
80
  displayOrder: integer("display_order").notNull().default(0),
81
81
  iconUrl: text("icon_url"),
82
82
  rpcUrl: text("rpc_url"), // chain node RPC endpoint
83
+ rpcHeaders: text("rpc_headers").notNull().default("{}"), // JSON: extra headers for RPC calls (e.g. {"TRON-PRO-API-KEY":"xxx"})
83
84
  oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
84
85
  xpub: text("xpub"), // HD wallet extended public key for deposit address derivation
85
- addressType: text("address_type").notNull().default("evm"), // "bech32" (BTC/LTC), "p2pkh" (DOGE), "evm" (ETH/ERC20)
86
+ addressType: text("address_type").notNull().default("evm"), // "bech32" (BTC/LTC), "p2pkh" (DOGE/TRX), "evm" (ETH/ERC20)
86
87
  encodingParams: text("encoding_params").notNull().default("{}"), // JSON: {"hrp":"bc"}, {"version":"0x1e"}, etc.
87
88
  watcherType: text("watcher_type").notNull().default("evm"), // "utxo" (BTC/LTC/DOGE) or "evm" (ETH/ERC20/TRX)
89
+ oracleAssetId: text("oracle_asset_id"), // CoinGecko slug (e.g. "bitcoin", "tron"). Null = stablecoin (1:1 USD) or use token symbol fallback.
88
90
  confirmations: integer("confirmations").notNull().default(1),
89
91
  nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
90
92
  createdAt: text("created_at").notNull().default(sql`(now())`),