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 },
};
}