From bcb24fa6e0a77e053f1cc0b569a214af2f9a03d5 Mon Sep 17 00:00:00 2001 From: TheDude Date: Wed, 13 Dec 2023 16:59:15 +0000 Subject: [PATCH 1/2] Change NOSTR client to multi connections (#26) * Work on multiple relay nostr client * Moved common logic to base relay subscription handling * Fixed issues with OnStateChanged in the signatures page * Fixed race condition in the loading of sigantures page and added loader div * Added guard in the founder page for receiving the same event from multiple relays --- .../Components/FounderProjectItem.razor | 2 - src/Angor/Client/Pages/Browse.razor | 2 - src/Angor/Client/Pages/Create.razor | 2 - src/Angor/Client/Pages/Founder.razor | 6 +- src/Angor/Client/Pages/Invest.razor | 3 + src/Angor/Client/Pages/Penalties.razor | 5 - src/Angor/Client/Pages/Settings.razor | 4 +- src/Angor/Client/Pages/Signatures.razor | 183 ++++++++---------- src/Angor/Client/Pages/Wallet.razor | 1 - src/Angor/Client/Program.cs | 6 +- src/Angor/Shared/DerivationOperations.cs | 16 ++ src/Angor/Shared/IDerivationOperations.cs | 1 + src/Angor/Shared/Models/NostrRelayInfo.cs | 21 ++ .../Services/INostrCommunicationFactory.cs | 87 +-------- src/Angor/Shared/Services/IRelayService.cs | 1 - src/Angor/Shared/Services/ISignService.cs | 17 ++ src/Angor/Shared/Services/NetworkService.cs | 8 + .../Services/NostrCommunicationFactory.cs | 88 +++++++++ src/Angor/Shared/Services/RelayService.cs | 173 ++++------------- .../Services/RelaySubscriptionsHanding.cs | 92 +++++++++ src/Angor/Shared/Services/SignService.cs | 129 ++++-------- 21 files changed, 416 insertions(+), 431 deletions(-) create mode 100644 src/Angor/Shared/Models/NostrRelayInfo.cs create mode 100644 src/Angor/Shared/Services/ISignService.cs create mode 100644 src/Angor/Shared/Services/NostrCommunicationFactory.cs create mode 100644 src/Angor/Shared/Services/RelaySubscriptionsHanding.cs diff --git a/src/Angor/Client/Components/FounderProjectItem.razor b/src/Angor/Client/Components/FounderProjectItem.razor index 857cbb78..e22af430 100644 --- a/src/Angor/Client/Components/FounderProjectItem.razor +++ b/src/Angor/Client/Components/FounderProjectItem.razor @@ -35,8 +35,6 @@ protected override async Task OnInitializedAsync() { - await RelayService.ConnectToRelaysAsync(); - await RelayService.LookupDirectMessagesForPubKeyAsync(FounderProject.ProjectInfo.NostrPubKey, FounderProject.LastRequestForSignaturesTime, 1, _ => { diff --git a/src/Angor/Client/Pages/Browse.razor b/src/Angor/Client/Pages/Browse.razor index 43ce298c..b7b847cd 100644 --- a/src/Angor/Client/Pages/Browse.razor +++ b/src/Angor/Client/Pages/Browse.razor @@ -73,8 +73,6 @@ protected override async Task OnInitializedAsync() { - await _RelayService.ConnectToRelaysAsync(); - projects = SessionStorage.GetProjectIndexerData() ?? new(); } diff --git a/src/Angor/Client/Pages/Create.razor b/src/Angor/Client/Pages/Create.razor index c9908796..938184b4 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -247,8 +247,6 @@ project.NostrPubKey = projectsKeys.NostrPubKey; } - await _RelayService.ConnectToRelaysAsync(); - _RelayService.RequestProjectCreateEventsByPubKey(_ => //TODO change the state to be storage driven and not event driven { nostrProfileCreated = _.Kind == NostrKind.Metadata; diff --git a/src/Angor/Client/Pages/Founder.razor b/src/Angor/Client/Pages/Founder.razor index ca29017b..23c9b45e 100644 --- a/src/Angor/Client/Pages/Founder.razor +++ b/src/Angor/Client/Pages/Founder.razor @@ -97,7 +97,6 @@ private async Task LookupProjectKeysOnIndexerAsync() { scanningForProjects = true; - await RelayService.ConnectToRelaysAsync(); var keys = storage.GetFounderKeys(); @@ -124,14 +123,15 @@ case { Kind: NostrKind.Metadata }: var nostrMetadata = JsonSerializer.Deserialize(e.Content, Angor.Shared.Services.RelayService.settings); var founderProject = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey); - if (founderProject != null) + if (founderProject != null && founderProject.Metadata is null) founderProject.Metadata = nostrMetadata; else notificationComponent.ShowNotificationMessage($"Couldn't find the project details for the project {nostrMetadata.Name} try adding the missing relay."); //TODO break; case { Kind: NostrKind.ApplicationSpecificData }: var projectInfo = JsonSerializer.Deserialize(e.Content, Angor.Shared.Services.RelayService.settings); - founderProjects.Add(new FounderProject { ProjectInfo = projectInfo }); + if(founderProjects.All(_ => _.ProjectInfo.NostrPubKey != e.Pubkey)) //Getting events from multiple relays + founderProjects.Add(new FounderProject { ProjectInfo = projectInfo }); break; } }, diff --git a/src/Angor/Client/Pages/Invest.razor b/src/Angor/Client/Pages/Invest.razor index ea7d1179..77e99c62 100644 --- a/src/Angor/Client/Pages/Invest.razor +++ b/src/Angor/Client/Pages/Invest.razor @@ -472,6 +472,9 @@ private async Task HandleSignatureReceivedAsync(string? nostrPrivateKeyHex, string _) { + if (recoverySigs?.Signatures.Any() ?? false) //multiple relays for the same message + return; + var signatureJson = await javascriptNostrToolsModule.InvokeAsync( "decryptNostr", nostrPrivateKeyHex, project.NostrPubKey, _); diff --git a/src/Angor/Client/Pages/Penalties.razor b/src/Angor/Client/Pages/Penalties.razor index c694a3ce..b8a1e692 100644 --- a/src/Angor/Client/Pages/Penalties.razor +++ b/src/Angor/Client/Pages/Penalties.razor @@ -1,17 +1,12 @@ @page "/penalties" -@using Angor.Shared @using Angor.Client.Storage @using Angor.Shared.Models @using Angor.Client.Services @using Blockcore.Consensus.ScriptInfo @using Blockcore.NBitcoin -@inject HttpClient Http -@inject IDerivationOperations _derivationOperations -@inject IWalletStorage _walletStorage; @inject IClientStorage storage; @inject NavigationManager NavigationManager -@inject INetworkConfiguration _NetworkConfiguration @inject IIndexerService _IndexerService @inherits BaseComponent diff --git a/src/Angor/Client/Pages/Settings.razor b/src/Angor/Client/Pages/Settings.razor index f6a1f418..bdb762e6 100644 --- a/src/Angor/Client/Pages/Settings.razor +++ b/src/Angor/Client/Pages/Settings.razor @@ -86,6 +86,7 @@ Link + Name Status Default @@ -96,7 +97,8 @@ { @relay.Url - @relay.Status.ToString() + @relay.Name + @relay.Status.ToString() @if (relay.IsPrimary) { diff --git a/src/Angor/Client/Pages/Signatures.razor b/src/Angor/Client/Pages/Signatures.razor index 1fcb8659..a4cf1e89 100644 --- a/src/Angor/Client/Pages/Signatures.razor +++ b/src/Angor/Client/Pages/Signatures.razor @@ -4,23 +4,20 @@ @using Angor.Shared.Models @using Angor.Client.Services @using Angor.Shared.ProtocolNew -@using Angor.Shared.Services @using Angor.Client.Models @using Blockcore.NBitcoin @using Blockcore.NBitcoin.DataEncoders @using System.Text.Json -@using Angor.Shared.Utilities +@using Angor.Shared.Services +@using System.Globalization @inject IJSRuntime JS -@inject HttpClient Http +@inject ILogger _Logger @inject IDerivationOperations _derivationOperations @inject IWalletStorage _walletStorage; @inject IClientStorage storage; @inject NavigationManager NavigationManager -@inject INetworkConfiguration _NetworkConfiguration -@inject IIndexerService _IndexerService -@inject IRelayService RelayService @inject ISignService SignService @inject IInvestorTransactionActions InvestorTransactionActions @inject IFounderTransactionActions FounderTransactionActions @@ -41,7 +38,7 @@ - @if (!pendingSignatures.Any()) + @if (!signaturesRequests.Any()) {

No pending signatures yet...

} @@ -50,62 +47,39 @@
- - - - - - - - - - @foreach (var signature in pendingSignatures.Where(_ => _.TransactionHex != null)) - { - - - - - - } - -
Amount to InvestTime ArrivedAction
@signature.AmountToInvest @network.CoinTicker@signature.TimeArrived.ToString("g") - -
-
-
- } - - -
- - @if (!approvedSignatures.Any()) - { -

No investments to approve yet...

- } - else - { - -
-
- - - - - - - - - - @foreach (var signature in approvedSignatures) - { + @if (messagesReceived) + { +
+ } + else + { +
Amount InvestedTime ArrivedTime Of Approval
+ - - - + + + - } - -
@signature.AmountToInvest @network.CoinTicker@signature.TimeArrived.ToString("g")@signature.TimeAproved.ToString("g")Investment amountReceived atStatus
+ + + @foreach (var signature in signaturesRequests.Where(_ => _ is { TransactionHex: not null, AmountToInvest: not null })) + { + + @signature.AmountToInvest?.ToString(CultureInfo.CurrentCulture) @network.CoinTicker + @signature.TimeArrived.ToString("g") + @if (signature.TimeApproved is null) + { + + } + else + { + Approved on - @signature.TimeApproved.ToString() + } + + } + + + }
} @@ -116,16 +90,12 @@ public string ProjectIdentifier { get; set; } public FounderProject FounderProject { get; set; } - private List pendingSignatures = new(); - - private List approvedSignatures = new(); - + private List signaturesRequests = new(); private IJSInProcessObjectReference? javascriptNostrToolsModule; - - int numberOfSignaturesHandled; protected override async Task OnInitializedAsync() { + _Logger.LogDebug("OnInitializedAsync"); if (hasWallet) { FounderProject = storage.GetFounderProjects() @@ -134,14 +104,17 @@ await FetchPendingSignatures(FounderProject); } + _Logger.LogDebug("End of OnInitializedAsync"); } protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender || javascriptNostrToolsModule == null) + _Logger.LogDebug("OnAfterRenderAsync"); + if (javascriptNostrToolsModule == null && signaturesRequests.Any()) { try { + _Logger.LogDebug("load nostr tools"); //TODO import the nostr tool module directly to c# class javascriptNostrToolsModule = await JS.InvokeAsync("import", "./NostrToolsMethods.js?version=" + DateTime.UtcNow.Ticks); } @@ -149,16 +122,19 @@ { Console.WriteLine(e); notificationComponent.ShowErrorMessage(e.Message); + return; } } - if (numberOfSignaturesHandled < pendingSignatures.Count) + _Logger.LogDebug("handled = {Count}, total = {SignaturesRequestsCount}", signaturesRequests.Count(x => x.AmountToInvest.HasValue), signaturesRequests.Count); + + if (signaturesRequests.Any(x => x.AmountToInvest == null)) { - var nostrPrivateKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), FounderProject.ProjectInfo.ProjectIndex); + var nostrPrivateKey = await _derivationOperations.DeriveProjectNostrPrivateKeyAsync(_walletStorage.GetWallet(), FounderProject.ProjectInfo.ProjectIndex); var nostrPrivateKeyHex = Encoders.Hex.EncodeData(nostrPrivateKey.ToBytes()); - foreach (var pendingSignature in pendingSignatures) + foreach (var pendingSignature in signaturesRequests.Where(_ => _.AmountToInvest == null)) { pendingSignature.TransactionHex = await javascriptNostrToolsModule.InvokeAsync( "decryptNostr", @@ -172,30 +148,38 @@ pendingSignature.AmountToInvest = investorTrx.Outputs.AsIndexedOutputs().Skip(2).Take(investorTrx.Outputs.Count - 3) //Todo get the actual outputs with taproot type .Sum(_ => _.TxOut.Value); - - StateHasChanged(); } catch (Exception e) { Console.WriteLine(e); - Console.WriteLine(pendingSignature.TransactionHex); + _Logger.LogDebug(pendingSignature.TransactionHex); pendingSignature.TransactionHex = null; } - finally - { - numberOfSignaturesHandled++; - } } + _Logger.LogDebug($"Calling StateHasChanged in OnAfterRenderAsync"); + messagesReceived = false; + StateHasChanged(); } + + _Logger.LogDebug("OnAfterRenderAsync Completed"); } bool messagesReceived; private async Task FetchPendingSignatures(FounderProject project) { - await SignService.LookupInvestmentRequestsAsync(project.ProjectInfo.NostrPubKey, project.LastRequestForSignaturesTime , async (investorPubKey,encryptedMessage, requestTime) => + await SignService.LookupInvestmentRequestsAsync(project.ProjectInfo.NostrPubKey, project.LastRequestForSignaturesTime , async + (investorPubKey,encryptedMessage, requestTime) => { + _Logger.LogDebug($"Event received"); + + if (signaturesRequests.Any(_ => _.investorPubKey == investorPubKey)) + return; //multiple relays could mean the same massage multiple times + + _Logger.LogDebug($"Event received is new"); + messagesReceived = true; + var signatureRequest = new SignatureRequest { investorPubKey = investorPubKey, @@ -203,12 +187,18 @@ TransactionHex = encryptedMessage //To be encrypted after js interop is loaded }; - pendingSignatures.Add(signatureRequest); + signaturesRequests.Add(signatureRequest); + _Logger.LogDebug($"Added to pendingSignatures"); }, () => { - if (messagesReceived) - StateHasChanged(); + _Logger.LogDebug($"End of messages"); + + if (!messagesReceived) + return; + + _Logger.LogDebug($"Calling StateHasChanged in EOSE"); + StateHasChanged(); }); } @@ -218,9 +208,9 @@ var signatureInfo = signProject(signature.TransactionHex, FounderProject.ProjectInfo, Encoders.Hex.EncodeData(key.ToBytes())); - var sigJson = System.Text.Json.JsonSerializer.Serialize(signatureInfo, settings); + var sigJson = JsonSerializer.Serialize(signatureInfo, RelayService.settings); - var nostrPrivateKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), FounderProject.ProjectInfo.ProjectIndex); + var nostrPrivateKey = await _derivationOperations.DeriveProjectNostrPrivateKeyAsync(_walletStorage.GetWallet(), FounderProject.ProjectInfo.ProjectIndex); var nostrPrivateKeyHex = Encoders.Hex.EncodeData(nostrPrivateKey.ToBytes()); @@ -234,8 +224,9 @@ storage.UpdateFounderProject(FounderProject); - pendingSignatures.Remove(signature); - approvedSignatures.Add(signature); + signaturesRequests.Single(_ => _.investorPubKey == signature.investorPubKey && _.TimeApproved is null) + .TimeApproved = FounderProject.LastRequestForSignaturesTime; + StateHasChanged(); } @@ -257,27 +248,11 @@ { public string investorPubKey { get; set; } - public decimal AmountToInvest { get; set; } + public decimal? AmountToInvest { get; set; } public DateTime TimeArrived { get; set; } - public DateTime TimeAproved { get; set; } + public DateTime? TimeApproved { get; set; } public string? TransactionHex { get; set; } } - - - //TODO move the settings to a common place - private JsonSerializerOptions settings => new() - { - // Equivalent to Formatting = Formatting.None - WriteIndented = false, - - // Equivalent to NullValueHandling = NullValueHandling.Ignore - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, - - // PropertyNamingPolicy equivalent to CamelCasePropertyNamesContractResolver - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - - Converters = { new UnixDateTimeConverter() } - }; } \ No newline at end of file diff --git a/src/Angor/Client/Pages/Wallet.razor b/src/Angor/Client/Pages/Wallet.razor index acbcd404..8223e648 100644 --- a/src/Angor/Client/Pages/Wallet.razor +++ b/src/Angor/Client/Pages/Wallet.razor @@ -6,7 +6,6 @@ @using Angor.Client.Storage @using Angor.Shared.Models -@inject HttpClient Http @inject IClientStorage storage; @inject IWalletStorage _walletStorage; @inject ILogger Logger; diff --git a/src/Angor/Client/Program.cs b/src/Angor/Client/Program.cs index e9634216..95f03daa 100644 --- a/src/Angor/Client/Program.cs +++ b/src/Angor/Client/Program.cs @@ -47,11 +47,7 @@ builder.Services.AddTransient(); builder.Services.AddTransient(); - - - - - +builder.Services.AddSingleton(); await builder.Build().RunAsync(); diff --git a/src/Angor/Shared/DerivationOperations.cs b/src/Angor/Shared/DerivationOperations.cs index c9b95888..ff92c609 100644 --- a/src/Angor/Shared/DerivationOperations.cs +++ b/src/Angor/Shared/DerivationOperations.cs @@ -216,6 +216,22 @@ public Key DeriveProjectNostrPrivateKey(WalletWords walletWords, int index) return extKey.PrivateKey; } + + public async Task DeriveProjectNostrPrivateKeyAsync(WalletWords walletWords, int index) + { + // founder key is derived from the path m/5' + ExtKey extendedKey = GetExtendedKey(walletWords); + + var path = $"m/44'/1237'/{index}/0/0"; + + var task = Task.Run(() => extendedKey.Derive(new KeyPath(path))); + + await Task.WhenAll(task); + + ExtKey extKey = task.Result; + + return extKey.PrivateKey; + } public uint DeriveProjectId(string founderKey) { diff --git a/src/Angor/Shared/IDerivationOperations.cs b/src/Angor/Shared/IDerivationOperations.cs index 1e24cb9b..7406b90b 100644 --- a/src/Angor/Shared/IDerivationOperations.cs +++ b/src/Angor/Shared/IDerivationOperations.cs @@ -20,4 +20,5 @@ public interface IDerivationOperations Key DeriveFounderRecoveryPrivateKey(WalletWords walletWords, int index); Key DeriveProjectNostrPrivateKey(WalletWords walletWords, int index); + Task DeriveProjectNostrPrivateKeyAsync(WalletWords walletWords, int index); } \ No newline at end of file diff --git a/src/Angor/Shared/Models/NostrRelayInfo.cs b/src/Angor/Shared/Models/NostrRelayInfo.cs new file mode 100644 index 00000000..1febc834 --- /dev/null +++ b/src/Angor/Shared/Models/NostrRelayInfo.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Angor.Shared.Models; + +public class NostrRelayInfo +{ + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("description")] + public string Description { get; set; } + [JsonPropertyName("pubkey")] + public string PubKey { get; set; } + [JsonPropertyName("contact")] + public string Contact { get; set; } + [JsonPropertyName("supported_nips")] + public int[] SupportedNips { get; set; } + [JsonPropertyName("software")] + public string Software { get; set; } + [JsonPropertyName("version")] + public string Version { get; set; } +} \ No newline at end of file diff --git a/src/Angor/Shared/Services/INostrCommunicationFactory.cs b/src/Angor/Shared/Services/INostrCommunicationFactory.cs index ca035995..a890397f 100644 --- a/src/Angor/Shared/Services/INostrCommunicationFactory.cs +++ b/src/Angor/Shared/Services/INostrCommunicationFactory.cs @@ -1,91 +1,10 @@ -using Microsoft.Extensions.Logging; using Nostr.Client.Client; -using Nostr.Client.Communicator; namespace Angor.Shared.Services; public interface INostrCommunicationFactory { - INostrClient CreateClient(INostrCommunicator communicator); - INostrCommunicator CreateCommunicator(string uri, string relayName); -} - -class NostrCommunicationFactory : INostrCommunicationFactory -{ - private ILogger _clientLogger; - private ILogger _communicatorLogger; - - public NostrCommunicationFactory(ILogger clientLogger, ILogger communicatorLogger) - { - _clientLogger = clientLogger; - _communicatorLogger = communicatorLogger; - } - - public INostrClient CreateClient(INostrCommunicator communicator) - { - var nostrClient = new NostrWebsocketClient(communicator, _clientLogger); - - nostrClient.Streams.UnknownMessageStream.Subscribe(_ => _clientLogger.LogError($"UnknownMessageStream {_.MessageType} {_.AdditionalData}")); - nostrClient.Streams.EventStream.Subscribe(_ => _clientLogger.LogInformation($"EventStream {_.Subscription} {_.AdditionalData}")); - nostrClient.Streams.NoticeStream.Subscribe(_ => _clientLogger.LogError($"NoticeStream {_.Message}")); - nostrClient.Streams.UnknownRawStream.Subscribe(_ => _clientLogger.LogError($"UnknownRawStream {_.Message}")); - - nostrClient.Streams.OkStream.Subscribe(_ => - { - _clientLogger.LogInformation($"OkStream {_.Accepted} message - {_.Message}"); - - // if (_.EventId != null && OkVerificationActions.ContainsKey(_.EventId)) - // { - // OkVerificationActions[_.EventId](_); - // OkVerificationActions.Remove(_.EventId); - // } - }); - - nostrClient.Streams.EoseStream.Subscribe(_ => - { - _clientLogger.LogInformation($"EoseStream {_.Subscription} message - {_.AdditionalData}"); - - // if (!subscriptions.ContainsKey(_.Subscription)) - // return; - - nostrClient.Streams.EventStream.Subscribe(_ => { }, _ => { },() => {_clientLogger.LogInformation("Event stream closed");}); - - // _clientLogger.LogInformation($"Disposing of subscription - {_.Subscription}"); - // subscriptions[_.Subscription].Dispose(); - // subscriptions.Remove(_.Subscription); - // _clientLogger.LogInformation($"subscription disposed - {_.Subscription}"); - }); - - return nostrClient; - } - - public INostrCommunicator CreateCommunicator(string uri, string relayName) - { - var nostrCommunicator = new NostrWebsocketCommunicator(new Uri(uri)) - { - Name = relayName, - ReconnectTimeout = null //TODO need to check what is the actual best time to set here - }; - - nostrCommunicator.DisconnectionHappened.Subscribe(_ => - { - if (_.Exception != null) - _communicatorLogger.LogError(_.Exception, - "Relay {RelayName} disconnected, type: {Type}, reason: {CloseStatusDescription}", - relayName, _.Type, _.CloseStatusDescription); - else - _communicatorLogger.LogInformation( - "Relay {RelayName} disconnected, type: {Type}, reason: {CloseStatusDescription}", - relayName, _.Type, _.CloseStatusDescription); - }); - - nostrCommunicator.MessageReceived.Subscribe(_ => - { - _communicatorLogger.LogInformation( - "message received on communicator {RelayName} - {Text} Relay message received, type: {MessageType}", - relayName, _.Text, _.MessageType); - }); - - return nostrCommunicator; - } + INostrClient GetOrCreateClient(INetworkService networkService); + void CloseClientConnection(); + int GetNumberOfRelaysConnected(); } \ No newline at end of file diff --git a/src/Angor/Shared/Services/IRelayService.cs b/src/Angor/Shared/Services/IRelayService.cs index 38960336..ff3b6998 100644 --- a/src/Angor/Shared/Services/IRelayService.cs +++ b/src/Angor/Shared/Services/IRelayService.cs @@ -7,7 +7,6 @@ namespace Angor.Shared.Services; public interface IRelayService { - Task ConnectToRelaysAsync(); void RegisterOKMessageHandler(string eventId, Action action); Task AddProjectAsync(ProjectInfo project, string nsec); Task CreateNostrProfileAsync(NostrMetadata metadata, string nsec); diff --git a/src/Angor/Shared/Services/ISignService.cs b/src/Angor/Shared/Services/ISignService.cs new file mode 100644 index 00000000..7d2c7c10 --- /dev/null +++ b/src/Angor/Shared/Services/ISignService.cs @@ -0,0 +1,17 @@ +using Angor.Shared.Models; + +namespace Angor.Client.Services; + +public interface ISignService +{ + DateTime RequestInvestmentSigs(SignRecoveryRequest signRecoveryRequest); + void LookupSignatureForInvestmentRequest(string investorNostrPubKey, string projectNostrPubKey, DateTime sigRequestSentTime, Func action); + + Task LookupInvestmentRequestsAsync(string nostrPubKey, DateTime? since, Action action, + Action onAllMessagesReceived); + + DateTime SendSignaturesToInvestor(string encryptedSignatureInfo, string nostrPrivateKey, + string investorNostrPubKey); + + void CloseConnection(); //TODO call close connection from the pages +} \ No newline at end of file diff --git a/src/Angor/Shared/Services/NetworkService.cs b/src/Angor/Shared/Services/NetworkService.cs index e7089ea0..68c82efd 100644 --- a/src/Angor/Shared/Services/NetworkService.cs +++ b/src/Angor/Shared/Services/NetworkService.cs @@ -1,4 +1,6 @@ using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; using Angor.Shared.Models; using Microsoft.Extensions.Logging; @@ -49,6 +51,8 @@ public async Task CheckServices(bool force = false) } } + var nostrHeaderMediaType = new MediaTypeWithQualityHeaderValue("application/nostr+json"); + _httpClient.DefaultRequestHeaders.Accept.Add(nostrHeaderMediaType); foreach (var relayUrl in settings.Relays) { if (force || (DateTime.UtcNow - relayUrl.LastCheck).Minutes > 1) @@ -59,11 +63,14 @@ public async Task CheckServices(bool force = false) { var uri = new Uri(relayUrl.Url); var httpUri = uri.Scheme == "wss" ? new Uri($"https://{uri.Host}/") : new Uri($"http://{uri.Host}/"); + var response = await _httpClient.GetAsync(httpUri); if (response.IsSuccessStatusCode) { relayUrl.Status = UrlStatus.Online; + var relayInfo = await response.Content.ReadFromJsonAsync(); + relayUrl.Name = relayInfo?.Name ?? string.Empty; } else { @@ -78,6 +85,7 @@ public async Task CheckServices(bool force = false) } } + _httpClient.DefaultRequestHeaders.Accept.Remove(nostrHeaderMediaType); _networkStorage.SetSettings(settings); } diff --git a/src/Angor/Shared/Services/NostrCommunicationFactory.cs b/src/Angor/Shared/Services/NostrCommunicationFactory.cs new file mode 100644 index 00000000..c6a3644b --- /dev/null +++ b/src/Angor/Shared/Services/NostrCommunicationFactory.cs @@ -0,0 +1,88 @@ +using System.Reactive.Linq; +using Microsoft.Extensions.Logging; +using Nostr.Client.Client; +using Nostr.Client.Communicator; + +namespace Angor.Shared.Services; + +public class NostrCommunicationFactory : IDisposable,INostrCommunicationFactory +{ + private readonly ILogger _clientLogger; + private readonly ILogger _communicatorLogger; + + private NostrMultiWebsocketClient? _nostrMultiWebsocketClient; + private readonly List _serviceSubscriptions; + + public NostrCommunicationFactory(ILogger clientLogger, ILogger communicatorLogger) + { + _clientLogger = clientLogger; + _communicatorLogger = communicatorLogger; + _serviceSubscriptions = new(); + } + + public INostrClient GetOrCreateClient(INetworkService networkService) + { + _nostrMultiWebsocketClient ??= new NostrMultiWebsocketClient(_clientLogger); + + foreach (var url in networkService.GetRelays() + .Where(url => _nostrMultiWebsocketClient.FindClient(url.Name) == null)) + { + var communicator = CreateCommunicator(url.Url, url.Name); + _nostrMultiWebsocketClient.RegisterClient(new NostrWebsocketClient(communicator,_clientLogger)); + communicator.StartOrFail(); + } + + _serviceSubscriptions.Add(_nostrMultiWebsocketClient.Streams.UnknownMessageStream.Subscribe(_ => _clientLogger.LogError($"UnknownMessageStream {_.MessageType} {_.AdditionalData}"))); + _serviceSubscriptions.Add(_nostrMultiWebsocketClient.Streams.EventStream.Where(_ => _.Event?.AdditionalData?.Any() ?? false).Subscribe(_ => _clientLogger.LogInformation($"EventStream {_.Subscription} {_.Event?.Id} {_.Event?.AdditionalData}"))); + _serviceSubscriptions.Add(_nostrMultiWebsocketClient.Streams.NoticeStream.Subscribe(_ => _clientLogger.LogError($"NoticeStream {_.Message}"))); + _serviceSubscriptions.Add(_nostrMultiWebsocketClient.Streams.UnknownRawStream.Subscribe(_ => _clientLogger.LogError($"UnknownRawStream {_.Message}"))); + + return _nostrMultiWebsocketClient; + } + + public void CloseClientConnection() + { + Dispose(); + } + + public int GetNumberOfRelaysConnected() + { + return _nostrMultiWebsocketClient?.Clients.Count ?? 0; + } + + public INostrCommunicator CreateCommunicator(string uri, string relayName) + { + var nostrCommunicator = new NostrWebsocketCommunicator(new Uri(uri)) + { + Name = relayName, + ReconnectTimeout = null //TODO need to check what is the actual best time to set here + }; + + _serviceSubscriptions.Add(nostrCommunicator.DisconnectionHappened.Subscribe(e => + { + if (e.Exception != null) + _communicatorLogger.LogError(e.Exception, + "Relay {relayName} disconnected, type: {Type}, reason: {CloseStatusDescription}", + relayName, e.Type, e.CloseStatusDescription); + else + _communicatorLogger.LogInformation( + "Relay {relayName} disconnected, type: {Type}, reason: {CloseStatusDescription}", + relayName, e.Type, e.CloseStatusDescription); + })); + + _serviceSubscriptions.Add(nostrCommunicator.MessageReceived.Subscribe(e => + { + _communicatorLogger.LogInformation( + "message received on communicator {relayName} - {Text} Relay message received, type: {MessageType}", + relayName, e.Text, e.MessageType); + })); + + return nostrCommunicator; + } + + public void Dispose() + { + _serviceSubscriptions.ForEach(subscription => subscription.Dispose()); + _nostrMultiWebsocketClient?.Dispose(); + } +} \ No newline at end of file diff --git a/src/Angor/Shared/Services/RelayService.cs b/src/Angor/Shared/Services/RelayService.cs index 90278595..c31b38e9 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -4,8 +4,6 @@ using Angor.Shared.Utilities; using Nostr.Client.Requests; using Microsoft.Extensions.Logging; -using Nostr.Client.Client; -using Nostr.Client.Communicator; using Nostr.Client.Keys; using Nostr.Client.Messages; using Nostr.Client.Messages.Metadata; @@ -13,56 +11,41 @@ namespace Angor.Shared.Services { - public class RelayService : IRelayService + public class RelayService : RelaySubscriptionsHanding,IRelayService { - private static NostrWebsocketClient? _nostrClient; - private static INostrCommunicator? _nostrCommunicator; - private ILogger _logger; + private INostrCommunicationFactory _communicationFactory; + private INetworkService networkService; + - private ILogger _clientLogger; - private ILogger _communicatorLogger; - - private Dictionary userSubscriptions = new(); - private Dictionary userEoseActions = new(); - private List serviceSubscriptions = new(); - private Dictionary> OkVerificationActions = new(); - - public RelayService( - ILogger logger, - ILogger clientLogger, - ILogger communicatorLogger) + private readonly List _serviceSubscriptions; + + public RelayService(ILogger logger, INostrCommunicationFactory communicationFactory, INetworkService networkService,ILogger baseLogger) + : base(baseLogger,communicationFactory,networkService) { _logger = logger; - _clientLogger = clientLogger; - _communicatorLogger = communicatorLogger; - } + _communicationFactory = communicationFactory; + this.networkService = networkService; - public async Task ConnectToRelaysAsync() - { - if (_nostrCommunicator == null) - { - SetupNostrCommunicator(); - } - - if (_nostrClient == null) - { - SetupNostrClient(); - } + var nostrClient = _communicationFactory.GetOrCreateClient(this.networkService); - await _nostrCommunicator.StartOrFail(); + _serviceSubscriptions = new(); + _serviceSubscriptions.Add(nostrClient.Streams.OkStream.Subscribe(HandleOkMessages)); + _serviceSubscriptions.Add(nostrClient.Streams.EoseStream.Subscribe(HandleEoseMessages)); } public void RegisterOKMessageHandler(string eventId, Action action) { - OkVerificationActions.Add(eventId,action); + OkVerificationActions.Add(eventId,new SubscriptionCallCounter>(action)); } public void LookupProjectsInfoByPubKeys(Action responseDataAction, Action? OnEndOfStreamAction,params string[] nostrPubKeys) { const string subscriptionName = "ProjectInfoLookups"; - if (_nostrClient == null) + var nostrClient = _communicationFactory.GetOrCreateClient(networkService); + + if (nostrClient == null) throw new InvalidOperationException("The nostr client is null"); var request = new NostrRequest(subscriptionName, new NostrFilter @@ -71,11 +54,11 @@ public void LookupProjectsInfoByPubKeys(Action responseDataAction, Action? Kinds = new[] { NostrKind.ApplicationSpecificData } }); - _nostrClient.Send(request); + nostrClient.Send(request); if (!userSubscriptions.ContainsKey(subscriptionName)) { - var subscription = _nostrClient.Streams.EventStream + var subscription = nostrClient.Streams.EventStream .Where(_ => _.Subscription == subscriptionName) .Select(_ => _.Event) .Subscribe(ev => @@ -83,20 +66,22 @@ public void LookupProjectsInfoByPubKeys(Action responseDataAction, Action? responseDataAction(JsonSerializer.Deserialize(ev.Content,settings)); }); - userSubscriptions.Add(subscriptionName, subscription); + userSubscriptions.Add(subscriptionName, new SubscriptionCallCounter(subscription)); } if (OnEndOfStreamAction != null) { - userEoseActions.Add(subscriptionName,OnEndOfStreamAction); + userEoseActions.Add(subscriptionName,new SubscriptionCallCounter(OnEndOfStreamAction)); } } public void RequestProjectCreateEventsByPubKey(Action onResponseAction, Action? onEoseAction,params string[] nostrPubKeys) { var subscriptionKey = Guid.NewGuid().ToString().Replace("-",""); - if (_nostrClient == null) throw new InvalidOperationException("The nostr client is null"); - _nostrClient.Send(new NostrRequest(subscriptionKey, new NostrFilter + + var nostrClient = _communicationFactory.GetOrCreateClient(networkService); + + nostrClient.Send(new NostrRequest(subscriptionKey, new NostrFilter { Authors = nostrPubKeys, Kinds = new[] { NostrKind.ApplicationSpecificData, NostrKind.Metadata}, @@ -105,25 +90,24 @@ public void RequestProjectCreateEventsByPubKey(Action onResponseActi if (userSubscriptions.ContainsKey(subscriptionKey)) return; - var subscription = _nostrClient.Streams.EventStream + var subscription = nostrClient.Streams.EventStream .Where(_ => _.Subscription == subscriptionKey) .Where(_ => _.Event is not null) .Select(_ => _.Event) .Subscribe(onResponseAction!); - userSubscriptions.Add(subscriptionKey, subscription); + userSubscriptions.Add(subscriptionKey, new SubscriptionCallCounter(subscription)); - userEoseActions.TryAdd(subscriptionKey, onEoseAction); + userEoseActions.TryAdd(subscriptionKey, new SubscriptionCallCounter(onEoseAction)); } public Task LookupDirectMessagesForPubKeyAsync(string nostrPubKey, DateTime? since, int? limit, Action onResponseAction) { - if (_nostrClient == null) - throw new InvalidOperationException("The nostr client is null"); + var nostrClient = _communicationFactory.GetOrCreateClient(networkService); var subscriptionKey = nostrPubKey + "DM"; - _nostrClient.Send(new NostrRequest(subscriptionKey, new NostrFilter + nostrClient.Send(new NostrRequest(subscriptionKey, new NostrFilter { P = new[] { nostrPubKey }, Kinds = new[] { NostrKind.EncryptedDm }, @@ -134,13 +118,13 @@ public Task LookupDirectMessagesForPubKeyAsync(string nostrPubKey, DateTime? sin if (!userSubscriptions.ContainsKey(subscriptionKey)) { - var subscription = _nostrClient.Streams.EventStream + var subscription = nostrClient.Streams.EventStream .Where(_ => _.Subscription == subscriptionKey) .Where(_ => _.Event is not null) .Select(_ => _.Event) .Subscribe(onResponseAction!); - userSubscriptions.Add(subscriptionKey, subscription); + userSubscriptions.Add(subscriptionKey, new SubscriptionCallCounter(subscription)); } return Task.CompletedTask; @@ -153,12 +137,8 @@ private string NostrCoordinatesIdentifierTag(string nostrPubKey) public void CloseConnection() { - userSubscriptions.Values.ToList().ForEach(_ => _.Dispose()); - serviceSubscriptions.ForEach(_ => _.Dispose()); - _nostrClient?.Dispose(); - _nostrCommunicator?.Dispose(); - _nostrClient = null; - _nostrCommunicator = null; + _serviceSubscriptions.ForEach(subscription => subscription.Dispose()); + Dispose(); } public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) @@ -173,10 +153,9 @@ public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) var signed = GetNip78NostrEvent(content) .Sign(key); - if (_nostrClient == null) - throw new InvalidOperationException(); + var nostrClient = _communicationFactory.GetOrCreateClient(networkService); - _nostrClient.Send(new NostrEventRequest(signed)); + nostrClient.Send(new NostrEventRequest(signed)); return Task.FromResult(signed.Id); } @@ -198,10 +177,9 @@ public Task CreateNostrProfileAsync(NostrMetadata metadata, string hexPr new NostrEventTag("l", "ProjectDeclaration", "#projectInfo")) }.Sign(key); - if (_nostrClient == null) - throw new InvalidOperationException(); + var nostrClient = _communicationFactory.GetOrCreateClient(networkService); - _nostrClient.Send(new NostrEventRequest(signed)); + nostrClient.Send(new NostrEventRequest(signed)); return Task.FromResult(signed.Id); } @@ -218,7 +196,8 @@ public Task DeleteProjectAsync(string eventId, string hexPrivateKey) Tags = new NostrEventTags(NostrEventTag.Event(eventId)) }.Sign(key); - _nostrClient.Send(deleteEvent); + var nostrClient = _communicationFactory.GetOrCreateClient(networkService); + nostrClient.Send(deleteEvent); return Task.FromResult(deleteEvent.Id); } @@ -259,76 +238,6 @@ private static NostrEvent GetNip99NostrEvent(ProjectInfo project, string content return ev; } - - private void SetupNostrClient() - { - _nostrClient = new NostrWebsocketClient(_nostrCommunicator, _clientLogger); - - serviceSubscriptions.Add(_nostrClient.Streams.UnknownMessageStream.Subscribe(_ => _clientLogger.LogError($"UnknownMessageStream {_.MessageType} {_.AdditionalData}"))); - serviceSubscriptions.Add(_nostrClient.Streams.EventStream.Subscribe(_ => _clientLogger.LogInformation($"EventStream {_.Subscription} {_.AdditionalData}"))); - serviceSubscriptions.Add(_nostrClient.Streams.NoticeStream.Subscribe(_ => _clientLogger.LogError($"NoticeStream {_.Message}"))); - serviceSubscriptions.Add(_nostrClient.Streams.UnknownRawStream.Subscribe(_ => _clientLogger.LogError($"UnknownRawStream {_.Message}"))); - - serviceSubscriptions.Add( _nostrClient.Streams.OkStream.Subscribe(_ => - { - _clientLogger.LogInformation($"OkStream {_.Accepted} message - {_.Message}"); - - if (_.EventId != null && OkVerificationActions.ContainsKey(_.EventId)) - { - OkVerificationActions[_.EventId](_); - OkVerificationActions.Remove(_.EventId); - } - })); - - serviceSubscriptions.Add(_nostrClient.Streams.EoseStream.Subscribe(_ => - { - _clientLogger.LogInformation($"EoseStream {_.Subscription} message - {_.AdditionalData}"); - - if (userEoseActions.ContainsKey(_.Subscription)) - { - _clientLogger.LogInformation($"Invoking action on EOSE - {_.Subscription}"); - userEoseActions[_.Subscription].Invoke(); - userEoseActions.Remove(_.Subscription); - _clientLogger.LogInformation($"Removed action on EOSE for subscription - {_.Subscription}"); - } - - if (!userSubscriptions.ContainsKey(_.Subscription)) - return; - _clientLogger.LogInformation($"Disposing of subscription - {_.Subscription}"); - _nostrClient.Send(new NostrCloseRequest(_.Subscription)); - userSubscriptions[_.Subscription].Dispose(); - userSubscriptions.Remove(_.Subscription); - _clientLogger.LogInformation($"subscription disposed - {_.Subscription}"); - })); - } - - private void SetupNostrCommunicator() - { - _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("wss://relay.angor.io")) - { - Name = "angor-relay.test", - ReconnectTimeout = null //TODO need to check what is the actual best time to set here - }; - - serviceSubscriptions.Add(_nostrCommunicator.DisconnectionHappened.Subscribe(info => - { - if (info.Exception != null) - _communicatorLogger.LogError(info.Exception, - "Relay disconnected, type: {Type}, reason: {CloseStatus}", info.Type, - info.CloseStatusDescription); - else - _communicatorLogger.LogInformation("Relay disconnected, type: {Type}, reason: {CloseStatus}", - info.Type, info.CloseStatusDescription); - })); - - serviceSubscriptions.Add(_nostrCommunicator.MessageReceived.Subscribe(info => - { - _communicatorLogger.LogInformation( - "message received on communicator - {Text} Relay message received, type: {MessageType}", - info.Text, info.MessageType); - })); - } - public static JsonSerializerOptions settings => new () { // Equivalent to Formatting = Formatting.None diff --git a/src/Angor/Shared/Services/RelaySubscriptionsHanding.cs b/src/Angor/Shared/Services/RelaySubscriptionsHanding.cs new file mode 100644 index 00000000..1403f07c --- /dev/null +++ b/src/Angor/Shared/Services/RelaySubscriptionsHanding.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.Logging; +using Nostr.Client.Requests; +using Nostr.Client.Responses; + +namespace Angor.Shared.Services; + +public class RelaySubscriptionsHanding : IDisposable +{ + private ILogger _logger; + protected Dictionary> userSubscriptions; + protected Dictionary> userEoseActions; + protected Dictionary>> OkVerificationActions; + + private INostrCommunicationFactory _communicationFactory; + private INetworkService _networkService; + + protected RelaySubscriptionsHanding(ILogger logger, INostrCommunicationFactory communicationFactory, INetworkService networkService) + { + _logger = logger; + _communicationFactory = communicationFactory; + _networkService = networkService; + userSubscriptions = new(); + userEoseActions = new(); + OkVerificationActions = new(); + } + + protected class SubscriptionCallCounter + { + public SubscriptionCallCounter(T item) + { + Item = item; + } + + public int NumberOfInvocations { get; set; } + public T Item { get; } + } + + + public void HandleOkMessages(NostrOkResponse _) + { + _logger.LogInformation($"OkStream {_.Accepted} message - {_.Message}"); + + if (OkVerificationActions.TryGetValue(_?.EventId ?? string.Empty, out SubscriptionCallCounter> value)) + { + value.NumberOfInvocations++; + value.Item(_); + if (value.NumberOfInvocations == _communicationFactory.GetNumberOfRelaysConnected()) + { + OkVerificationActions.Remove(_.EventId ?? string.Empty); + } + } + } + + public void HandleEoseMessages(NostrEoseResponse _) + { + _logger.LogInformation($"EoseStream {_.Subscription} message - {_.AdditionalData}"); + + if (userEoseActions.TryGetValue(_.Subscription, out SubscriptionCallCounter value)) + { + value.NumberOfInvocations++; + if (userEoseActions[_.Subscription].NumberOfInvocations == _communicationFactory.GetNumberOfRelaysConnected()) + { + _logger.LogInformation($"Invoking action on EOSE - {_.Subscription}"); + value.Item.Invoke(); + userEoseActions.Remove(_.Subscription); + _logger.LogInformation($"Removed action on EOSE for subscription - {_.Subscription}"); + } + } + + if (!userSubscriptions.ContainsKey(_.Subscription)) + return; + + userSubscriptions[_.Subscription].NumberOfInvocations++; + + if (userSubscriptions[_.Subscription].NumberOfInvocations != _communicationFactory.GetNumberOfRelaysConnected()) + return; + + _logger.LogInformation($"Disposing of subscription - {_.Subscription}"); + _communicationFactory + .GetOrCreateClient(_networkService) + .Send(new NostrCloseRequest(_.Subscription)); + userSubscriptions[_.Subscription].Item.Dispose(); + userSubscriptions.Remove(_.Subscription); + _logger.LogInformation($"subscription disposed - {_.Subscription}"); + } + + public void Dispose() + { + userSubscriptions.Values.ToList().ForEach(_ => _.Item.Dispose()); + _communicationFactory.CloseClientConnection(); + } +} \ No newline at end of file diff --git a/src/Angor/Shared/Services/SignService.cs b/src/Angor/Shared/Services/SignService.cs index 85f05cd6..11cfea5e 100644 --- a/src/Angor/Shared/Services/SignService.cs +++ b/src/Angor/Shared/Services/SignService.cs @@ -1,87 +1,31 @@ -using System.Net.Http.Json; -using System.Reactive.Linq; +using System.Reactive.Linq; using Angor.Shared.Models; +using Angor.Shared.Services; using Microsoft.Extensions.Logging; using Nostr.Client.Client; -using Nostr.Client.Communicator; using Nostr.Client.Keys; using Nostr.Client.Messages; using Nostr.Client.Requests; namespace Angor.Client.Services { - public interface ISignService + public class SignService : RelaySubscriptionsHanding, ISignService { - Task AddSignKeyAsync(ProjectInfo project, string founderRecoveryPrivateKey, string nostrPrivateKey); - DateTime RequestInvestmentSigs(SignRecoveryRequest signRecoveryRequest); - void LookupSignatureForInvestmentRequest(string investorNostrPubKey, string projectNostrPubKey, DateTime sigRequestSentTime, Func action); + private readonly INostrCommunicationFactory _communicationFactory; + private readonly INetworkService _networkService; + + private readonly List _serviceSubscriptions; - Task LookupInvestmentRequestsAsync(string nostrPubKey, DateTime? since, Action action, - Action onAllMessagesReceived); - - DateTime SendSignaturesToInvestor(string encryptedSignatureInfo, string nostrPrivateKey, - string investorNostrPubKey); - } - - public class SignService : ISignService - { - - private HttpClient _httpClient; - private static INostrClient _nostrClient; - private static INostrCommunicator _nostrCommunicator; - - private Dictionary subscriptions = new (); - private Dictionary eoseActions = new(); - public SignService(ILogger _logger, HttpClient httpClient) + public SignService(ILogger _logger, ILogger logger, INostrCommunicationFactory communicationFactory, INetworkService networkService) + : base(logger,communicationFactory,networkService) { - _httpClient = httpClient; - _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("wss://relay.angor.io")); - - _nostrCommunicator.Name = "angor-relay.test"; - _nostrCommunicator.ReconnectTimeout = null; + _communicationFactory = communicationFactory; + _networkService = networkService; - _nostrCommunicator.DisconnectionHappened.Subscribe(info => - { - _logger.LogError(info.Exception, "Relay disconnected, type: {type}, reason: {reason}.", info.Type, info.CloseStatus); - _nostrCommunicator.Start(); - }); - _nostrCommunicator.MessageReceived.Subscribe(info => _logger.LogInformation(info.Text, "Relay message received, type: {type}", info.MessageType)); - - _nostrCommunicator.StartOrFail(); - - _nostrClient = new NostrWebsocketClient(_nostrCommunicator, _logger); - - _nostrClient.Streams.EoseStream.Subscribe(_ => - { - _logger.LogInformation("End of stream on subscription" + _.Subscription); - - if (eoseActions.ContainsKey(_.Subscription)) - { - _logger.LogInformation("Invoking end of stream event on subscription" + _.Subscription); - eoseActions[_.Subscription].Invoke(); - eoseActions.Remove(_.Subscription); - } + var nostrClient = _communicationFactory.GetOrCreateClient(_networkService); - if (subscriptions.ContainsKey(_.Subscription)) - { - _logger.LogInformation("Closing and disposing of subscription - " + _.Subscription); - _nostrClient.Send(new NostrCloseRequest(_.Subscription)); - subscriptions[_.Subscription].Dispose(); - subscriptions.Remove(_.Subscription); - } - }); - } - - public async Task AddSignKeyAsync(ProjectInfo project, string founderRecoveryPrivateKey, string nostrPrivateKey) - { - var response = await _httpClient.PostAsJsonAsync($"/api/TestSign", - new SignData - { - ProjectIdentifier = project.ProjectIdentifier, - FounderRecoveryPrivateKey = founderRecoveryPrivateKey, - NostrPrivateKey = nostrPrivateKey - }); - response.EnsureSuccessStatusCode(); + _serviceSubscriptions = new(); + _serviceSubscriptions.Add(nostrClient.Streams.EoseStream.Subscribe(HandleEoseMessages)); } public DateTime RequestInvestmentSigs(SignRecoveryRequest signRecoveryRequest) @@ -93,28 +37,28 @@ public DateTime RequestInvestmentSigs(SignRecoveryRequest signRecoveryRequest) Kind = NostrKind.EncryptedDm, CreatedAt = DateTime.UtcNow, Content = signRecoveryRequest.EncryptedContent, - Tags = new NostrEventTags(new[] - { - NostrEventTag.Profile(signRecoveryRequest.NostrPubKey), - new NostrEventTag(NostrEventTag.CoordinatesIdentifier, - NostrCoordinatesIdentifierTag(signRecoveryRequest.NostrPubKey)) - }) + Tags = new NostrEventTags( + NostrEventTag.Profile(signRecoveryRequest.NostrPubKey), + new NostrEventTag(NostrEventTag.CoordinatesIdentifier, NostrCoordinatesIdentifierTag(signRecoveryRequest.NostrPubKey))) }; - // Blazor does not support AES so needs to be done manually in the UI + // Blazor does not support AES so it needs to be done manually in javascript // var encrypted = ev.EncryptDirect(sender, receiver); // var signed = encrypted.Sign(sender); var signed = ev.Sign(sender); - _nostrClient.Send(new NostrEventRequest(signed)); + var nostrClient = _communicationFactory.GetOrCreateClient(_networkService); + nostrClient.Send(new NostrEventRequest(signed)); return signed.CreatedAt!.Value; } public void LookupSignatureForInvestmentRequest(string investorNostrPubKey, string projectNostrPubKey, DateTime sigRequestSentTime, Func action) { - var subscription = _nostrClient.Streams.EventStream + var nostrClient = _communicationFactory.GetOrCreateClient(_networkService); + + var subscription = nostrClient.Streams.EventStream .Where(_ => _.Subscription == projectNostrPubKey) .Where(_ => _.Event.Kind == NostrKind.EncryptedDm) .Subscribe(_ => @@ -122,9 +66,9 @@ public void LookupSignatureForInvestmentRequest(string investorNostrPubKey, stri action.Invoke(_.Event.Content); }); - subscriptions.TryAdd(projectNostrPubKey,subscription); + userSubscriptions.TryAdd(projectNostrPubKey, new SubscriptionCallCounter(subscription)); - _nostrClient.Send(new NostrRequest(projectNostrPubKey, new NostrFilter + nostrClient.Send(new NostrRequest(projectNostrPubKey, new NostrFilter { Authors = new[] { projectNostrPubKey }, //From founder P = new[] { investorNostrPubKey }, // To investor @@ -137,9 +81,10 @@ public void LookupSignatureForInvestmentRequest(string investorNostrPubKey, stri public Task LookupInvestmentRequestsAsync(string nostrPubKey, DateTime? since, Action action, Action onAllMessagesReceived) { + var nostrClient = _communicationFactory.GetOrCreateClient(_networkService); var subscriptionKey = nostrPubKey + "sig_req"; - var subscription = _nostrClient.Streams.EventStream + var subscription = nostrClient.Streams.EventStream .Where(_ => _.Subscription == subscriptionKey) .Select(_ => _.Event) .Subscribe(_ => @@ -147,17 +92,16 @@ public Task LookupInvestmentRequestsAsync(string nostrPubKey, DateTime? since, A action.Invoke(_.Pubkey,_.Content, _.CreatedAt.Value); }); - subscriptions.TryAdd(subscriptionKey, subscription); + userSubscriptions.TryAdd(subscriptionKey, new SubscriptionCallCounter(subscription)); + userEoseActions.TryAdd(subscriptionKey,new SubscriptionCallCounter(onAllMessagesReceived)); - _nostrClient.Send(new NostrRequest(subscriptionKey, new NostrFilter + nostrClient.Send(new NostrRequest(subscriptionKey, new NostrFilter { - P = new[] { nostrPubKey }, + P = new[] { nostrPubKey }, //To founder Kinds = new[] { NostrKind.EncryptedDm }, - A = new []{ NostrCoordinatesIdentifierTag(nostrPubKey)}, + A = new []{ NostrCoordinatesIdentifierTag(nostrPubKey)}, //Only signature requests Since = since })); - - eoseActions.TryAdd(subscriptionKey,onAllMessagesReceived); return Task.CompletedTask; } @@ -180,11 +124,18 @@ public DateTime SendSignaturesToInvestor(string encryptedSignatureInfo, string n var signed = ev.Sign(nostrPrivateKey); - _nostrClient.Send(new NostrEventRequest(signed)); + var nostrClient = _communicationFactory.GetOrCreateClient(_networkService); + nostrClient.Send(new NostrEventRequest(signed)); return ev.CreatedAt.Value; } + public void CloseConnection() + { + _serviceSubscriptions.ForEach(subscription => subscription.Dispose()); + Dispose(); + } + private string NostrCoordinatesIdentifierTag(string nostrPubKey) { return $"{(int)NostrKind.ApplicationSpecificData}:{nostrPubKey}:AngorApp"; From 64d33dbf00213b8330a2fd417668c2be33c82ac0 Mon Sep 17 00:00:00 2001 From: Dan Gershony Date: Wed, 13 Dec 2023 17:00:27 +0000 Subject: [PATCH 2/2] generate address qrcode (#18) * generate address qrcode * Create a component --- src/Angor/Client/Angor.Client.csproj | 1 + src/Angor/Client/Components/ShowQrCode.razor | 58 ++++++++++++++++++++ src/Angor/Client/Pages/Wallet.razor | 38 +++++++------ 3 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 src/Angor/Client/Components/ShowQrCode.razor diff --git a/src/Angor/Client/Angor.Client.csproj b/src/Angor/Client/Angor.Client.csproj index fc81b3df..d10606e9 100644 --- a/src/Angor/Client/Angor.Client.csproj +++ b/src/Angor/Client/Angor.Client.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Angor/Client/Components/ShowQrCode.razor b/src/Angor/Client/Components/ShowQrCode.razor new file mode 100644 index 00000000..dd54bf46 --- /dev/null +++ b/src/Angor/Client/Components/ShowQrCode.razor @@ -0,0 +1,58 @@ +@using Angor.Shared.Services +@using Angor.Client.Models +@using Nostr.Client.Messages +@using Nostr.Client.Messages.Metadata +@using QRCoder + + +
+

QR Code

+ @if (!string.IsNullOrEmpty(base64qrcode)) + { + QR Code + } +
+ +@code { + + [Parameter] + public string Data { get; set; } + + private static string lastqrcode; + private static string lastaddress; + private string base64qrcode; + + protected override async Task OnInitializedAsync() + { + GenerateQRCode(Data); + } + + public Task GenerateQRCode(string newData) + { + Data = newData; + + if (lastaddress == Data) + { + base64qrcode = lastqrcode; + return Task.CompletedTask; + } + + return Task.Run(() => + { + base64qrcode = GenerateQRCodeInternal(Data); + lastqrcode = base64qrcode; + lastaddress = Data; + + StateHasChanged(); + + }); + } + + public static string GenerateQRCodeInternal(string content) + { + using QRCodeGenerator qrGenerator = new QRCodeGenerator(); + using QRCodeData qrCodeData = qrGenerator.CreateQrCode(content, QRCodeGenerator.ECCLevel.Q); + using PngByteQRCode pngByteQRCode = new PngByteQRCode(qrCodeData); + return Convert.ToBase64String(pngByteQRCode.GetGraphic(10)); + } +} \ No newline at end of file diff --git a/src/Angor/Client/Pages/Wallet.razor b/src/Angor/Client/Pages/Wallet.razor index 8223e648..8385a308 100644 --- a/src/Angor/Client/Pages/Wallet.razor +++ b/src/Angor/Client/Pages/Wallet.razor @@ -5,6 +5,7 @@ @using Angor.Client.Services @using Angor.Client.Storage @using Angor.Shared.Models +@using Angor.Client.Components @inject IClientStorage storage; @inject IWalletStorage _walletStorage; @@ -24,8 +25,7 @@ - - + @if (!hasWallet) { @@ -162,21 +162,17 @@

Receive Address

-

@localAccountInfo.GetNextReceiveAddress()

+

@nextReceiveAddress

-
-

QR Code

- - QR Code -
+ + +
- - @@ -390,6 +386,10 @@ private int _feeMax = 3; DateTime _lastFeeRefresh = DateTime.MinValue; + string? nextReceiveAddress = string.Empty; + + ShowQrCode showQrCode; + protected override Task OnInitializedAsync() { hasWallet = _walletStorage.HasWallet(); @@ -397,7 +397,9 @@ if (hasWallet) { localAccountInfo = GetAccountInfoFromStorage(); + nextReceiveAddress = localAccountInfo.GetNextReceiveAddress(); } + return Task.CompletedTask; } @@ -415,7 +417,11 @@ storage.SetAccountInfo(network.Name, accountInfo); localAccountInfo = accountInfo; - + + nextReceiveAddress = localAccountInfo.GetNextReceiveAddress(); + + showQrCode.GenerateQRCode(nextReceiveAddress); + return new OperationResult { Success = true }; }); @@ -474,7 +480,7 @@ ClearWalletWords(); StateHasChanged(); } - + private void ClearWalletWords() { walletWords = null; @@ -509,17 +515,15 @@ public async Task CopyNextReceiveAddress() { - var address = localAccountInfo.GetNextReceiveAddress(); - - if (string.IsNullOrEmpty(address)) + if (string.IsNullOrEmpty(nextReceiveAddress)) { notificationComponent.ShowErrorMessage("New address was not created"); return; } - await _clipboardService.WriteTextAsync(address); + await _clipboardService.WriteTextAsync(nextReceiveAddress); } - + private async Task RefreshFee() { // refresh fee if last refresh was 60 seconds ago