initkkk
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* Server-side signing now lives in `wallet-signer.service.ts` (custodial).
|
||||
*/
|
||||
import { ethers } from 'ethers';
|
||||
import { createHash } from 'crypto';
|
||||
import { env } from '../config/env';
|
||||
import { getEvmTokens, getTrxTokens, getSolTokens } from '../lib/token-registry';
|
||||
|
||||
@@ -177,10 +178,11 @@ async function evmBalance(
|
||||
tokens: { symbol: string; addr: string }[],
|
||||
): Promise<{ native: string; tokens: Record<string, string> }> {
|
||||
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
|
||||
const native = await withTimeout(provider.getBalance(address), TIMEOUT_MS, 'EVM balance timeout');
|
||||
// H52 — Promise.allSettled: одна сбоящая RPC subcall не валит весь endpoint в 502
|
||||
const nativeP = withTimeout(provider.getBalance(address), TIMEOUT_MS, 'EVM balance timeout').then(b => b.toString()).catch(() => '0');
|
||||
|
||||
const tokenBalances: Record<string, string> = {};
|
||||
await Promise.all(
|
||||
await Promise.allSettled(
|
||||
tokens.map(async ({ symbol, addr }) => {
|
||||
try {
|
||||
const c = new ethers.Contract(addr, ERC20_ABI, provider);
|
||||
@@ -192,7 +194,8 @@ async function evmBalance(
|
||||
}),
|
||||
);
|
||||
|
||||
return { native: native.toString(), tokens: tokenBalances };
|
||||
const native = await nativeP;
|
||||
return { native, tokens: tokenBalances };
|
||||
}
|
||||
|
||||
async function solBalance(address: string): Promise<{ native: string; tokens: Record<string, string> }> {
|
||||
@@ -316,18 +319,42 @@ async function scanTransactions(
|
||||
async function btcTransactions(address: string, limit: number): Promise<TxItem[]> {
|
||||
const txs = await fetchJson(`${BLOCKSTREAM}/address/${address}/txs`);
|
||||
return (txs as any[]).slice(0, limit).map((tx) => {
|
||||
const inSelf = tx.vin.some((v: any) => v.prevout?.scriptpubkey_address === address);
|
||||
const outSelf = tx.vout.some((v: any) => v.scriptpubkey_address === address);
|
||||
const direction: TxItem['direction'] = inSelf && outSelf ? 'self' : inSelf ? 'out' : 'in';
|
||||
const vin = Array.isArray(tx.vin) ? tx.vin : [];
|
||||
const vout = Array.isArray(tx.vout) ? tx.vout : [];
|
||||
const inSelf = vin.some((v: any) => v.prevout?.scriptpubkey_address === address);
|
||||
const allOutsSelf = vout.length > 0 && vout.every((v: any) => v.scriptpubkey_address === address);
|
||||
const anyOutsExternal = vout.some((v: any) => v.scriptpubkey_address && v.scriptpubkey_address !== address);
|
||||
|
||||
// H49 — корректная direction logic:
|
||||
// self = consolidation/tx where ВСЕ outs идут обратно к нам (rare; обычно нет change)
|
||||
// out = мы spend'им (inSelf=true) И есть external recipient
|
||||
// in = мы получаем (НЕ inSelf, есть out к нам)
|
||||
let direction: TxItem['direction'];
|
||||
if (inSelf && allOutsSelf) {
|
||||
direction = 'self';
|
||||
} else if (inSelf && anyOutsExternal) {
|
||||
direction = 'out';
|
||||
} else {
|
||||
direction = 'in';
|
||||
}
|
||||
|
||||
// amount: для 'out' — sum external outs; для 'in' — sum outs к нам; для 'self' — 0
|
||||
let amountSat = 0n;
|
||||
if (direction === 'in') {
|
||||
amountSat = vout
|
||||
.filter((v: any) => v.scriptpubkey_address === address)
|
||||
.reduce((s: bigint, v: any) => s + BigInt(v.value || 0), 0n);
|
||||
} else if (direction === 'out') {
|
||||
amountSat = vout
|
||||
.filter((v: any) => v.scriptpubkey_address && v.scriptpubkey_address !== address)
|
||||
.reduce((s: bigint, v: any) => s + BigInt(v.value || 0), 0n);
|
||||
}
|
||||
|
||||
return {
|
||||
txid: tx.txid,
|
||||
timestamp: tx.status?.block_time ?? null,
|
||||
direction,
|
||||
amount: String(
|
||||
tx.vout
|
||||
.filter((v: any) => (direction === 'in' ? v.scriptpubkey_address === address : v.scriptpubkey_address !== address))
|
||||
.reduce((s: bigint, v: any) => s + BigInt(v.value), 0n),
|
||||
),
|
||||
amount: String(amountSat),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -358,7 +385,7 @@ async function trxTransactions(address: string, limit: number): Promise<TxItem[]
|
||||
}
|
||||
|
||||
async function solTransactions(address: string, limit: number): Promise<TxItem[]> {
|
||||
const res = await fetchJson(SOL_RPC, {
|
||||
const sigsRes = await fetchJson(SOL_RPC, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -368,11 +395,56 @@ async function solTransactions(address: string, limit: number): Promise<TxItem[]
|
||||
params: [address, { limit }],
|
||||
}),
|
||||
});
|
||||
return ((res.result as any[]) || []).map((sig) => ({
|
||||
txid: sig.signature,
|
||||
timestamp: sig.blockTime ?? null,
|
||||
direction: 'self' as const, // без deep parsing — направление неизвестно
|
||||
}));
|
||||
// H50 — H18: filter out failed txs + deep-parse selected (limit small concurrency).
|
||||
const allSigs = ((sigsRes.result as any[]) || []).filter((s) => s.err === null);
|
||||
|
||||
// Fetch tx details для balance deltas — batch parallel но небольшим limit'ом
|
||||
const results: TxItem[] = [];
|
||||
for (const sig of allSigs.slice(0, limit)) {
|
||||
try {
|
||||
const txRes = await fetchJson(SOL_RPC, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'getTransaction',
|
||||
params: [sig.signature, { maxSupportedTransactionVersion: 0, encoding: 'json' }],
|
||||
}),
|
||||
});
|
||||
const tx = txRes.result;
|
||||
const accountKeys = tx?.transaction?.message?.accountKeys || [];
|
||||
const idx = accountKeys.indexOf(address);
|
||||
const pre = tx?.meta?.preBalances?.[idx];
|
||||
const post = tx?.meta?.postBalances?.[idx];
|
||||
let direction: TxItem['direction'] = 'self';
|
||||
let amount: string | undefined;
|
||||
if (typeof pre === 'number' && typeof post === 'number') {
|
||||
const delta = post - pre;
|
||||
if (delta < 0) {
|
||||
direction = 'out';
|
||||
amount = String(-delta);
|
||||
} else if (delta > 0) {
|
||||
direction = 'in';
|
||||
amount = String(delta);
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
txid: sig.signature,
|
||||
timestamp: sig.blockTime ?? null,
|
||||
direction,
|
||||
amount,
|
||||
});
|
||||
} catch {
|
||||
// Если getTransaction fails — fallback на minimal entry
|
||||
results.push({
|
||||
txid: sig.signature,
|
||||
timestamp: sig.blockTime ?? null,
|
||||
direction: 'self',
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─────────────────────── HELPERS ───────────────────────
|
||||
@@ -584,10 +656,52 @@ function tronAddressToHex(address: string): string {
|
||||
return hex.slice(2, 42); // 20 bytes без префикса 0x41
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode TRON address: 20-byte hex (без 0x41 prefix) → base58check 'T...' string.
|
||||
* H51 — фронтенд получал raw hex `41...` instead of readable `T...`. Fix.
|
||||
*/
|
||||
function hexToTron(hex: string): string {
|
||||
// Формат TronGrid: 41xxxx (20 bytes after prefix). Это base58check.
|
||||
// Для простоты возвращаем как есть hex (можно улучшить на full base58check если нужно).
|
||||
return hex;
|
||||
if (!hex) return '';
|
||||
// Принимаем hex с или без префикса 0x41
|
||||
let bytesHex = hex.toLowerCase();
|
||||
if (bytesHex.startsWith('0x')) bytesHex = bytesHex.slice(2);
|
||||
// Если уже 21 byte (с prefix) — оставляем; если 20 — добавляем 0x41
|
||||
if (bytesHex.length === 40) {
|
||||
bytesHex = '41' + bytesHex;
|
||||
} else if (bytesHex.length !== 42) {
|
||||
// Unknown length — fail-safe return raw input для backward compat
|
||||
return hex;
|
||||
}
|
||||
if (!/^[0-9a-f]+$/.test(bytesHex)) return hex;
|
||||
|
||||
const payload = Buffer.from(bytesHex, 'hex');
|
||||
// SHA256d checksum (4 bytes)
|
||||
const h1 = createHash('sha256').update(payload).digest();
|
||||
const h2 = createHash('sha256').update(h1).digest();
|
||||
const fullBytes = Buffer.concat([payload, h2.subarray(0, 4)]);
|
||||
|
||||
// base58 encode
|
||||
return base58Encode(fullBytes);
|
||||
}
|
||||
|
||||
const BASE58_ALPHABET_OPS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
|
||||
function base58Encode(bytes: Buffer): string {
|
||||
let num = 0n;
|
||||
for (const b of bytes) {
|
||||
num = (num << 8n) + BigInt(b);
|
||||
}
|
||||
let s = '';
|
||||
while (num > 0n) {
|
||||
s = BASE58_ALPHABET_OPS[Number(num % 58n)] + s;
|
||||
num /= 58n;
|
||||
}
|
||||
// Leading zero bytes → leading '1's
|
||||
for (const b of bytes) {
|
||||
if (b === 0) s = '1' + s;
|
||||
else break;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
async function fetchJson(url: string, init?: RequestInit): Promise<any> {
|
||||
|
||||
Reference in New Issue
Block a user