@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.
- package/dist/billing/crypto/__tests__/address-gen.test.js +191 -90
- package/dist/billing/crypto/__tests__/key-server.test.js +3 -0
- package/dist/billing/crypto/address-gen.js +32 -0
- package/dist/billing/crypto/evm/eth-watcher.js +52 -41
- package/dist/billing/crypto/evm/watcher.js +5 -11
- package/dist/billing/crypto/key-server-entry.js +8 -1
- package/dist/billing/crypto/key-server.js +19 -14
- package/dist/billing/crypto/oracle/coingecko.js +3 -0
- package/dist/billing/crypto/payment-method-store.d.ts +2 -0
- package/dist/billing/crypto/payment-method-store.js +5 -0
- package/dist/billing/crypto/tron/address-convert.js +15 -5
- package/dist/billing/crypto/watcher-service.js +9 -9
- package/dist/db/schema/crypto.d.ts +34 -0
- package/dist/db/schema/crypto.js +3 -1
- package/docs/superpowers/plans/2026-03-24-crypto-plugin-phase1.md +697 -0
- package/docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md +309 -0
- package/drizzle/migrations/0022_oracle_asset_id_column.sql +23 -0
- package/drizzle/migrations/0022_rpc_headers_column.sql +1 -0
- package/drizzle/migrations/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/billing/crypto/__tests__/address-gen.test.ts +207 -88
- package/src/billing/crypto/__tests__/key-server.test.ts +3 -0
- package/src/billing/crypto/address-gen.ts +31 -0
- package/src/billing/crypto/evm/eth-watcher.ts +64 -47
- package/src/billing/crypto/evm/watcher.ts +8 -9
- package/src/billing/crypto/key-server-entry.ts +7 -1
- package/src/billing/crypto/key-server.ts +26 -19
- package/src/billing/crypto/oracle/coingecko.ts +3 -0
- package/src/billing/crypto/payment-method-store.ts +7 -0
- package/src/billing/crypto/tron/address-convert.ts +13 -4
- package/src/billing/crypto/watcher-service.ts +12 -11
- 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
|
|
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(
|
|
20
|
-
const
|
|
21
|
-
for (let i = 0; i <
|
|
22
|
-
|
|
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
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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,
|
package/src/db/schema/crypto.ts
CHANGED
|
@@ -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())`),
|