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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public interface INostrCommunicationFactory
void CloseClientConnection();
int GetNumberOfRelaysConnected();
bool EoseEventReceivedOnAllRelays(string subscription);
bool MonitoringEoseReceivedOnSubscription(string subscription);
bool MonitoringEoseReceivedOnSubscription(string subscription, bool includeDiscoveryRelays = false);
void ClearEoseReceivedOnSubscriptionMonitoring(string subscription);
bool OkEventReceivedOnAllRelays(string eventId);
void MonitoringOkReceivedOnSubscription(string eventId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public interface IRelaySubscriptionsHandling
{
bool TryAddOKAction(string eventId, Action<NostrOkResponse> action);
void HandleOkMessages(NostrOkResponse _);
bool TryAddEoseAction(string subscriptionName, Action action);
bool TryAddEoseAction(string subscriptionName, Action action, bool includeDiscoveryRelays = false);
void HandleEoseMessages(NostrEoseResponse _);
bool RelaySubscriptionAdded(string subscriptionKey);
bool TryAddRelaySubscription(string subscriptionKey, IDisposable subscription, bool keepActive = false);
Expand Down
28 changes: 17 additions & 11 deletions src/shared/Angor.Shared/Services/NostrCommunicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,31 @@ public NostrCommunicationFactory(ILogger<NostrWebsocketClient> clientLogger, ILo
_okCalledOnSubscriptionClients = new();
}

private ConcurrentDictionary<string, byte> GetAllConnectedRelayNames()
private ConcurrentDictionary<string, byte> GetAllConnectedRelayNames(bool includeDiscoveryRelays = false)
{
var allRelays = new ConcurrentDictionary<string, byte>();

if (_nostrMultiWebsocketClient != null)
{
foreach (var client in _nostrMultiWebsocketClient.Clients)
{
allRelays.TryAdd(client.Communicator.Name, 0);
// Only track relays whose websocket is actually running. A relay that never
// completed the WS upgrade will never send EOSE, and its DisconnectionHappened
// already fired before any subscription was monitored — so including it here
// would block the "all relays sent EOSE" completion check forever.
if (client.Communicator.IsRunning)
allRelays.TryAdd(client.Communicator.Name, 0);
}
}

// if (_nostrMultiWebsocketClientDiscovery != null)
// {
// foreach (var client in _nostrMultiWebsocketClientDiscovery.Clients)
// {
// allRelays.Add(client.Communicator.Name);
// }
// }
if (includeDiscoveryRelays && _nostrMultiWebsocketClientDiscovery != null)
{
foreach (var client in _nostrMultiWebsocketClientDiscovery.Clients)
{
if (client.Communicator.IsRunning)
allRelays.TryAdd(client.Communicator.Name, 0);
}
}

return allRelays;
}
Expand Down Expand Up @@ -190,10 +196,10 @@ public bool EoseEventReceivedOnAllRelays(string subscription)
return response;
}

public bool MonitoringEoseReceivedOnSubscription(string subscription)
public bool MonitoringEoseReceivedOnSubscription(string subscription, bool includeDiscoveryRelays = false)
{
_logger.LogDebug($"Started monitoring subscription {subscription}");
var relayNames = GetAllConnectedRelayNames();
var relayNames = GetAllConnectedRelayNames(includeDiscoveryRelays);
if (_eoseCalledOnSubscriptionClients.TryAdd(subscription, relayNames))
return true;

Expand Down
13 changes: 10 additions & 3 deletions src/shared/Angor.Shared/Services/RelayService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,16 @@ public void LookupLatestProjects<T>(Action<EventInfo<T>> onResponseAction, Actio
public void LookupRelayListForNPubs(Action<string, List<NostrEventTag>> onResponse, Action onEndOfStream, params string[] npubs)
{
var client = _communicationFactory.GetOrCreateClient(_networkService);
// NIP-65 relay lists (kind 10002) are published to the discovery ("purple pages") relays,
// so query them alongside the regular relays — the account's relay list is often only there.
var discoveryClient = _communicationFactory.GetOrCreateDiscoveryClients(_networkService);

var subscriptionKey = Guid.NewGuid().ToString().Replace("-", "");

if (!_subscriptionsHandling.RelaySubscriptionAdded(subscriptionKey))
{
var subscription = client.Streams.EventStream
.Merge(discoveryClient.Streams.EventStream)
.Where(_ => _.Subscription == subscriptionKey)
.Where(_ => _.Event is not null)
.Select(_ => _.Event)
Expand All @@ -404,14 +408,17 @@ public void LookupRelayListForNPubs(Action<string, List<NostrEventTag>> onRespon

if (onEndOfStream != null)
{
_subscriptionsHandling.TryAddEoseAction(subscriptionKey, onEndOfStream);
_subscriptionsHandling.TryAddEoseAction(subscriptionKey, onEndOfStream, includeDiscoveryRelays: true);
}

client.Send(new NostrRequest(subscriptionKey, new NostrFilter
var request = new NostrRequest(subscriptionKey, new NostrFilter
{
Authors = npubs,
Kinds = [NostrKind.RelayListMetadata],
}));
});

client.Send(request);
discoveryClient.Send(request);
}

public async Task<ProjectMetadata?> FetchProfileMetadataAsync(string nostrPubKeyHex)
Expand Down
10 changes: 8 additions & 2 deletions src/shared/Angor.Shared/Services/RelaySubscriptionsHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@ public void HandleOkMessages(NostrOkResponse okResponse)
_communicationFactory.ClearOkReceivedOnSubscriptionMonitoring(okResponse.EventId);
}

public bool TryAddEoseAction(string subscriptionName, Action action)
public bool TryAddEoseAction(string subscriptionName, Action action, bool includeDiscoveryRelays = false)
{
if (action == null) throw new ArgumentNullException(nameof(action));

var add = _communicationFactory.MonitoringEoseReceivedOnSubscription(subscriptionName);
var add = _communicationFactory.MonitoringEoseReceivedOnSubscription(subscriptionName, includeDiscoveryRelays);

if (!add)
_logger.LogDebug($"Subscription {subscriptionName} is already being monitored");
Expand Down Expand Up @@ -190,6 +190,12 @@ public void CloseSubscription(string subscriptionKey)
_communicationFactory
.GetOrCreateClient(_networkService)
.Send(new NostrCloseRequest(subscriptionKey));

// Some lookups (e.g. NIP-65 relay lists) also send the REQ to the discovery relays;
// closing there too is harmless when the subscription was never opened on them.
_communicationFactory
.GetOrCreateDiscoveryClients(_networkService)
.Send(new NostrCloseRequest(subscriptionKey));

subscription.Dispose();
relaySubscriptionsKeepActive.Remove(subscriptionKey, out _);
Expand Down
49 changes: 46 additions & 3 deletions src/webapp/Angor.Client/Pages/InvestView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,23 @@
<img src="/assets/img/angor-logo.svg" alt="Angor" />
<span>Angor Invest</span>
</a>
<span class="hub-network-badge @(network.IsMainnet ? "" : "testnet")">@network.Name</span>
<span class="d-flex align-items-center gap-3">
<span class="hub-network-badge @(network.IsMainnet ? "" : "testnet")">@network.Name</span>
<a class="hub-link" href="/settings" title="Relay and indexer settings">
<Icon IconName="settings" Width="18" Height="18" />
<span class="d-none d-sm-inline ms-1">Settings</span>
</a>
</span>
</header>

@if (loadingProject)
{
<div class="container mt-5 text-center">
<div class="spinner-border text-success" role="status"></div>
<p class="mt-3" style="color: var(--text-secondary);">Loading project...</p>
<p class="small text-muted">
Taking too long? Check your <a class="hub-link" href="/settings">relay and indexer settings</a>.
</p>
</div>
return;
}
Expand All @@ -74,8 +83,9 @@
<Icon IconName="info" Width="24" Height="24" class="me-2" />
<span>@(loadError ?? "The project was not found.")</span>
</div>
<div class="text-center mt-3">
<div class="text-center mt-3 d-flex justify-content-center gap-3 flex-wrap">
<a class="hub-link" href="https://angor.io" target="_blank" rel="noopener">Browse projects on angor.io</a>
<a class="hub-link" href="/settings">Check relay &amp; indexer settings</a>
</div>
</div>
return;
Expand Down Expand Up @@ -918,6 +928,7 @@

private Project? project;
private bool loadingProject = true;
private static readonly TimeSpan ProjectLoadTimeout = TimeSpan.FromSeconds(30);
private string? loadError;
private bool buildSpinner;
private bool investSpinner;
Expand Down Expand Up @@ -1150,6 +1161,8 @@
/// </summary>
private async Task LoadRemoteProjectAsync()
{
StartProjectLoadWatchdog();

try
{
var projectIndexerData = await _IndexerService.GetProjectByIdAsync(ProjectId);
Expand Down Expand Up @@ -1228,6 +1241,36 @@
}
}

/// <summary>
/// Overall timeout for the remote project load. The Nostr EOSE completion callback can be
/// lost when relays disconnect mid-flight (or never connect at all), which would leave the
/// page on the loading spinner forever — fall back to an error message instead.
/// </summary>
private void StartProjectLoadWatchdog()
{
_ = Task.Run(async () =>
{
await Task.Delay(ProjectLoadTimeout);

if (!loadingProject)
return;

_Logger.LogWarning("Project load timed out after {Timeout}s — relays did not complete", ProjectLoadTimeout.TotalSeconds);

await InvokeAsync(() =>
{
if (!loadingProject)
return;

if (project?.ProjectInfo == null)
loadError = "Could not load the project from the relays. Please check your connection and refresh the page.";

loadingProject = false;
StateHasChanged();
});
});
}

/// <summary>
/// Validate the configured network against the project's NetworkName (from the Nostr event).
/// Returns true when the networks match. When they don't: switches network automatically if
Expand Down Expand Up @@ -1378,7 +1421,7 @@
_Logger.LogInformation("Merged {Count} relays from project profile", relays.Count);
}
},
null,
() => _Logger.LogDebug("NIP-65 relay list lookup completed (EOSE from all relays incl. discovery)"),
nostrPubKey);
}
catch (Exception ex)
Expand Down
Loading