'eth', 'TRX', 'TRON' => 'trx', 'BTC', 'BITCOIN' => 'btc', 'BNB', 'BSC', 'BINANCE' => 'bsc', default => null, }; } /** * Map Tokenview webhook coin → wallet_addresses column for native value. */ public static function nativeColumnForCoin(string $coin): ?string { return match (strtoupper($coin)) { 'ETH' => 'eth', 'TRX' => 'trx', 'BTC' => 'btc', 'BSC', 'BNB' => 'bnb', default => null, }; } public function syncMonitor(WalletAddress $address): bool { if (! $this->client->enabled()) { return false; } $coin = self::coinAbbrFromChainType($address->chain_type); if ($coin === null) { Log::info('tokenview sync skipped: unsupported chain', [ 'id' => $address->id, 'chain_type' => $address->chain_type, ]); return false; } $addr = (string) $address->address; if ((int) $address->monitor === 1) { return $this->client->addAddress($coin, $addr); } return $this->client->removeAddress($coin, $addr); } /** * @param array $payload */ public function handleWebhook(array $payload): void { $address = trim((string) ($payload['address'] ?? '')); $txid = trim((string) ($payload['txid'] ?? '')); $coin = strtoupper(trim((string) ($payload['coin'] ?? ''))); if ($address === '' || $txid === '' || $coin === '') { return; } $tokenSymbol = strtoupper(trim((string) ($payload['tokenSymbol'] ?? ''))); $value = $payload['value'] ?? null; $tokenValue = $payload['tokenValue'] ?? null; $lookup = $this->normalizeAddress($address, $coin); $rows = WalletAddress::query() ->where('monitor', 1) ->where(function ($q) use ($lookup, $address) { $q->where('address', $lookup)->orWhere('address', $address); if (strcasecmp($lookup, $address) !== 0) { $q->orWhereRaw('LOWER(address) = ?', [strtolower($lookup)]); } }) ->get(); if ($rows->isEmpty()) { return; } $dedupeSymbol = $tokenSymbol; try { TokenviewEvent::query()->create([ 'txid' => $txid, 'address' => $lookup, 'coin' => $coin, 'token_symbol' => $dedupeSymbol, 'value' => is_scalar($value) ? (string) $value : null, 'token_value' => is_scalar($tokenValue) ? (string) $tokenValue : null, ]); } catch (\Throwable) { // unique violation → already processed return; } $deltas = $this->resolveDeltas($coin, $value, $tokenSymbol, $tokenValue); if ($deltas === []) { 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(); } }); $inbound = []; foreach ($deltas as $col => $delta) { if ($delta > 0) { $inbound[$col] = $delta; } } if ($inbound === []) { return; } $deviceKey = Device::query()->whereKey($rows->first()->device_id)->value('device_id') ?: (string) $rows->first()->device_id; foreach ($inbound as $col => $delta) { $this->telegram->notifyBalanceChange( (string) $deviceKey, $lookup, strtoupper($col), WalletAddress::formatAmount($col, $delta), $coin ); } } public function verifySignature(string $rawBody, ?string $signature): bool { $signKey = (string) config('coruna.tokenview.sign_key'); if ($signKey === '') { return true; } if ($signature === null || $signature === '') { return false; } $expected = hash_hmac('sha256', $rawBody, $signKey); return hash_equals($expected, strtolower($signature)) || hash_equals($expected, $signature); } /** * @return array column => delta */ private function resolveDeltas(string $coin, mixed $value, string $tokenSymbol, mixed $tokenValue): array { $deltas = []; if ($tokenSymbol !== '' && is_numeric($tokenValue)) { $col = strtolower($tokenSymbol); if (in_array($col, WalletAddress::COIN_COLUMNS, true)) { $deltas[$col] = (float) $tokenValue; } } if (is_numeric($value)) { $nativeCol = self::nativeColumnForCoin($coin); if ($nativeCol !== null) { // Prefer token delta when both present for same column (USDT-only token txs // often still send a tiny native gas value — keep both columns when distinct). $deltas[$nativeCol] = (float) $value; } } return $deltas; } private function normalizeAddress(string $address, string $coin): string { $c = strtolower($coin); if (in_array($c, ['eth', 'bsc', 'bnb'], true) || str_starts_with($address, '0x') || str_starts_with($address, '0X')) { return strtolower($address); } return $address; } }