Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions src/design/App/UI/Shared/PaymentFlow/PaymentFlowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -398,15 +398,15 @@ 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;
}
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;
Expand All @@ -429,6 +429,24 @@ private async Task GenerateReceiveAddressAsync()
}
}

/// <summary>
/// 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).
/// </summary>
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
// ═══════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 0 additions & 2 deletions src/sdk/Angor.Sdk/Common/NetworkConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ public List<SettingsUrl> 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 },
};
}

Expand Down
39 changes: 32 additions & 7 deletions src/shared/Angor.Shared/Services/MempoolSpaceIndexerApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> PublishTransactionAsync(string trxHex)
{
var guardError = TransactionGuard.RejectAllZeroP2trOutputs(trxHex);
Expand Down Expand Up @@ -188,9 +195,17 @@ public async Task<AddressBalance[]> GetAdressBalancesAsync(List<AddressInfo> 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<AddressBalance>();

Expand All @@ -199,7 +214,7 @@ public async Task<AddressBalance[]> GetAdressBalancesAsync(List<AddressInfo> 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<AddressResponse>(new JsonSerializerOptions()
{ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower });
Expand All @@ -223,11 +238,21 @@ public async Task<AddressBalance[]> GetAdressBalancesAsync(List<AddressInfo> 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<List<MempoolTransaction>>(new JsonSerializerOptions()
{ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower });
Expand Down
18 changes: 9 additions & 9 deletions src/shared/Angor.Shared/Services/NostrCommunicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}));

Expand All @@ -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}");
}
}));

Expand Down Expand Up @@ -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);
}));

Expand All @@ -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}");
}
}));

Expand Down Expand Up @@ -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}",
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
2 changes: 0 additions & 2 deletions src/webapp/Angor.Client/NetworkConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,6 @@ public List<SettingsUrl> 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 },
};
}

Expand Down
Loading