create([ 'device_id' => 'dev-tv-1', 'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', ]); return WalletAddress::query()->create(array_merge([ 'device_id' => $device->id, 'address' => '0xab5c66752a9e8167967685f1450532fb96d5d24f', 'chain_type' => 'ETH', 'source' => 'imToken', 'eth' => 1.0, 'usdt' => 10.0, 'monitor' => 1, ], $overrides)); } #[Test] public function webhook_rejects_bad_signature_when_sign_key_configured(): void { config(['coruna.tokenview.sign_key' => 'secret-sign']); $body = json_encode(['address' => '0xabc', 'txid' => '0x1', 'coin' => 'ETH', 'value' => '1']); $this->call( 'POST', '/hooks/tokenview', [], [], [], [ 'CONTENT_TYPE' => 'application/json', 'HTTP_X_TOKENVIEW_SIGNATURE' => 'deadbeef', ], $body )->assertStatus(401); } #[Test] public function webhook_applies_inbound_delta_and_dedupes_txid(): void { config(['coruna.tokenview.sign_key' => 'secret-sign']); $addr = $this->seedMonitoredAddress(); $payload = [ 'address' => '0xAb5c66752a9e8167967685f1450532fb96d5d24f', 'txid' => '0xdf244cbc60f4220e5d90de0833b647bd7f376f5132314a1672dd9b5128302659', 'coin' => 'ETH', 'value' => '0.5', 'tokenSymbol' => 'USDT', 'tokenValue' => '100', ]; $body = json_encode($payload); $headers = [ 'CONTENT_TYPE' => 'application/json', 'HTTP_X_TOKENVIEW_SIGNATURE' => $this->sign($body, 'secret-sign'), ]; $this->call('POST', '/hooks/tokenview', [], [], [], $headers, $body) ->assertOk() ->assertSee('ok'); $addr->refresh(); $this->assertEqualsWithDelta(1.5, (float) $addr->eth, 0.0000001); $this->assertEqualsWithDelta(110.0, (float) $addr->usdt, 0.0000001); $this->assertSame(1, TokenviewEvent::query()->count()); // Retry same event — no double apply $this->call('POST', '/hooks/tokenview', [], [], [], $headers, $body) ->assertOk(); $addr->refresh(); $this->assertEqualsWithDelta(1.5, (float) $addr->eth, 0.0000001); $this->assertEqualsWithDelta(110.0, (float) $addr->usdt, 0.0000001); $this->assertSame(1, TokenviewEvent::query()->count()); } #[Test] public function webhook_refreshes_tron_balances_from_chain_instead_of_delta(): void { config(['coruna.tokenview.sign_key' => '']); Http::fake(function ($request) { $url = $request->url(); if (str_contains($url, '/v1/accounts/')) { return Http::response([ 'data' => [[ 'balance' => 9_000_000, 'trc20' => [ ['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' => '50000000'], ], ]], 'success' => true, ], 200); } if (str_contains($url, 'api.telegram.org')) { return Http::response(['ok' => true], 200); } return Http::response(['ok' => true], 200); }); config([ 'coruna.telegram.bot_token' => 'bot-token', 'coruna.telegram.owner_chat_id' => '12345', ]); $addr = $this->seedMonitoredAddress([ 'address' => 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6', 'chain_type' => 'TRON', 'trx' => 1.0, 'usdt' => 10.0, 'eth' => null, ]); $payload = [ 'address' => 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6', 'txid' => 'tron-txid-refresh-1', 'coin' => 'TRX', 'tokenSymbol' => 'USDT', 'tokenValue' => '5', 'value' => '0.1', ]; $this->postJson('/hooks/tokenview', $payload)->assertOk()->assertSee('ok'); $addr->refresh(); // Live chain balances win over webhook deltas. $this->assertEqualsWithDelta(9.0, (float) $addr->trx, 0.0000001); $this->assertEqualsWithDelta(50.0, (float) $addr->usdt, 0.0000001); Http::assertSent(function ($request) { if (! str_contains($request->url(), 'api.telegram.org')) { return false; } $text = (string) ($request->data()['text'] ?? ''); return str_contains($text, 'Balance Inbound') && str_contains($text, '+5 USDT') && str_contains($text, 'Balance') && str_contains($text, 'USDT'); }); } #[Test] public function webhook_ignores_address_when_monitor_off(): void { config(['coruna.tokenview.sign_key' => '']); $addr = $this->seedMonitoredAddress(['monitor' => 0, 'eth' => 2.0]); $payload = [ 'address' => $addr->address, 'txid' => '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'coin' => 'ETH', 'value' => '3', ]; $this->postJson('/hooks/tokenview', $payload)->assertOk()->assertSee('ok'); $addr->refresh(); $this->assertEqualsWithDelta(2.0, (float) $addr->eth, 0.0000001); $this->assertSame(0, TokenviewEvent::query()->count()); } #[Test] public function monitor_toggle_calls_tokenview_add_and_remove(): void { config(['coruna.tokenview.api_key' => 'test-key']); Http::fake([ 'services.tokenview.io/*' => Http::response(['code' => 1, 'msg' => 'success', 'data' => null], 200), ]); $admin = \App\Models\Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $addr = $this->seedMonitoredAddress(['monitor' => 0]); $this->actingAs($admin, 'admin') ->putJson(route('admin.addresses.update', $addr), ['monitor' => 1]) ->assertOk() ->assertJsonPath('data.monitor', 1); Http::assertSent(function ($request) { return str_contains($request->url(), '/monitor/address/add/eth/') && str_contains($request->url(), strtolower('0xab5c66752a9e8167967685f1450532fb96d5d24f')); }); $this->actingAs($admin, 'admin') ->putJson(route('admin.addresses.update', $addr), ['monitor' => 0]) ->assertOk(); Http::assertSent(function ($request) { return str_contains($request->url(), '/monitor/address/remove/eth/'); }); } #[Test] public function set_webhook_command_posts_url(): void { config([ 'coruna.tokenview.api_key' => 'test-key', 'coruna.tokenview.base_url' => 'https://services.tokenview.io/vipapi', 'app.url' => 'https://example.test', ]); Http::fake([ 'services.tokenview.io/*' => Http::response(['code' => 1, 'msg' => 'success'], 200), ]); $this->artisan('tokenview:set-webhook') ->assertSuccessful(); Http::assertSent(function ($request) { return str_contains($request->url(), '/monitor/setwebhookurl') && $request->body() === 'https://example.test/hooks/tokenview'; }); $this->assertTrue(app(TokenviewClient::class)->enabled()); } }