@wopr-network/platform-core 1.63.0 → 1.63.2

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.
@@ -13,6 +13,7 @@ function createMockDb() {
13
13
  addressType: "bech32",
14
14
  encodingParams: '{"hrp":"bc"}',
15
15
  watcherType: "utxo",
16
+ oracleAssetId: "bitcoin",
16
17
  confirmations: 6,
17
18
  };
18
19
  const db = {
@@ -182,6 +183,7 @@ describe("key-server routes", () => {
182
183
  addressType: "evm",
183
184
  encodingParams: "{}",
184
185
  watcherType: "evm",
186
+ oracleAssetId: "ethereum",
185
187
  confirmations: 1,
186
188
  };
187
189
  const db = {
@@ -242,6 +244,7 @@ describe("key-server routes", () => {
242
244
  addressType: "evm",
243
245
  encodingParams: "{}",
244
246
  watcherType: "evm",
247
+ oracleAssetId: "ethereum",
245
248
  confirmations: 1,
246
249
  };
247
250
  const db = {
@@ -47,5 +47,5 @@ export declare class EvmWatcher {
47
47
  poll(): Promise<void>;
48
48
  }
49
49
  /** Create an RPC caller for a given URL (plain JSON-RPC over fetch). */
50
- export declare function createRpcCaller(rpcUrl: string): RpcCall;
50
+ export declare function createRpcCaller(rpcUrl: string, extraHeaders?: Record<string, string>): RpcCall;
51
51
  export {};
@@ -133,12 +133,23 @@ export class EvmWatcher {
133
133
  }
134
134
  }
135
135
  /** Create an RPC caller for a given URL (plain JSON-RPC over fetch). */
136
- export function createRpcCaller(rpcUrl) {
136
+ export function createRpcCaller(rpcUrl, extraHeaders) {
137
137
  let id = 0;
138
+ // Extract apikey query param and pass as TRON-PRO-API-KEY header (TronGrid JSON-RPC ignores query params)
139
+ const headers = { "Content-Type": "application/json", ...extraHeaders };
140
+ try {
141
+ const url = new URL(rpcUrl);
142
+ const apiKey = url.searchParams.get("apikey");
143
+ if (apiKey)
144
+ headers["TRON-PRO-API-KEY"] = apiKey;
145
+ }
146
+ catch {
147
+ // Not a valid URL — proceed without extra headers
148
+ }
138
149
  return async (method, params) => {
139
150
  const res = await fetch(rpcUrl, {
140
151
  method: "POST",
141
- headers: { "Content-Type": "application/json" },
152
+ headers,
142
153
  body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }),
143
154
  });
144
155
  if (!res.ok)
@@ -48,7 +48,14 @@ async function main() {
48
48
  const chainlink = BASE_RPC_URL
49
49
  ? new ChainlinkOracle({ rpcCall: createRpcCaller(BASE_RPC_URL) })
50
50
  : new FixedPriceOracle();
51
- const coingecko = new CoinGeckoOracle();
51
+ // Build token→CoinGecko ID map from DB (zero-deploy chain additions)
52
+ const allMethods = await methodStore.listAll();
53
+ const dbTokenIds = {};
54
+ for (const m of allMethods) {
55
+ if (m.oracleAssetId)
56
+ dbTokenIds[m.token] = m.oracleAssetId;
57
+ }
58
+ const coingecko = new CoinGeckoOracle({ tokenIds: dbTokenIds });
52
59
  const oracle = new CompositeOracle(chainlink, coingecko);
53
60
  const app = createKeyServerApp({
54
61
  db,
@@ -257,19 +257,6 @@ export function createKeyServerApp(deps) {
257
257
  if (!body.id || !body.xpub || !body.token) {
258
258
  return c.json({ error: "id, xpub, and token are required" }, 400);
259
259
  }
260
- // Record the path allocation (idempotent — ignore if already exists)
261
- const inserted = (await deps.db
262
- .insert(pathAllocations)
263
- .values({
264
- coinType: body.coin_type,
265
- accountIndex: body.account_index,
266
- chainId: body.id,
267
- xpub: body.xpub,
268
- })
269
- .onConflictDoNothing());
270
- if (inserted.rowCount === 0) {
271
- return c.json({ error: "Path allocation already exists", path: `m/44'/${body.coin_type}'/${body.account_index}'` }, 409);
272
- }
273
260
  // Validate encoding_params match address_type requirements
274
261
  const addrType = body.address_type ?? "evm";
275
262
  const encParams = body.encoding_params ?? {};
@@ -279,7 +266,7 @@ export function createKeyServerApp(deps) {
279
266
  if (addrType === "p2pkh" && !encParams.version) {
280
267
  return c.json({ error: "p2pkh address_type requires encoding_params.version" }, 400);
281
268
  }
282
- // Upsert the payment method
269
+ // Upsert payment method FIRST (path_allocations has FK to payment_methods.id)
283
270
  await deps.methodStore.upsert({
284
271
  id: body.id,
285
272
  type: body.type ?? "native",
@@ -297,8 +284,25 @@ export function createKeyServerApp(deps) {
297
284
  addressType: body.address_type ?? "evm",
298
285
  encodingParams: JSON.stringify(body.encoding_params ?? {}),
299
286
  watcherType: body.watcher_type ?? "evm",
287
+ oracleAssetId: body.oracle_asset_id ?? null,
300
288
  confirmations: body.confirmations ?? 6,
301
289
  });
290
+ // Record the path allocation (idempotent — ignore if already exists)
291
+ const inserted = (await deps.db
292
+ .insert(pathAllocations)
293
+ .values({
294
+ coinType: body.coin_type,
295
+ accountIndex: body.account_index,
296
+ chainId: body.id,
297
+ xpub: body.xpub,
298
+ })
299
+ .onConflictDoNothing());
300
+ if (inserted.rowCount === 0) {
301
+ return c.json({
302
+ message: "Path allocation already exists, payment method updated",
303
+ path: `m/44'/${body.coin_type}'/${body.account_index}'`,
304
+ }, 200);
305
+ }
302
306
  return c.json({ id: body.id, path: `m/44'/${body.coin_type}'/${body.account_index}'` }, 201);
303
307
  });
304
308
  /** PATCH /admin/chains/:id — update metadata (icon_url, display_order, display_name) */
@@ -13,6 +13,9 @@ const COINGECKO_IDS = {
13
13
  UNI: "uniswap",
14
14
  AERO: "aerodrome-finance",
15
15
  TRX: "tron",
16
+ BNB: "binancecoin",
17
+ POL: "matic-network",
18
+ AVAX: "avalanche-2",
16
19
  };
17
20
  /** Default cache TTL: 60 seconds. CoinGecko free tier allows 10-30 req/min. */
18
21
  const DEFAULT_CACHE_TTL_MS = 60_000;
@@ -16,6 +16,7 @@ export interface PaymentMethodRecord {
16
16
  addressType: string;
17
17
  encodingParams: string;
18
18
  watcherType: string;
19
+ oracleAssetId: string | null;
19
20
  confirmations: number;
20
21
  }
21
22
  export interface IPaymentMethodStore {
@@ -49,6 +49,7 @@ export class DrizzlePaymentMethodStore {
49
49
  addressType: method.addressType,
50
50
  encodingParams: method.encodingParams,
51
51
  watcherType: method.watcherType,
52
+ oracleAssetId: method.oracleAssetId,
52
53
  confirmations: method.confirmations,
53
54
  })
54
55
  .onConflictDoUpdate({
@@ -69,6 +70,7 @@ export class DrizzlePaymentMethodStore {
69
70
  addressType: method.addressType,
70
71
  encodingParams: method.encodingParams,
71
72
  watcherType: method.watcherType,
73
+ oracleAssetId: method.oracleAssetId,
72
74
  confirmations: method.confirmations,
73
75
  },
74
76
  });
@@ -108,6 +110,7 @@ function toRecord(row) {
108
110
  addressType: row.addressType,
109
111
  encodingParams: row.encodingParams,
110
112
  watcherType: row.watcherType,
113
+ oracleAssetId: row.oracleAssetId,
111
114
  confirmations: row.confirmations,
112
115
  };
113
116
  }
@@ -8,6 +8,13 @@
8
8
  import { sha256 } from "@noble/hashes/sha2.js";
9
9
  const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
10
10
  function base58decode(s) {
11
+ // Count leading '1' characters (each represents a 0x00 byte)
12
+ let leadingZeros = 0;
13
+ for (const ch of s) {
14
+ if (ch !== "1")
15
+ break;
16
+ leadingZeros++;
17
+ }
11
18
  let num = 0n;
12
19
  for (const ch of s) {
13
20
  const idx = BASE58_ALPHABET.indexOf(ch);
@@ -15,11 +22,14 @@ function base58decode(s) {
15
22
  throw new Error(`Invalid base58 character: ${ch}`);
16
23
  num = num * 58n + BigInt(idx);
17
24
  }
18
- const hex = num.toString(16).padStart(50, "0"); // 25 bytes = 50 hex chars
19
- const bytes = new Uint8Array(25);
20
- for (let i = 0; i < 25; i++)
21
- 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++)
28
+ dataBytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
29
+ // Prepend leading zero bytes
30
+ const result = new Uint8Array(leadingZeros + dataBytes.length);
31
+ result.set(dataBytes, leadingZeros);
32
+ return result;
23
33
  }
24
34
  /**
25
35
  * Convert a Tron T... address to 0x hex (20 bytes, no 0x41 prefix).
@@ -274,12 +274,12 @@ export async function startWatchers(opts) {
274
274
  const nativeEvmMethods = evmMethods.filter((m) => m.type === "native");
275
275
  const erc20Methods = evmMethods.filter((m) => m.type === "erc20" && m.contractAddress);
276
276
  const BACKFILL_BLOCKS = 1000; // Scan ~30min of blocks on first deploy to catch missed deposits
277
- // Address conversion helpers for chains with non-EVM address formats (e.g. Tron T...).
278
- // The EVM watcher uses 0x hex addresses; the DB stores native format (T... for Tron).
279
- // Determined by addressType from the DBnot by inspecting addresses at runtime.
280
- const needsAddrConvert = (method) => method.addressType === "p2pkh";
281
- const toWatcherAddr = (addr, method) => needsAddrConvert(method) && isTronAddress(addr) ? tronToHex(addr) : addr;
282
- const fromWatcherAddr = (addr, method) => needsAddrConvert(method) ? hexToTron(addr) : addr;
277
+ // Address conversion for EVM-watched chains with non-0x address formats (Tron T...).
278
+ // Only applies to chains routed through the EVM watcher but storing non-hex addresses.
279
+ // UTXO chains (DOGE p2pkh) never enter this path they use the UTXO watcher.
280
+ const isTronMethod = (method) => method.addressType === "p2pkh" && method.chain === "tron";
281
+ const toWatcherAddr = (addr, method) => isTronMethod(method) && isTronAddress(addr) ? tronToHex(addr) : addr;
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;
@@ -716,6 +716,23 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
716
716
  identity: undefined;
717
717
  generated: undefined;
718
718
  }, {}, {}>;
719
+ oracleAssetId: import("drizzle-orm/pg-core").PgColumn<{
720
+ name: "oracle_asset_id";
721
+ tableName: "payment_methods";
722
+ dataType: "string";
723
+ columnType: "PgText";
724
+ data: string;
725
+ driverParam: string;
726
+ notNull: false;
727
+ hasDefault: false;
728
+ isPrimaryKey: false;
729
+ isAutoincrement: false;
730
+ hasRuntimeDefault: false;
731
+ enumValues: [string, ...string[]];
732
+ baseColumn: never;
733
+ identity: undefined;
734
+ generated: undefined;
735
+ }, {}, {}>;
719
736
  confirmations: import("drizzle-orm/pg-core").PgColumn<{
720
737
  name: "confirmations";
721
738
  tableName: "payment_methods";
@@ -75,9 +75,10 @@ export const paymentMethods = pgTable("payment_methods", {
75
75
  rpcUrl: text("rpc_url"), // chain node RPC endpoint
76
76
  oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
77
77
  xpub: text("xpub"), // HD wallet extended public key for deposit address derivation
78
- addressType: text("address_type").notNull().default("evm"), // "bech32" (BTC/LTC), "p2pkh" (DOGE), "evm" (ETH/ERC20)
78
+ addressType: text("address_type").notNull().default("evm"), // "bech32" (BTC/LTC), "p2pkh" (DOGE/TRX), "evm" (ETH/ERC20)
79
79
  encodingParams: text("encoding_params").notNull().default("{}"), // JSON: {"hrp":"bc"}, {"version":"0x1e"}, etc.
80
80
  watcherType: text("watcher_type").notNull().default("evm"), // "utxo" (BTC/LTC/DOGE) or "evm" (ETH/ERC20/TRX)
81
+ oracleAssetId: text("oracle_asset_id"), // CoinGecko slug (e.g. "bitcoin", "tron"). Null = stablecoin (1:1 USD) or use token symbol fallback.
81
82
  confirmations: integer("confirmations").notNull().default(1),
82
83
  nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
83
84
  createdAt: text("created_at").notNull().default(sql `(now())`),
@@ -0,0 +1,23 @@
1
+ ALTER TABLE "payment_methods" ADD COLUMN "oracle_asset_id" text;
2
+ --> statement-breakpoint
3
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'bitcoin' WHERE "token" = 'BTC';
4
+ --> statement-breakpoint
5
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'ethereum' WHERE "token" = 'ETH';
6
+ --> statement-breakpoint
7
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'dogecoin' WHERE "token" = 'DOGE';
8
+ --> statement-breakpoint
9
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'litecoin' WHERE "token" = 'LTC';
10
+ --> statement-breakpoint
11
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'tron' WHERE "token" = 'TRX';
12
+ --> statement-breakpoint
13
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'binancecoin' WHERE "token" = 'BNB';
14
+ --> statement-breakpoint
15
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'matic-network' WHERE "token" = 'POL';
16
+ --> statement-breakpoint
17
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'avalanche-2' WHERE "token" = 'AVAX';
18
+ --> statement-breakpoint
19
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'chainlink' WHERE "token" = 'LINK';
20
+ --> statement-breakpoint
21
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'uniswap' WHERE "token" = 'UNI';
22
+ --> statement-breakpoint
23
+ UPDATE "payment_methods" SET "oracle_asset_id" = 'aerodrome-finance' WHERE "token" = 'AERO';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.63.0",
3
+ "version": "1.63.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,6 +17,7 @@ function createMockDb() {
17
17
  addressType: "bech32",
18
18
  encodingParams: '{"hrp":"bc"}',
19
19
  watcherType: "utxo",
20
+ oracleAssetId: "bitcoin",
20
21
  confirmations: 6,
21
22
  };
22
23
 
@@ -199,6 +200,7 @@ describe("key-server routes", () => {
199
200
  addressType: "evm",
200
201
  encodingParams: "{}",
201
202
  watcherType: "evm",
203
+ oracleAssetId: "ethereum",
202
204
  confirmations: 1,
203
205
  };
204
206
 
@@ -264,6 +266,7 @@ describe("key-server routes", () => {
264
266
  addressType: "evm",
265
267
  encodingParams: "{}",
266
268
  watcherType: "evm",
269
+ oracleAssetId: "ethereum",
267
270
  confirmations: 1,
268
271
  };
269
272
 
@@ -180,12 +180,21 @@ export class EvmWatcher {
180
180
  }
181
181
 
182
182
  /** Create an RPC caller for a given URL (plain JSON-RPC over fetch). */
183
- export function createRpcCaller(rpcUrl: string): RpcCall {
183
+ export function createRpcCaller(rpcUrl: string, extraHeaders?: Record<string, string>): RpcCall {
184
184
  let id = 0;
185
+ // Extract apikey query param and pass as TRON-PRO-API-KEY header (TronGrid JSON-RPC ignores query params)
186
+ const headers: Record<string, string> = { "Content-Type": "application/json", ...extraHeaders };
187
+ try {
188
+ const url = new URL(rpcUrl);
189
+ const apiKey = url.searchParams.get("apikey");
190
+ if (apiKey) headers["TRON-PRO-API-KEY"] = apiKey;
191
+ } catch {
192
+ // Not a valid URL — proceed without extra headers
193
+ }
185
194
  return async (method: string, params: unknown[]): Promise<unknown> => {
186
195
  const res = await fetch(rpcUrl, {
187
196
  method: "POST",
188
- headers: { "Content-Type": "application/json" },
197
+ headers,
189
198
  body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }),
190
199
  });
191
200
  if (!res.ok) throw new Error(`RPC ${method} failed: ${res.status}`);
@@ -55,7 +55,13 @@ async function main(): Promise<void> {
55
55
  const chainlink = BASE_RPC_URL
56
56
  ? new ChainlinkOracle({ rpcCall: createRpcCaller(BASE_RPC_URL) })
57
57
  : new FixedPriceOracle();
58
- const coingecko = new CoinGeckoOracle();
58
+ // Build token→CoinGecko ID map from DB (zero-deploy chain additions)
59
+ const allMethods = await methodStore.listAll();
60
+ const dbTokenIds: Record<string, string> = {};
61
+ for (const m of allMethods) {
62
+ if (m.oracleAssetId) dbTokenIds[m.token] = m.oracleAssetId;
63
+ }
64
+ const coingecko = new CoinGeckoOracle({ tokenIds: dbTokenIds });
59
65
  const oracle = new CompositeOracle(chainlink, coingecko);
60
66
 
61
67
  const app = createKeyServerApp({
@@ -323,6 +323,7 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
323
323
  address_type?: string;
324
324
  encoding_params?: Record<string, string>;
325
325
  watcher_type?: string;
326
+ oracle_asset_id?: string;
326
327
  icon_url?: string;
327
328
  display_order?: number;
328
329
  }>();
@@ -331,24 +332,6 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
331
332
  return c.json({ error: "id, xpub, and token are required" }, 400);
332
333
  }
333
334
 
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
335
  // Validate encoding_params match address_type requirements
353
336
  const addrType = body.address_type ?? "evm";
354
337
  const encParams = body.encoding_params ?? {};
@@ -359,7 +342,7 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
359
342
  return c.json({ error: "p2pkh address_type requires encoding_params.version" }, 400);
360
343
  }
361
344
 
362
- // Upsert the payment method
345
+ // Upsert payment method FIRST (path_allocations has FK to payment_methods.id)
363
346
  await deps.methodStore.upsert({
364
347
  id: body.id,
365
348
  type: body.type ?? "native",
@@ -377,9 +360,31 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
377
360
  addressType: body.address_type ?? "evm",
378
361
  encodingParams: JSON.stringify(body.encoding_params ?? {}),
379
362
  watcherType: body.watcher_type ?? "evm",
363
+ oracleAssetId: body.oracle_asset_id ?? null,
380
364
  confirmations: body.confirmations ?? 6,
381
365
  });
382
366
 
367
+ // Record the path allocation (idempotent — ignore if already exists)
368
+ const inserted = (await deps.db
369
+ .insert(pathAllocations)
370
+ .values({
371
+ coinType: body.coin_type,
372
+ accountIndex: body.account_index,
373
+ chainId: body.id,
374
+ xpub: body.xpub,
375
+ })
376
+ .onConflictDoNothing()) as { rowCount: number };
377
+
378
+ if (inserted.rowCount === 0) {
379
+ return c.json(
380
+ {
381
+ message: "Path allocation already exists, payment method updated",
382
+ path: `m/44'/${body.coin_type}'/${body.account_index}'`,
383
+ },
384
+ 200,
385
+ );
386
+ }
387
+
383
388
  return c.json({ id: body.id, path: `m/44'/${body.coin_type}'/${body.account_index}'` }, 201);
384
389
  });
385
390
 
@@ -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. */
@@ -19,6 +19,7 @@ export interface PaymentMethodRecord {
19
19
  addressType: string;
20
20
  encodingParams: string;
21
21
  watcherType: string;
22
+ oracleAssetId: string | null;
22
23
  confirmations: number;
23
24
  }
24
25
 
@@ -93,6 +94,7 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
93
94
  addressType: method.addressType,
94
95
  encodingParams: method.encodingParams,
95
96
  watcherType: method.watcherType,
97
+ oracleAssetId: method.oracleAssetId,
96
98
  confirmations: method.confirmations,
97
99
  })
98
100
  .onConflictDoUpdate({
@@ -113,6 +115,7 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
113
115
  addressType: method.addressType,
114
116
  encodingParams: method.encodingParams,
115
117
  watcherType: method.watcherType,
118
+ oracleAssetId: method.oracleAssetId,
116
119
  confirmations: method.confirmations,
117
120
  },
118
121
  });
@@ -156,6 +159,7 @@ function toRecord(row: typeof paymentMethods.$inferSelect): PaymentMethodRecord
156
159
  addressType: row.addressType,
157
160
  encodingParams: row.encodingParams,
158
161
  watcherType: row.watcherType,
162
+ oracleAssetId: row.oracleAssetId,
159
163
  confirmations: row.confirmations,
160
164
  };
161
165
  }
@@ -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,14 +360,15 @@ 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.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;
@@ -82,9 +82,10 @@ export const paymentMethods = pgTable("payment_methods", {
82
82
  rpcUrl: text("rpc_url"), // chain node RPC endpoint
83
83
  oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
84
84
  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)
85
+ addressType: text("address_type").notNull().default("evm"), // "bech32" (BTC/LTC), "p2pkh" (DOGE/TRX), "evm" (ETH/ERC20)
86
86
  encodingParams: text("encoding_params").notNull().default("{}"), // JSON: {"hrp":"bc"}, {"version":"0x1e"}, etc.
87
87
  watcherType: text("watcher_type").notNull().default("evm"), // "utxo" (BTC/LTC/DOGE) or "evm" (ETH/ERC20/TRX)
88
+ oracleAssetId: text("oracle_asset_id"), // CoinGecko slug (e.g. "bitcoin", "tron"). Null = stablecoin (1:1 USD) or use token symbol fallback.
88
89
  confirmations: integer("confirmations").notNull().default(1),
89
90
  nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
90
91
  createdAt: text("created_at").notNull().default(sql`(now())`),