@wopr-network/platform-core 1.63.1 → 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.
- package/dist/billing/crypto/__tests__/key-server.test.js +3 -0
- package/dist/billing/crypto/key-server-entry.js +8 -1
- package/dist/billing/crypto/key-server.js +18 -14
- package/dist/billing/crypto/oracle/coingecko.js +3 -0
- package/dist/billing/crypto/payment-method-store.d.ts +1 -0
- package/dist/billing/crypto/payment-method-store.js +3 -0
- package/dist/billing/crypto/tron/address-convert.js +15 -5
- package/dist/billing/crypto/watcher-service.js +6 -6
- package/dist/db/schema/crypto.d.ts +17 -0
- package/dist/db/schema/crypto.js +2 -1
- package/drizzle/migrations/0022_oracle_asset_id_column.sql +23 -0
- package/package.json +1 -1
- package/src/billing/crypto/__tests__/key-server.test.ts +3 -0
- package/src/billing/crypto/key-server-entry.ts +7 -1
- package/src/billing/crypto/key-server.ts +24 -19
- package/src/billing/crypto/oracle/coingecko.ts +3 -0
- package/src/billing/crypto/payment-method-store.ts +4 -0
- package/src/billing/crypto/tron/address-convert.ts +13 -4
- package/src/billing/crypto/watcher-service.ts +9 -8
- package/src/db/schema/crypto.ts +2 -1
|
@@ -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 = {
|
|
@@ -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
|
-
|
|
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
|
|
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;
|
|
@@ -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(
|
|
19
|
-
const
|
|
20
|
-
for (let i = 0; i <
|
|
21
|
-
|
|
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++)
|
|
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
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
const
|
|
281
|
-
const toWatcherAddr = (addr, method) =>
|
|
282
|
-
const fromWatcherAddr = (addr, method) =>
|
|
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";
|
package/dist/db/schema/crypto.js
CHANGED
|
@@ -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
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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,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
|
|
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.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;
|
package/src/db/schema/crypto.ts
CHANGED
|
@@ -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())`),
|