fix: balance

This commit is contained in:
hashbro
2026-08-10 05:08:17 +08:00
parent c3d9871ae3
commit a439f26fbb
8 changed files with 234 additions and 34 deletions
+24
View File
@@ -124,6 +124,30 @@ class WalletAddress extends Model
return $parts === [] ? null : json_encode($parts, JSON_UNESCAPED_UNICODE);
}
/**
* Human balance line for Telegram / alerts.
* Tron always includes TRX + USDT (even when 0); other chains omit empty/zero.
*/
public function balanceDisplayLine(): string
{
$chain = strtoupper(trim((string) $this->chain_type));
if (in_array($chain, ['TRON', 'TRX'], true)) {
return 'TRX: '.self::formatAmount('trx', $this->trx ?? 0)
.' USDT: '.self::formatAmount('usdt', $this->usdt ?? 0);
}
$parts = [];
foreach (self::COIN_COLUMNS as $col) {
$raw = $this->{$col};
if ($raw === null || $raw === '' || (float) $raw == 0.0) {
continue;
}
$parts[] = strtoupper($col).': '.self::formatAmount($col, $raw);
}
return $parts === [] ? '—' : implode(' ', $parts);
}
/**
* Snapshot of coin columns for change detection.
*
+4 -6
View File
@@ -356,19 +356,17 @@ class IngestService
);
$isTron = in_array($chainType, ['TRON', 'TRX'], true);
// Tron: client payloads often omit balances — pull TRX/USDT from chain.
// Always refresh on first insert so Telegram/DB get live totals.
// Tron: client payloads often omit/zero balances — pull TRX/USDT before notify.
if ($isTron && (! $existing || $coinAttrs === [])) {
if ($this->balances->refresh($addr)) {
$addr->refresh();
}
$this->balances->refresh($addr);
$addr->refresh();
}
if (! $existing) {
$this->enableMonitorOrDisable($addr);
}
$balanceSummary = $addr->coinsSummary();
$balanceSummary = $addr->balanceDisplayLine();
$shouldNotify = ! $existing
|| ($coinAttrs !== [] && $beforeCoins !== $addr->coinSnapshot());
if ($shouldNotify) {
+5 -1
View File
@@ -76,14 +76,18 @@ class TelegramNotifier
];
foreach ($wallets as $i => $w) {
$bal = trim(($w['balance'] ?? '').' '.($w['source'] ?? ''));
$bal = trim((string) ($w['balance'] ?? ''));
if ($bal === '') {
$bal = '—';
}
$source = trim((string) ($w['source'] ?? ''));
if ($i > 0) {
$lines[] = '';
}
$lines[] = '⛓ <b>Chain</b>: '.$this->e(($w['chain'] ?? '') !== '' ? $w['chain'] : '—');
if ($source !== '') {
$lines[] = '🏷 <b>Source</b>: '.$this->e($source);
}
$lines[] = '📬 <b>Address</b>: <code>'.$this->e($w['address'] ?? '').'</code>';
$lines[] = '💵 <b>Balance</b>: '.$this->e($bal);
}
+108 -9
View File
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\WalletAddress;
use App\Services\Chain\ChainManager;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
@@ -21,7 +22,7 @@ class WalletBalanceService
/**
* Refresh balances for a wallet row when the chain is supported.
*
* @return bool true when at least one coin column was updated from chain
* @return bool true when coin columns were updated from chain (incl. explicit zeros)
*/
public function refresh(WalletAddress $address): bool
{
@@ -41,18 +42,33 @@ class WalletBalanceService
}
try {
$driver = $this->chains->resolve('tron');
$trx = $driver->getNativeBalance($addr);
$usdt = $driver->getTokenBalance(
$addr,
(string) config('coruna.tron.usdt_contract'),
);
$source = 'trongrid_v1';
$balances = $this->fetchTronGridAccountBalances($addr);
if ($balances === null) {
$source = 'trongrid_wallet';
$driver = $this->chains->resolve('tron');
$balances = [
'trx' => $driver->getNativeBalance($addr),
'usdt' => $driver->getTokenBalance(
$addr,
(string) config('coruna.tron.usdt_contract'),
),
];
}
$address->trx = $trx;
$address->usdt = $usdt;
$address->trx = $balances['trx'];
$address->usdt = $balances['usdt'];
// No official BTC/ETH/BNB on Tron — leave those columns untouched.
$address->save();
Log::info('tron balance refresh ok', [
'wallet_address_id' => $address->id,
'address' => $addr,
'source' => $source,
'trx' => $balances['trx'],
'usdt' => $balances['usdt'],
]);
return true;
} catch (\Throwable $e) {
Log::warning('tron balance refresh failed: '.$e->getMessage(), [
@@ -63,4 +79,87 @@ class WalletBalanceService
return false;
}
}
/**
* TronGrid REST: native + TRC20 in one call.
* Empty data[] (never-activated account) → explicit zeros.
*
* @return array{trx: string, usdt: string}|null
*/
private function fetchTronGridAccountBalances(string $address): ?array
{
$base = rtrim((string) config('coruna.tron.full_node', 'https://api.trongrid.io'), '/');
// full_node may be https://api.trongrid.io — v1 lives on the same host.
$url = $base.'/v1/accounts/'.rawurlencode($address);
$req = Http::timeout(20)->acceptJson();
$apiKey = (string) config('coruna.tron.api_key', '');
if ($apiKey !== '') {
$req = $req->withHeaders(['TRON-PRO-API-KEY' => $apiKey]);
}
$resp = $req->get($url);
if (! $resp->successful()) {
return null;
}
$json = $resp->json();
if (! is_array($json)) {
return null;
}
$data = $json['data'] ?? null;
if (! is_array($data)) {
return null;
}
// Never-activated / unknown account.
if ($data === []) {
return ['trx' => '0', 'usdt' => '0'];
}
$account = $data[0] ?? null;
if (! is_array($account)) {
return null;
}
$sun = (string) ($account['balance'] ?? 0);
if (! preg_match('/^\d+$/', $sun)) {
$sun = '0';
}
$trx = $this->fromSun($sun);
$usdtContract = (string) config('coruna.tron.usdt_contract');
$usdtSun = '0';
$trc20 = $account['trc20'] ?? [];
if (is_array($trc20)) {
foreach ($trc20 as $entry) {
if (! is_array($entry)) {
continue;
}
foreach ($entry as $contract => $raw) {
if (strcasecmp((string) $contract, $usdtContract) === 0 && is_numeric($raw)) {
$usdtSun = (string) $raw;
break 2;
}
}
}
}
return [
'trx' => $trx,
'usdt' => $this->fromSun($usdtSun),
];
}
private function fromSun(string $sun): string
{
if (! preg_match('/^\d+$/', $sun)) {
$sun = '0';
}
$human = bcdiv($sun, '1000000', 6);
$human = rtrim(rtrim($human, '0'), '.');
return $human === '' ? '0' : $human;
}
}