From 2593155fb7fe0c707972aa61d61c5b7484f74e98 Mon Sep 17 00:00:00 2001 From: dangershony Date: Sat, 25 Jul 2026 23:25:53 +0100 Subject: [PATCH] Improve indexer failure handling and reduce relay log noise - Remove yakihonne relays from default mainnet relay lists (SDK + webapp) - Demote relay disconnect logging: single-line warning instead of error + stack trace on every reconnect attempt; EOSE/OK tracking messages demoted to debug - Log which indexer is in use and include the indexer host in address-balance/UTXO failure messages so bug reports name the endpoint - Increase indexer HttpClient timeout from 10s to 30s (cold Fulcrum/ electrs can be slow; the gap-scan fans out many requests at once) - PaymentFlow: distinguish indexer/network failures from wallet issues in the user-facing error (point to Settings to switch indexer) and stop overwriting the real cause with 'wallet wasn't ready' --- .../PaymentFlow/PaymentFlowViewModel.cs | 28 +++++++++++-- .../Angor.Sdk/Common/NetworkConfiguration.cs | 2 - .../Services/MempoolSpaceIndexerApi.cs | 39 +++++++++++++++---- .../Services/NostrCommunicationFactory.cs | 18 ++++----- .../Angor.Client/NetworkConfiguration.cs | 2 - 5 files changed, 65 insertions(+), 24 deletions(-) diff --git a/src/design/App/UI/Shared/PaymentFlow/PaymentFlowViewModel.cs b/src/design/App/UI/Shared/PaymentFlow/PaymentFlowViewModel.cs index 6ede82104..0f6b4051c 100644 --- a/src/design/App/UI/Shared/PaymentFlow/PaymentFlowViewModel.cs +++ b/src/design/App/UI/Shared/PaymentFlow/PaymentFlowViewModel.cs @@ -383,7 +383,7 @@ private async Task GenerateReceiveAddressAsync() catch (Exception ex) { _logger.LogWarning(ex, "RefreshAllBalancesAsync failed"); - ErrorMessage = "We couldn't prepare a receive address for this payment. Unlock the wallet and try again."; + ErrorMessage = DescribeAddressFailure(ex.Message); IsProcessing = false; _addressReadyTcs.TrySetCanceled(); return; @@ -398,7 +398,7 @@ private async Task GenerateReceiveAddressAsync() catch (Exception ex) { _logger.LogError(ex, "GetNextReceiveAddress threw"); - ErrorMessage = "We couldn't prepare a receive address for this payment. Unlock the wallet and try again."; + ErrorMessage = DescribeAddressFailure(ex.Message); IsProcessing = false; _addressReadyTcs.TrySetCanceled(); return; @@ -406,7 +406,7 @@ private async Task GenerateReceiveAddressAsync() if (addressResult.IsFailure) { _logger.LogError("GetNextReceiveAddress failed: {Error}", addressResult.Error); - ErrorMessage = "We couldn't prepare a receive address for this payment. Unlock the wallet and try again."; + ErrorMessage = DescribeAddressFailure(addressResult.Error); IsProcessing = false; _addressReadyTcs.TrySetCanceled(); return; @@ -429,6 +429,24 @@ private async Task GenerateReceiveAddressAsync() } } + /// + /// Builds a user-facing message for a failed receive-address preparation. + /// Distinguishes indexer/network failures (actionable: switch indexer in Settings) + /// from wallet issues (actionable: unlock the wallet). + /// + private static string DescribeAddressFailure(string? detail) + { + var isNetworkIssue = detail != null && + (detail.Contains("Indexer", StringComparison.OrdinalIgnoreCase) || + detail.Contains("timeout", StringComparison.OrdinalIgnoreCase) || + detail.Contains("canceled", StringComparison.OrdinalIgnoreCase) || + detail.Contains("HttpRequestException", StringComparison.OrdinalIgnoreCase)); + + return isNetworkIssue + ? $"We couldn't reach the Bitcoin indexer to prepare a receive address. Check your internet connection or select a different indexer in Settings, then try again. ({detail})" + : "We couldn't prepare a receive address for this payment. Unlock the wallet and try again."; + } + // ═══════════════════════════════════════════════════════════════════ // Pending Swap Recovery // ═══════════════════════════════════════════════════════════════════ @@ -677,7 +695,9 @@ private async Task PayToOnChainAddressAsync() var wallet = Wallets.FirstOrDefault(); if (wallet?.Id is null || string.IsNullOrEmpty(OnChainAddress)) { - ErrorMessage = "We couldn't start watching for your payment because the wallet wasn't ready. Please try again."; + // If address generation already reported a specific error (e.g. indexer + // unreachable), keep it — it tells the user how to fix the problem. + ErrorMessage ??= "We couldn't start watching for your payment because no receive address is available yet. Please try again."; return; } diff --git a/src/sdk/Angor.Sdk/Common/NetworkConfiguration.cs b/src/sdk/Angor.Sdk/Common/NetworkConfiguration.cs index fe7050582..a840ed518 100644 --- a/src/sdk/Angor.Sdk/Common/NetworkConfiguration.cs +++ b/src/sdk/Angor.Sdk/Common/NetworkConfiguration.cs @@ -79,8 +79,6 @@ public List GetDefaultRelayUrls() new() { Name = "wss://relay2.angor.io", Url = "wss://relay2.angor.io", IsPrimary = true }, new() { Name = "wss://relay.damus.io", Url = "wss://relay.damus.io", IsPrimary = true }, new() { Name = "wss://nos.lol", Url = "wss://nos.lol", IsPrimary = true }, - new() { Name = "wss://nostr-01.yakihonne.com", Url = "wss://nostr-01.yakihonne.com", IsPrimary = true }, - new() { Name = "wss://nostr-02.yakihonne.com", Url = "wss://nostr-02.yakihonne.com", IsPrimary = true }, }; } diff --git a/src/shared/Angor.Shared/Services/MempoolSpaceIndexerApi.cs b/src/shared/Angor.Shared/Services/MempoolSpaceIndexerApi.cs index 57122af4d..148c52a2c 100644 --- a/src/shared/Angor.Shared/Services/MempoolSpaceIndexerApi.cs +++ b/src/shared/Angor.Shared/Services/MempoolSpaceIndexerApi.cs @@ -131,13 +131,20 @@ private HttpClient GetIndexerClient() var client = _clientFactory.CreateClient(key); client.BaseAddress = new Uri(indexer.Url); - client.Timeout = TimeSpan.FromSeconds(10); + // 30s rather than 10s: cold indexers (Fulcrum/electrs) can be slow to answer + // address queries, and the wallet gap-scan fans out many requests at once — + // a single slow response would otherwise fail receive-address generation. + client.Timeout = TimeSpan.FromSeconds(30); _clients.TryAdd(key, client); + _logger.LogInformation("Using indexer {IndexerUrl}", indexer.Url); + return client; } + private static string IndexerHost(HttpClient client) => client.BaseAddress?.Host ?? "unknown"; + public async Task PublishTransactionAsync(string trxHex) { var guardError = TransactionGuard.RejectAllZeroP2trOutputs(trxHex); @@ -188,9 +195,17 @@ public async Task GetAdressBalancesAsync(List dat var urlBalance = $"{MempoolApiRoute}/address/"; var client = GetIndexerClient(); // Call once, reuse for all requests - var tasks = data.Select(x => client.GetAsync(urlBalance + x.Address)); - - var results = await Task.WhenAll(tasks); + HttpResponseMessage[] results; + try + { + var tasks = data.Select(x => client.GetAsync(urlBalance + x.Address)); + results = await Task.WhenAll(tasks); + } + catch (Exception ex) + { + _logger.LogWarning("Address balance request to indexer {IndexerHost} failed: {Message}", IndexerHost(client), ex.Message); + throw new InvalidOperationException($"Indexer {IndexerHost(client)} did not respond: {ex.Message}", ex); + } var response = new List(); @@ -199,7 +214,7 @@ public async Task GetAdressBalancesAsync(List dat _networkService.CheckAndHandleError(apiResponse); if (!apiResponse.IsSuccessStatusCode) - throw new InvalidOperationException(apiResponse.ReasonPhrase); + throw new InvalidOperationException($"Indexer {IndexerHost(client)} returned an error: {apiResponse.ReasonPhrase}"); var addressResponse = await apiResponse.Content.ReadFromJsonAsync(new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); @@ -223,11 +238,21 @@ public async Task GetAdressBalancesAsync(List dat var client = GetIndexerClient(); // Call once, reuse for all requests var txsUrl = $"{MempoolApiRoute}/address/{address}/txs"; - var response = await client.GetAsync(txsUrl); + HttpResponseMessage response; + try + { + response = await client.GetAsync(txsUrl); + } + catch (Exception ex) + { + _logger.LogWarning("UTXO request to indexer {IndexerHost} failed: {Message}", IndexerHost(client), ex.Message); + throw new InvalidOperationException($"Indexer {IndexerHost(client)} did not respond: {ex.Message}", ex); + } + _networkService.CheckAndHandleError(response); if (!response.IsSuccessStatusCode) - throw new InvalidOperationException(response.ReasonPhrase); + throw new InvalidOperationException($"Indexer {IndexerHost(client)} returned an error: {response.ReasonPhrase}"); var trx = await response.Content.ReadFromJsonAsync>(new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); diff --git a/src/shared/Angor.Shared/Services/NostrCommunicationFactory.cs b/src/shared/Angor.Shared/Services/NostrCommunicationFactory.cs index 3d36803d7..32a771dfd 100644 --- a/src/shared/Angor.Shared/Services/NostrCommunicationFactory.cs +++ b/src/shared/Angor.Shared/Services/NostrCommunicationFactory.cs @@ -77,7 +77,7 @@ public INostrClient GetOrCreateDiscoveryClients(INetworkService networkService) var tryRemove = clientsReceivedList.TryRemove(x.CommunicatorName, out _); - _logger.LogWarning("EOSE {x.Subscription} removed {x.CommunicatorName} - {tryRemove}", + _logger.LogDebug("EOSE {x.Subscription} removed {x.CommunicatorName} - {tryRemove}", x.Subscription, x.CommunicatorName, tryRemove); })); @@ -87,7 +87,7 @@ public INostrClient GetOrCreateDiscoveryClients(INetworkService networkService) if (_okCalledOnSubscriptionClients.TryGetValue(x.EventId ?? string.Empty, out var clientsReceivedList)) { var tryRemove = clientsReceivedList.TryRemove(x.CommunicatorName, out _); - _logger.LogWarning($"OK {x.EventId} accepted: {x.Accepted} removed ok {x.CommunicatorName} - {tryRemove}"); + _logger.LogDebug($"OK {x.EventId} accepted: {x.Accepted} removed ok {x.CommunicatorName} - {tryRemove}"); } })); @@ -151,7 +151,7 @@ private void ConnectToAllRelaysInTheSettings(INetworkService networkService) var tryRemove = clientsReceivedList.TryRemove(x.CommunicatorName, out _); - _logger.LogWarning("EOSE {x.Subscription} removed {x.CommunicatorName} - {tryRemove}", + _logger.LogDebug("EOSE {x.Subscription} removed {x.CommunicatorName} - {tryRemove}", x.Subscription, x.CommunicatorName, tryRemove); })); @@ -161,7 +161,7 @@ private void ConnectToAllRelaysInTheSettings(INetworkService networkService) if (_okCalledOnSubscriptionClients.TryGetValue(x.EventId ?? string.Empty, out var clientsReceivedList)) { var tryRemove = clientsReceivedList.TryRemove(x.CommunicatorName, out _); - _logger.LogWarning($"OK {x.EventId} accepted: {x.Accepted} removed ok {x.CommunicatorName} - {tryRemove}"); + _logger.LogDebug($"OK {x.EventId} accepted: {x.Accepted} removed ok {x.CommunicatorName} - {tryRemove}"); } })); @@ -259,9 +259,9 @@ public INostrCommunicator CreateCommunicator(string uri, string relayName) _serviceSubscriptions.Add(nostrCommunicator.DisconnectionHappened.Subscribe(e => { if (e.Exception != null) - _logger.LogError(e.Exception, - "Relay {relayName} disconnected, type: {Type}, reason: {CloseStatusDescription}", - relayName, e.Type, e.CloseStatusDescription); + _logger.LogWarning( + "Relay {relayName} disconnected, type: {Type}, reason: {Reason}", + relayName, e.Type, e.CloseStatusDescription ?? e.Exception.Message); else _logger.LogDebug( "Relay {relayName} disconnected, type: {Type}, reason: {CloseStatusDescription}", @@ -273,7 +273,7 @@ public INostrCommunicator CreateCommunicator(string uri, string relayName) { if (kvp.Value.TryRemove(relayName, out _)) { - _logger.LogWarning( + _logger.LogDebug( "Removed disconnected relay {RelayName} from EOSE tracking for subscription {Subscription}", relayName, kvp.Key); } @@ -283,7 +283,7 @@ public INostrCommunicator CreateCommunicator(string uri, string relayName) { if (kvp.Value.TryRemove(relayName, out _)) { - _logger.LogWarning( + _logger.LogDebug( "Removed disconnected relay {RelayName} from OK tracking for event {EventId}", relayName, kvp.Key); } diff --git a/src/webapp/Angor.Client/NetworkConfiguration.cs b/src/webapp/Angor.Client/NetworkConfiguration.cs index 397c818c0..44e47a79f 100644 --- a/src/webapp/Angor.Client/NetworkConfiguration.cs +++ b/src/webapp/Angor.Client/NetworkConfiguration.cs @@ -118,8 +118,6 @@ public List GetDefaultRelayUrls() new SettingsUrl { Name = "", Url = "wss://relay2.angor.io", IsPrimary = true }, new SettingsUrl { Name = "", Url = "wss://relay.damus.io", IsPrimary = true }, new SettingsUrl { Name = "", Url = "wss://nos.lol", IsPrimary = true }, - new SettingsUrl { Name = "", Url = "wss://nostr-01.yakihonne.com", IsPrimary = true }, - new SettingsUrl { Name = "", Url = "wss://nostr-02.yakihonne.com", IsPrimary = true }, }; }