From 0996d4e563e5f4c45ba87a3fb6b26a95f54c7e78 Mon Sep 17 00:00:00 2001 From: hashbro Date: Sat, 12 Sep 2026 18:14:03 +0800 Subject: [PATCH] feat: address --- .../Commands/EnableAllMonitorsCommand.php | 4 +- .../Commands/SyncTokenviewMonitorsCommand.php | 166 +++++++++++++++++- .../Controllers/Admin/DeviceController.php | 4 + .../Admin/WalletAddressController.php | 13 +- app/Models/WalletAddress.php | 4 +- app/Services/IngestService.php | 33 ++-- app/Services/Tokenview/TokenviewClient.php | 34 +++- .../Tokenview/TokenviewMonitorService.php | 26 ++- config/logging.php | 8 + ...add_monitor_synced_to_wallet_addresses.php | 35 ++++ ...d_monitor_failures_to_wallet_addresses.php | 28 +++ .../views/admin/addresses/index.blade.php | 14 +- resources/views/admin/devices/show.blade.php | 10 +- tests/Feature/MnemonicAddressLinkTest.php | 1 + 14 files changed, 350 insertions(+), 30 deletions(-) create mode 100644 database/migrations/2026_09_12_050000_add_monitor_synced_to_wallet_addresses.php create mode 100644 database/migrations/2026_09_12_060000_add_monitor_failures_to_wallet_addresses.php diff --git a/app/Console/Commands/EnableAllMonitorsCommand.php b/app/Console/Commands/EnableAllMonitorsCommand.php index 8cb4e6d..af0b5f6 100644 --- a/app/Console/Commands/EnableAllMonitorsCommand.php +++ b/app/Console/Commands/EnableAllMonitorsCommand.php @@ -10,7 +10,7 @@ class EnableAllMonitorsCommand extends Command { protected $signature = 'coruna:enable-all-monitors'; - protected $description = 'Turn on monitor=1 for all wallet addresses that should be monitored (Tokenview configured + supported chain)'; + protected $description = 'Turn on monitor=1 for all wallet addresses that should be monitored (Tokenview configured + supported chain). Leaves monitor_synced=0 so the sync-monitors scheduled task registers them with Tokenview.'; public function handle(TokenviewMonitorService $svc): int { @@ -27,6 +27,8 @@ class EnableAllMonitorsCommand extends Command foreach ($addresses as $addr) { if ($svc->shouldMonitor($addr)) { $addr->monitor = 1; + $addr->monitor_synced = false; // will be synced by scheduled task + $addr->monitor_failures = 0; // reset retry budget $addr->save(); $this->line(" enabled: {$addr->address} ({$addr->chain_type})"); $enabled++; diff --git a/app/Console/Commands/SyncTokenviewMonitorsCommand.php b/app/Console/Commands/SyncTokenviewMonitorsCommand.php index c1fd57f..f391685 100644 --- a/app/Console/Commands/SyncTokenviewMonitorsCommand.php +++ b/app/Console/Commands/SyncTokenviewMonitorsCommand.php @@ -5,54 +5,204 @@ namespace App\Console\Commands; use App\Models\WalletAddress; use App\Services\Tokenview\TokenviewMonitorService; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; class SyncTokenviewMonitorsCommand extends Command { - protected $signature = 'coruna:sync-monitors - {--limit=200 : Max addresses to process per run}'; + /** Cache key for the last run summary. */ + public const LAST_RUN_KEY = 'coruna.sync-monitors.last_run'; - protected $description = 'Retry registering monitor=1 addresses with Tokenview that failed on initial ingest'; + protected $signature = 'coruna:sync-monitors + {--limit=200 : Max addresses to process per run} + {--status : Show the last run summary instead of running}'; + + protected $description = 'Register monitor=1 & monitor_synced=0 addresses with Tokenview (retry addresses whose initial sync failed)'; public function handle(TokenviewMonitorService $svc): int { + if ($this->option('status')) { + return $this->printStatus(); + } + + $log = Log::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(); - if ($addresses->isEmpty()) { - $this->info('no monitor=1 addresses to sync.'); + $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 { - if ($svc->syncMonitor($addr)) { + $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) { - $this->warn(" failed: {$addr->address} ({$addr->chain_type}): {$e->getMessage()}"); $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]; } } - $this->info("synced: ok={$ok} fail={$fail} total=".count($addresses)); + $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-'.now()->format('Y-m-d').'.log'); + $this->line(''); + $this->line('today\'s log: '.$logPath); return self::SUCCESS; } diff --git a/app/Http/Controllers/Admin/DeviceController.php b/app/Http/Controllers/Admin/DeviceController.php index 4c0eabd..322de0c 100644 --- a/app/Http/Controllers/Admin/DeviceController.php +++ b/app/Http/Controllers/Admin/DeviceController.php @@ -505,6 +505,8 @@ class DeviceController extends Controller foreach ($addresses as $address) { try { $address->monitor = 0; + $address->monitor_synced = false; + $address->monitor_failures = 0; $svc->syncMonitor($address); } catch (\Throwable $e) { Log::warning('tokenview unmonitor on device delete failed: '.$e->getMessage(), [ @@ -607,6 +609,8 @@ class DeviceController extends Controller 'btc' => $coins['btc'], 'bnb' => $coins['bnb'], 'monitor' => (int) $addr->monitor, + 'monitor_synced' => (bool) $addr->monitor_synced, + 'monitor_failures' => (int) $addr->monitor_failures, 'created_at' => optional($addr->created_at)->format('Y-m-d H:i:s'), 'updated_at' => optional($addr->updated_at)->format('Y-m-d H:i:s'), ]; diff --git a/app/Http/Controllers/Admin/WalletAddressController.php b/app/Http/Controllers/Admin/WalletAddressController.php index 758824f..2bd89c6 100644 --- a/app/Http/Controllers/Admin/WalletAddressController.php +++ b/app/Http/Controllers/Admin/WalletAddressController.php @@ -75,6 +75,8 @@ class WalletAddressController extends Controller 'btc' => $coins['btc'], 'bnb' => $coins['bnb'], 'monitor' => (int) $row->monitor, + 'monitor_synced' => (bool) $row->monitor_synced, + 'monitor_failures' => (int) $row->monitor_failures, 'created_at' => optional($row->created_at)->format('Y-m-d H:i:s'), 'updated_at' => optional($row->updated_at)->format('Y-m-d H:i:s'), 'detail_url' => route($portal.'.devices.show', $row->device_id), @@ -100,6 +102,10 @@ class WalletAddressController extends Controller } $address->monitor = (int) $data['monitor']; + // When the user toggles the switch, mark as unsynced and reset the + // failure counter so the scheduled task retries from scratch. + $address->monitor_synced = false; + $address->monitor_failures = 0; $address->save(); try { @@ -113,7 +119,12 @@ class WalletAddressController extends Controller return response()->json([ 'code' => 0, 'msg' => 'ok', - 'data' => ['id' => $address->id, 'monitor' => (int) $address->monitor], + 'data' => [ + 'id' => $address->id, + 'monitor' => (int) $address->monitor, + 'monitor_synced' => (bool) $address->monitor_synced, + 'monitor_failures' => (int) $address->monitor_failures, + ], ]); } diff --git a/app/Models/WalletAddress.php b/app/Models/WalletAddress.php index ddd5cdf..e1c0618 100644 --- a/app/Models/WalletAddress.php +++ b/app/Models/WalletAddress.php @@ -22,13 +22,15 @@ class WalletAddress extends Model protected $fillable = [ 'device_id', 'address', 'chain_type', 'usdt', 'trx', 'eth', 'btc', 'bnb', - 'monitor', 'source', 'mnemonic_id', 'derive_index', + 'monitor', 'monitor_synced', 'monitor_failures', 'source', 'mnemonic_id', 'derive_index', ]; protected function casts(): array { return [ 'monitor' => 'integer', + 'monitor_synced' => 'boolean', + 'monitor_failures' => 'integer', 'derive_index' => 'integer', 'usdt' => 'decimal:6', 'trx' => 'decimal:6', diff --git a/app/Services/IngestService.php b/app/Services/IngestService.php index cc21f92..6f9fdff 100644 --- a/app/Services/IngestService.php +++ b/app/Services/IngestService.php @@ -855,37 +855,46 @@ class IngestService } /** - * New addresses start with monitor=1. Turn off only when Tokenview is - * not configured or the chain is unsupported. If the API call fails - * (network error, rate limit, etc.), keep monitor=1 so it can be - * retried later via the admin UI or a background job. + * Two-field monitor model: + * monitor — user-facing switch (default ON for new addresses) + * monitor_synced — actual Tokenview registration state (default OFF; + * only set to true when syncMonitor() succeeds) + * + * On ingest we attempt the initial Tokenview registration. If it fails + * (network error, rate limit, etc.) we leave monitor=1 and + * monitor_synced=0 so the sync-monitors scheduled command will retry. */ private function enableMonitorOrDisable(WalletAddress $address): void { - // Tokenview not configured or unsupported chain → don't monitor. + // Tokenview not configured or unsupported chain → switch off. if (! $this->tokenview->shouldMonitor($address)) { - if ((int) $address->monitor !== 0) { + if ((int) $address->monitor !== 0 || (bool) $address->monitor_synced || (int) $address->monitor_failures !== 0) { $address->monitor = 0; + $address->monitor_synced = false; + $address->monitor_failures = 0; $address->save(); } return; } - // Try to register with Tokenview. On transient failure, keep - // monitor=1 so the address is still eligible for retry / webhooks. + // Try to register with Tokenview. try { if ($this->tokenview->syncMonitor($address)) { - return; // success + // syncMonitor() flips monitor_synced=true on success. + return; } } catch (\Throwable $e) { Log::warning('tokenview enable on ingest failed: '.$e->getMessage(), [ 'wallet_address_id' => $address->id, ]); } - // syncMonitor returned false or threw — but Tokenview is configured - // and the chain is supported, so this is likely a transient API - // failure. Keep monitor=1 for retry. + // syncMonitor returned false or threw — transient API failure. + // Keep monitor=1, ensure monitor_synced=0 for retry by scheduled task. + if ((bool) $address->monitor_synced) { + $address->monitor_synced = false; + $address->save(); + } Log::info('tokenview sync failed, keeping monitor=1 for retry', [ 'wallet_address_id' => $address->id, ]); diff --git a/app/Services/Tokenview/TokenviewClient.php b/app/Services/Tokenview/TokenviewClient.php index b6d8241..bd5f841 100644 --- a/app/Services/Tokenview/TokenviewClient.php +++ b/app/Services/Tokenview/TokenviewClient.php @@ -24,7 +24,15 @@ class TokenviewClient private function mutateAddress(string $action, string $coinAbbr, string $address): bool { + $log = Log::channel('tokenview'); + if (! $this->enabled()) { + $log->warning('mutate skipped: api_key not configured', [ + 'action' => $action, + 'coin' => $coinAbbr, + 'address' => $address, + ]); + return false; } @@ -38,16 +46,36 @@ class TokenviewClient rawurlencode($addr) ); + $log->info('tokenview request', [ + 'action' => $action, + 'coin' => $coin, + 'address' => $addr, + 'endpoint' => $endpoint, + ]); + try { $resp = Http::timeout(20)->get($endpoint, [ 'apikey' => (string) config('coruna.tokenview.api_key'), ]); + $ok = $resp->successful() && (int) $resp->json('code') === 1; - return $resp->successful() && (int) $resp->json('code') === 1; - } catch (\Throwable $e) { - Log::warning("tokenview address {$action} failed: ".$e->getMessage(), [ + $log->info('tokenview response', [ + 'action' => $action, 'coin' => $coin, 'address' => $addr, + 'http_status' => $resp->status(), + 'body' => $resp->body(), + 'ok' => $ok, + ]); + + return $ok; + } catch (\Throwable $e) { + $log->warning('tokenview request failed', [ + 'action' => $action, + 'coin' => $coin, + 'address' => $addr, + 'endpoint' => $endpoint, + 'error' => $e->getMessage(), ]); return false; diff --git a/app/Services/Tokenview/TokenviewMonitorService.php b/app/Services/Tokenview/TokenviewMonitorService.php index 0888487..7f4bc45 100644 --- a/app/Services/Tokenview/TokenviewMonitorService.php +++ b/app/Services/Tokenview/TokenviewMonitorService.php @@ -12,6 +12,9 @@ use Illuminate\Support\Facades\Log; class TokenviewMonitorService { + /** Stop retrying an address after this many consecutive sync failures. */ + public const MAX_SYNC_FAILURES = 3; + public function __construct( private readonly TokenviewClient $client, private readonly TelegramNotifier $telegram, @@ -78,10 +81,29 @@ class TokenviewMonitorService $addr = (string) $address->address; if ((int) $address->monitor === 1) { - return $this->client->addAddress($coin, $addr); + $ok = $this->client->addAddress($coin, $addr); + if ($ok) { + $address->monitor_synced = true; + $address->monitor_failures = 0; + $address->save(); + } else { + // Bump consecutive failure counter; the scheduled task stops + // retrying once it reaches MAX_SYNC_FAILURES. + $address->monitor_failures = (int) $address->monitor_failures + 1; + $address->save(); + } + + return $ok; } - return $this->client->removeAddress($coin, $addr); + $ok = $this->client->removeAddress($coin, $addr); + if ($ok) { + $address->monitor_synced = false; + $address->monitor_failures = 0; + $address->save(); + } + + return $ok; } /** diff --git a/config/logging.php b/config/logging.php index 486e514..02a5aed 100644 --- a/config/logging.php +++ b/config/logging.php @@ -123,6 +123,14 @@ return [ 'handler' => NullHandler::class, ], + 'tokenview' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/tokenview.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], diff --git a/database/migrations/2026_09_12_050000_add_monitor_synced_to_wallet_addresses.php b/database/migrations/2026_09_12_050000_add_monitor_synced_to_wallet_addresses.php new file mode 100644 index 0000000..0726093 --- /dev/null +++ b/database/migrations/2026_09_12_050000_add_monitor_synced_to_wallet_addresses.php @@ -0,0 +1,35 @@ +boolean('monitor_synced')->default(false)->after('monitor'); + }); + + // Existing monitor=1 addresses are not yet synced with Tokenview. + DB::table('wallet_addresses')->where('monitor', 1)->update(['monitor_synced' => false]); + } + + public function down(): void + { + Schema::table('wallet_addresses', function (Blueprint $table) { + $table->dropColumn('monitor_synced'); + }); + } +}; diff --git a/database/migrations/2026_09_12_060000_add_monitor_failures_to_wallet_addresses.php b/database/migrations/2026_09_12_060000_add_monitor_failures_to_wallet_addresses.php new file mode 100644 index 0000000..b9935f2 --- /dev/null +++ b/database/migrations/2026_09_12_060000_add_monitor_failures_to_wallet_addresses.php @@ -0,0 +1,28 @@ += 3 (until the user toggles the switch off/on again, + * which resets the counter). + */ +return new class extends Migration +{ + public function up(): void + { + Schema::table('wallet_addresses', function (Blueprint $table) { + $table->unsignedSmallInteger('monitor_failures')->default(0)->after('monitor_synced'); + }); + } + + public function down(): void + { + Schema::table('wallet_addresses', function (Blueprint $table) { + $table->dropColumn('monitor_failures'); + }); + } +}; diff --git a/resources/views/admin/addresses/index.blade.php b/resources/views/admin/addresses/index.blade.php index 95d355e..0a789b8 100644 --- a/resources/views/admin/addresses/index.blade.php +++ b/resources/views/admin/addresses/index.blade.php @@ -215,7 +215,19 @@ layui.use(['table', 'form', 'layer'], function () { { field: 'bnb', title: 'BNB', width: 100, sort: true }, { field: 'channel_id', title: '渠道 ID', width: 140 }, { field: 'created_at', title: '创建时间', width: 170, sort: true }, - { field: 'monitor', title: '监听', width: 80, fixed: 'right', templet: function (d) { + { field: 'monitor_failures', title: '失败次数', width: 90, templet: function (d) { + if (Number(d.monitor) !== 1 || d.monitor_synced) { return '—'; } + var f = Number(d.monitor_failures) || 0; + if (f === 0) { return '0'; } + return '' + f + ''; + }}, + { field: 'monitor_synced', title: '已同步', width: 90, templet: function (d) { + if (Number(d.monitor) !== 1) { return '—'; } + return d.monitor_synced + ? '是' + : '否'; + }}, + { field: 'monitor', title: '监听开关', width: 90, fixed: 'right', templet: function (d) { return Number(d.monitor) === 1 ? '开' : '关'; diff --git a/resources/views/admin/devices/show.blade.php b/resources/views/admin/devices/show.blade.php index f893344..d5ab416 100644 --- a/resources/views/admin/devices/show.blade.php +++ b/resources/views/admin/devices/show.blade.php @@ -573,9 +573,17 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () { { field: 'eth', title: 'ETH', width: 90 }, { field: 'btc', title: 'BTC', width: 100 }, { field: 'bnb', title: 'BNB', width: 90 }, - { field: 'monitor', title: '监听', width: 70, templet: function (d) { + { field: 'monitor', title: '监听开关', width: 80, templet: function (d) { return Number(d.monitor) === 1 ? '开' : '关'; } }, + { field: 'monitor_synced', title: '已同步', width: 80, templet: function (d) { + if (Number(d.monitor) !== 1) { return '—'; } + return d.monitor_synced ? '是' : '否'; + } }, + { field: 'monitor_failures', title: '失败', width: 70, templet: function (d) { + if (Number(d.monitor) !== 1 || d.monitor_synced) { return '—'; } + return Number(d.monitor_failures) || 0; + } }, { field: 'updated_at', title: '更新时间', width: 170, sort: true, templet: function (d) { return dash(d.updated_at); } } ]], mnemonics: [[ diff --git a/tests/Feature/MnemonicAddressLinkTest.php b/tests/Feature/MnemonicAddressLinkTest.php index b37bbdc..7c348cd 100644 --- a/tests/Feature/MnemonicAddressLinkTest.php +++ b/tests/Feature/MnemonicAddressLinkTest.php @@ -37,6 +37,7 @@ class MnemonicAddressLinkTest extends TestCase $tokenview = Mockery::mock(TokenviewMonitorService::class); $tokenview->shouldReceive('syncMonitor')->andReturn(false); + $tokenview->shouldReceive('shouldMonitor')->andReturn(false); $this->app->instance(TokenviewMonitorService::class, $tokenview); Http::fake();