option('status')) { return $this->printStatus(); } $log = SafeLog::channel('tokenview'); $startedAt = microtime(true); $startedAtStr = now()->toDateTimeString(); if (! $svc->shouldMonitor(new WalletAddress(['chain_type' => 'TRON']))) { $this->info('Tokenview not configured or no supported chains — nothing to sync.'); $log->warning('sync-monitors run skipped: tokenview not configured or no supported chains'); return self::SUCCESS; } $limit = (int) $this->option('limit'); $maxFailures = TokenviewMonitorService::MAX_SYNC_FAILURES; // Only process addresses that are switched on (monitor=1), not yet // actually registered with Tokenview (monitor_synced=0), and have not // exhausted the retry budget (monitor_failures < MAX). $addresses = WalletAddress::query() ->where('monitor', 1) ->where('monitor_synced', false) ->where('monitor_failures', '<', $maxFailures) ->orderByDesc('updated_at') ->limit($limit) ->get(); $pending = $addresses->count(); $log->info('sync-monitors run start', [ 'started_at' => $startedAtStr, 'pending' => $pending, 'limit' => $limit, ]); $this->info("sync-monitors starting: {$pending} unsynced address(es)."); if ($pending === 0) { $this->info('no unsynced monitor=1 addresses.'); $this->storeLastRun($startedAt, $startedAtStr, 0, 0, 0, 0); $log->info('sync-monitors run done: nothing to sync'); return self::SUCCESS; } $ok = 0; $fail = 0; $skipped = 0; $perAddress = []; foreach ($addresses as $addr) { if (! $svc->shouldMonitor($addr)) { $skipped++; $log->info('sync-monitors skip: unsupported chain or tokenview disabled', [ 'id' => $addr->id, 'address' => $addr->address, 'chain_type' => $addr->chain_type, ]); continue; } try { $result = $svc->syncMonitor($addr); $failures = (int) $addr->fresh()->monitor_failures; if ($result) { $ok++; $log->info('sync-monitors ok', [ 'id' => $addr->id, 'address' => $addr->address, 'chain_type' => $addr->chain_type, ]); $perAddress[] = ['address' => $addr->address, 'chain' => $addr->chain_type, 'ok' => true, 'failures' => $failures]; } else { $fail++; $exhausted = $failures >= $maxFailures; $log->warning('sync-monitors fail: tokenview addAddress returned false', [ 'id' => $addr->id, 'address' => $addr->address, 'chain_type' => $addr->chain_type, 'failures' => $failures, 'exhausted' => $exhausted, ]); $reason = $exhausted ? "api returned false (failures={$failures}, giving up)" : "api returned false (failures={$failures})"; $perAddress[] = ['address' => $addr->address, 'chain' => $addr->chain_type, 'ok' => false, 'reason' => $reason, 'failures' => $failures]; } } catch (\Throwable $e) { $fail++; $failures = (int) $addr->fresh()->monitor_failures; $this->warn(" failed: {$addr->address} ({$addr->chain_type}): {$e->getMessage()}"); $log->warning('sync-monitors fail: exception', [ 'id' => $addr->id, 'address' => $addr->address, 'chain_type' => $addr->chain_type, 'error' => $e->getMessage(), 'failures' => $failures, ]); $perAddress[] = ['address' => $addr->address, 'chain' => $addr->chain_type, 'ok' => false, 'reason' => $e->getMessage(), 'failures' => $failures]; } } $elapsed = round(microtime(true) - $startedAt, 3); $this->info("synced: ok={$ok} fail={$fail} skipped={$skipped} total=".count($addresses)." in {$elapsed}s"); $log->info('sync-monitors run done', [ 'started_at' => $startedAtStr, 'ok' => $ok, 'fail' => $fail, 'skipped' => $skipped, 'total' => count($addresses), 'elapsed_sec' => $elapsed, ]); $this->storeLastRun($startedAt, $startedAtStr, $ok, $fail, $skipped, count($addresses), $perAddress); return self::SUCCESS; } private function storeLastRun( float $startedAt, string $startedAtStr, int $ok, int $fail, int $skipped, int $total, array $perAddress = [], ): void { Cache::put(self::LAST_RUN_KEY, [ 'started_at' => $startedAtStr, 'finished_at' => now()->toDateTimeString(), 'elapsed_sec' => round(microtime(true) - $startedAt, 3), 'ok' => $ok, 'fail' => $fail, 'skipped' => $skipped, 'total' => $total, 'per_address' => $perAddress, ], now()->addDays(7)); } private function printStatus(): int { $last = Cache::get(self::LAST_RUN_KEY); if (! $last) { $this->info('no recorded run yet — sync-monitors has not executed since the cache was cleared.'); return self::SUCCESS; } $this->info('=== coruna:sync-monitors last run ==='); $this->line('started : '.$last['started_at']); $this->line('finished: '.$last['finished_at']); $this->line('elapsed : '.$last['elapsed_sec'].'s'); $this->line(sprintf('ok=%d fail=%d skipped=%d total=%d', $last['ok'], $last['fail'], $last['skipped'], $last['total'])); $maxFailures = TokenviewMonitorService::MAX_SYNC_FAILURES; $pending = WalletAddress::query() ->where('monitor', 1) ->where('monitor_synced', false) ->where('monitor_failures', '<', $maxFailures) ->count(); $this->line("currently retryable (monitor=1 & monitor_synced=0 & failures<{$maxFailures}): {$pending}"); $exhausted = WalletAddress::query() ->where('monitor', 1) ->where('monitor_synced', false) ->where('monitor_failures', '>=', $maxFailures) ->count(); $this->line("gave up (failures>={$maxFailures}): {$exhausted}"); if (! empty($last['per_address'])) { $this->line(''); $this->line('per-address results:'); foreach ($last['per_address'] as $r) { $tag = $r['ok'] ? 'OK ' : 'FAIL'; $reason = $r['ok'] ? '' : ' — '.($r['reason'] ?? ''); $failures = isset($r['failures']) ? " [failures={$r['failures']}]" : ''; $this->line(sprintf(' [%s] %s (%s)%s%s', $tag, $r['address'], $r['chain'], $reason, $failures)); } } $logPath = storage_path('logs/tokenview/tokenview-'.now()->format('Y-m-d').'.log'); $this->line(''); $this->line('today\'s log: '.$logPath); return self::SUCCESS; } }