From 1a73ea2904f79cb597097f936cf1e5f3826d8030 Mon Sep 17 00:00:00 2001 From: TheDude Date: Tue, 26 Sep 2023 14:25:46 +0100 Subject: [PATCH 1/7] Added the nostr id to the create project transaction --- src/Angor/Client/Pages/Create.razor | 3 +- src/Angor/Shared/DerivationOperations.cs | 32 ++++++++++++++++++- src/Angor/Shared/Models/FounderPubKeys.cs | 2 ++ src/Angor/Shared/Models/ProjectInfo.cs | 2 ++ .../ProtocolNew/FounderTransactionActions.cs | 4 +-- .../ProtocolNew/IFounderTransactionActions.cs | 2 +- .../Scripts/IProjectScriptsBuilder.cs | 2 +- .../Scripts/ProjectScriptsBuilder.cs | 5 +-- 8 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/Angor/Client/Pages/Create.razor b/src/Angor/Client/Pages/Create.razor index a34f35fa..fbd7f5b6 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -183,6 +183,7 @@ project.FounderKey = projectsKeys.FounderKey; project.FounderRecoveryKey = projectsKeys.FounderRecoveryKey; project.ProjectIdentifier = projectsKeys.ProjectIdentifier; + project.NostrPubKey = projectsKeys.NostrPubKey; } return Task.CompletedTask; @@ -224,7 +225,7 @@ var feeEstimation = await _WalletOperations.GetFeeEstimationAsync(); - var transaction = _founderTransactionActions.CreateNewProjectTransaction(project.FounderKey, _derivationOperations.AngorKeyToScript(project.ProjectIdentifier), 10000); + var transaction = _founderTransactionActions.CreateNewProjectTransaction(project.FounderKey, _derivationOperations.AngorKeyToScript(project.ProjectIdentifier), 10000, project.NostrPubKey); signedTransaction = _WalletOperations.AddInputsAndSignTransaction(accountInfo.GetNextChangeReceiveAddress(), transaction, _walletStorage.GetWallet(), accountInfo, feeEstimation.First()); diff --git a/src/Angor/Shared/DerivationOperations.cs b/src/Angor/Shared/DerivationOperations.cs index 686cde27..3f6c812f 100644 --- a/src/Angor/Shared/DerivationOperations.cs +++ b/src/Angor/Shared/DerivationOperations.cs @@ -32,12 +32,14 @@ public FounderKeyCollection DeriveProjectKeys(WalletWords walletWords, string an var founderKey = DeriveFounderKey(walletWords, i); var founderRecoveryKey = DeriveFounderRecoveryKey(walletWords, i); var projectIdentifier = DeriveAngorKey(founderKey, angorTestKey); - + var nostrPubKey = DeriveNostrId(walletWords, i); + founderKeyCollection.Keys.Add(new FounderKeys { ProjectIdentifier = projectIdentifier, FounderRecoveryKey = founderRecoveryKey, FounderKey = founderKey, + NostrPubKey = nostrPubKey, Index = i }); } @@ -176,6 +178,34 @@ public string DeriveFounderKey(WalletWords walletWords, int index) return extPubKey.PubKey.ToHex(); } + + public string DeriveNostrId(WalletWords walletWords, int index) + { + // founder key is derived from the path m/5' + + Network network = _networkConfiguration.GetNetwork(); + + ExtKey extendedKey; + try + { + extendedKey = _hdOperations.GetExtendedKey(walletWords.Words, walletWords.Passphrase); + } + catch (NotSupportedException ex) + { + _logger.LogError("Exception occurred: {0}", ex.ToString()); + + if (ex.Message == "Unknown") + throw new Exception("Please make sure you enter valid mnemonic words."); + + throw; + } + + var path = $"m/44'/1237'/{index}/0/0"; + + ExtPubKey extPubKey = _hdOperations.GetExtendedPublicKey(extendedKey.PrivateKey, extendedKey.ChainCode, path); + + return extPubKey.PubKey.ToHex(); + } public string DeriveFounderRecoveryKey(WalletWords walletWords, int index) { diff --git a/src/Angor/Shared/Models/FounderPubKeys.cs b/src/Angor/Shared/Models/FounderPubKeys.cs index c4ec2ca2..262679cc 100644 --- a/src/Angor/Shared/Models/FounderPubKeys.cs +++ b/src/Angor/Shared/Models/FounderPubKeys.cs @@ -13,5 +13,7 @@ public class FounderKeys public string ProjectIdentifier { get; set; } + public string NostrPubKey { get; set; } + public int Index { get; set; } } \ No newline at end of file diff --git a/src/Angor/Shared/Models/ProjectInfo.cs b/src/Angor/Shared/Models/ProjectInfo.cs index 2ef654a9..f9812c6e 100644 --- a/src/Angor/Shared/Models/ProjectInfo.cs +++ b/src/Angor/Shared/Models/ProjectInfo.cs @@ -10,6 +10,8 @@ public class ProjectInfo public string FounderKey { get; set; } public string FounderRecoveryKey { get; set; } public string ProjectIdentifier { get; set; } + + public string NostrPubKey { get; set; } public DateTime StartDate { get; set; } public DateTime PenaltyDate { get; set; } public DateTime ExpiryDate { get; set; } diff --git a/src/Angor/Shared/ProtocolNew/FounderTransactionActions.cs b/src/Angor/Shared/ProtocolNew/FounderTransactionActions.cs index 294d2887..4533e367 100644 --- a/src/Angor/Shared/ProtocolNew/FounderTransactionActions.cs +++ b/src/Angor/Shared/ProtocolNew/FounderTransactionActions.cs @@ -135,7 +135,7 @@ public Transaction SpendFounderStage(ProjectInfo projectInfo, IEnumerable inves int stageNumber, Script founderRecieveAddress, string founderPrivateKey, FeeEstimation fee); - Transaction CreateNewProjectTransaction(string founderKey, Script angorKey, long angorFeeSatoshis); + Transaction CreateNewProjectTransaction(string founderKey, Script angorKey, long angorFeeSatoshis, string nostrPubKey); } \ No newline at end of file diff --git a/src/Angor/Shared/ProtocolNew/Scripts/IProjectScriptsBuilder.cs b/src/Angor/Shared/ProtocolNew/Scripts/IProjectScriptsBuilder.cs index 321c8f7e..a14db20a 100644 --- a/src/Angor/Shared/ProtocolNew/Scripts/IProjectScriptsBuilder.cs +++ b/src/Angor/Shared/ProtocolNew/Scripts/IProjectScriptsBuilder.cs @@ -7,7 +7,7 @@ public interface IProjectScriptsBuilder { Script GetAngorFeeOutputScript(string angorKey); Script BuildInvestorInfoScript(string investorKey); - Script BuildFounderInfoScript(string founderKey); + Script BuildFounderInfoScript(string founderKey, string nostrPubKey); Script BuildSeederInfoScript(string investorKey, uint256 secretHash); diff --git a/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs b/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs index cdb2106c..e8212dbe 100644 --- a/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs +++ b/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs @@ -23,10 +23,11 @@ public Script BuildInvestorInfoScript(string investorKey) Op.GetPushOp(new PubKey(investorKey).ToBytes())); } - public Script BuildFounderInfoScript(string founderKey) + public Script BuildFounderInfoScript(string founderKey, string nostrPuKey) { return new Script(OpcodeType.OP_RETURN, - Op.GetPushOp(new PubKey(founderKey).ToBytes())); + Op.GetPushOp(new PubKey(founderKey).ToBytes()), + Op.GetPushOp(new PubKey(nostrPuKey).ToBytes())); } public Script BuildSeederInfoScript(string investorKey, uint256 secretHash) From dfb3fb0b32bdccc82cda57ab7dedf125f78115ce Mon Sep 17 00:00:00 2001 From: TheDude Date: Thu, 5 Oct 2023 15:10:23 +0100 Subject: [PATCH 2/7] Started converting the relay service to communication with the relay --- src/Angor/Client/Pages/Browse.razor | 1 + src/Angor/Client/Pages/Create.razor | 1 + src/Angor/Client/Program.cs | 1 + src/Angor/Shared/Angor.Shared.csproj | 1 + src/Angor/Shared/Services/ISessionStorage.cs | 9 ++ src/Angor/Shared/Services/RelayService.cs | 90 +++++++++++++++----- 6 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 src/Angor/Shared/Services/ISessionStorage.cs diff --git a/src/Angor/Client/Pages/Browse.razor b/src/Angor/Client/Pages/Browse.razor index 83fdd505..9e702a5a 100644 --- a/src/Angor/Client/Pages/Browse.razor +++ b/src/Angor/Client/Pages/Browse.razor @@ -2,6 +2,7 @@ @using Angor.Client.Storage @using Angor.Client.Services @using Angor.Shared.Models +@using Angor.Shared.Services @inject HttpClient Http @inject IClientStorage storage; @inject NavigationManager NavigationManager diff --git a/src/Angor/Client/Pages/Create.razor b/src/Angor/Client/Pages/Create.razor index fbd7f5b6..e81cdfaf 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -6,6 +6,7 @@ @using Blockcore.Consensus.TransactionInfo @using Angor.Client.Services @using Angor.Shared.ProtocolNew +@using Angor.Shared.Services @using Blockcore.NBitcoin @using Blockcore.NBitcoin.DataEncoders diff --git a/src/Angor/Client/Program.cs b/src/Angor/Client/Program.cs index b01b1d1b..f976fe6d 100644 --- a/src/Angor/Client/Program.cs +++ b/src/Angor/Client/Program.cs @@ -6,6 +6,7 @@ using Angor.Shared.ProtocolNew; using Angor.Shared.ProtocolNew.Scripts; using Angor.Shared.ProtocolNew.TransactionBuilders; +using Angor.Shared.Services; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; diff --git a/src/Angor/Shared/Angor.Shared.csproj b/src/Angor/Shared/Angor.Shared.csproj index 00cf5d90..91b0143b 100644 --- a/src/Angor/Shared/Angor.Shared.csproj +++ b/src/Angor/Shared/Angor.Shared.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Angor/Shared/Services/ISessionStorage.cs b/src/Angor/Shared/Services/ISessionStorage.cs new file mode 100644 index 00000000..4c5b0de1 --- /dev/null +++ b/src/Angor/Shared/Services/ISessionStorage.cs @@ -0,0 +1,9 @@ +using Angor.Shared.Models; + +namespace Angor.Client.Services; + +public interface ISessionStorage +{ + void StoreProjectInfo(ProjectInfo project); + ProjectInfo? GetProjectById(string projectId); +} \ No newline at end of file diff --git a/src/Angor/Shared/Services/RelayService.cs b/src/Angor/Shared/Services/RelayService.cs index 1879608d..a1caa74c 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -1,46 +1,94 @@ -using System.Net.Http.Json; +using Angor.Client.Services; using Angor.Shared.Models; +using Nostr.Client.Messages.Metadata; +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; +using Nostr.Client.Requests; -namespace Angor.Client.Services +namespace Angor.Shared.Services { public interface IRelayService { - Task> GetProjectsAsync(); - Task AddProjectAsync(ProjectInfo project); + Task AddProjectAsync(ProjectInfo project, string nsec); Task GetProjectAsync(string projectId); } public class RelayService : IRelayService { - - private readonly HttpClient _httpClient; + + private readonly INostrClient _nostrClient; + // private const string nsec = "nsec1l0a7m5dlg4h9wurhnmgsq5nv9cqyvdwsutk4yf3w4fzzaqw7n80ssdfzkg"; private readonly string _baseUrl = "/api/Test"; // "https://your-base-url/api/test"; - public RelayService(HttpClient httpClient) + private readonly ISessionStorage _storage; + private ILogger _logger; + + public RelayService(INostrClient httpClient, ISessionStorage storage, ILogger logger) { - _httpClient = httpClient; + _nostrClient = httpClient; + _storage = storage; + _logger = logger; } - public async Task> GetProjectsAsync() + public async Task RequestProjectDataAsync(string nostrPubKey) { - var response = await _httpClient.GetAsync($"{_baseUrl}"); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadFromJsonAsync>(); - } - public async Task AddProjectAsync(ProjectInfo project) + _nostrClient.Send(new NostrFilter + { Authors = new[] { nostrPubKey }, Kinds = new[] { NostrKind.Metadata } }); + + var url = new Uri("wss://relay.damus.io"); + + using var communicator = new NostrWebsocketCommunicator(url); + + _nostrClient.Streams.EventStream.Subscribe(response => + { + var ev = response.Event; + _logger.LogInformation("{kind}: {content}", ev?.Kind, ev?.Content); + + if (ev is not NostrMetadataEvent evm) + return; + + var projectInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(ev.Content); + _storage.StoreProjectInfo(projectInfo); + _logger.LogInformation("Name: {name}, about: {about}", evm.Metadata?.Name, evm.Metadata?.About + }); + + await communicator.Start(); + } + + public Task AddProjectAsync(ProjectInfo project, string nsec) { - var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}", project); - response.EnsureSuccessStatusCode(); + var content = Newtonsoft.Json.JsonConvert.SerializeObject(project); + + var ev = new NostrEvent + { + Kind = NostrKind.Metadata, + CreatedAt = DateTime.UtcNow, + Content = content, + Pubkey = project.NostrPubKey, + Tags = new NostrEventTags(new NostrEventTag("ProjectDeclaration")) + }; + + var key = NostrPrivateKey.FromBech32(nsec); + var signed = ev.Sign(key); + + _nostrClient.Send(new NostrEventRequest(signed)); + + _storage.StoreProjectInfo(project); + + return Task.CompletedTask; } - public async Task GetProjectAsync(string projectId) + public Task GetProjectAsync(string projectId) { - var response = await _httpClient.GetAsync($"{_baseUrl}/project/{projectId}"); + var projectInfo = _storage.GetProjectById(projectId); - return response.IsSuccessStatusCode - ? await response.Content.ReadFromJsonAsync() - : null; + return Task.FromResult(projectInfo); } } From d4df9b20995da9cfc40b9ce8cb643aacd6d4ca5c Mon Sep 17 00:00:00 2001 From: TheDude Date: Thu, 12 Oct 2023 17:26:23 +0100 Subject: [PATCH 3/7] Tests communicating the test relay --- src/Angor/Client/Angor.Client.csproj | 2 + src/Angor/Client/Pages/Browse.razor | 9 ++- src/Angor/Client/Pages/Create.razor | 6 +- .../Client/Storage/LocalSessionStorage.cs | 25 ++++++ src/Angor/Shared/DerivationOperations.cs | 30 +++++++ src/Angor/Shared/IDerivationOperations.cs | 1 + src/Angor/Shared/Services/IndexerService.cs | 1 + src/Angor/Shared/Services/RelayService.cs | 81 +++++++++++++++---- 8 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 src/Angor/Client/Storage/LocalSessionStorage.cs diff --git a/src/Angor/Client/Angor.Client.csproj b/src/Angor/Client/Angor.Client.csproj index d15028b9..fa627785 100644 --- a/src/Angor/Client/Angor.Client.csproj +++ b/src/Angor/Client/Angor.Client.csproj @@ -9,10 +9,12 @@ + + diff --git a/src/Angor/Client/Pages/Browse.razor b/src/Angor/Client/Pages/Browse.razor index 9e702a5a..310059a9 100644 --- a/src/Angor/Client/Pages/Browse.razor +++ b/src/Angor/Client/Pages/Browse.razor @@ -53,13 +53,18 @@ protected override async Task OnInitializedAsync() { - + await _RelayService.ConnectToRelaysAsync(); } private async Task SearchProjects() { var blockchainProjects = await _IndexerService.GetProjectsAsync(); + // foreach (var blockchainProject in blockchainProjects) + // { + // _RelayService.RequestProjectDataAsync(blockchainProject.) + // } + foreach (var blockchainProject in blockchainProjects) { var project = await _RelayService.GetProjectAsync(blockchainProject.ProjectIdentifier); @@ -73,7 +78,7 @@ private void ImportProject(ProjectInfo project) { - storage.AddBrowseProject(project); + storage.AddBrowseProject(project); NavigationManager.NavigateTo($"/view/{project.ProjectIdentifier}"); } diff --git a/src/Angor/Client/Pages/Create.razor b/src/Angor/Client/Pages/Create.razor index e81cdfaf..cc090bad 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -259,8 +259,10 @@ if (!response.Success) return response; - await _RelayService.AddProjectAsync(project); - + var nostrKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); + + await _RelayService.AddProjectAsync(project, NBitcoin.DataEncoders.Encoders.Hex.EncodeData(nostrKey.ToBytes())); + // todo this code must be reviewed again as we send the recovery private key to the signing server var key = _derivationOperations.DeriveFounderRecoveryPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); diff --git a/src/Angor/Client/Storage/LocalSessionStorage.cs b/src/Angor/Client/Storage/LocalSessionStorage.cs new file mode 100644 index 00000000..5846beec --- /dev/null +++ b/src/Angor/Client/Storage/LocalSessionStorage.cs @@ -0,0 +1,25 @@ +using Angor.Client.Services; +using Angor.Shared.Models; +using Blazored.SessionStorage; + +namespace Angor.Client.Storage; + +public class LocalSessionStorage : ISessionStorage +{ + private ISyncSessionStorageService _sessionStorageService; + + public LocalSessionStorage(ISyncSessionStorageService sessionStorageService) + { + _sessionStorageService = sessionStorageService; + } + + public void StoreProjectInfo(ProjectInfo project) + { + _sessionStorageService.SetItem(project.ProjectIdentifier,project); + } + + public ProjectInfo? GetProjectById(string projectId) + { + return _sessionStorageService.GetItem(projectId); + } +} \ No newline at end of file diff --git a/src/Angor/Shared/DerivationOperations.cs b/src/Angor/Shared/DerivationOperations.cs index 3f6c812f..abe19e86 100644 --- a/src/Angor/Shared/DerivationOperations.cs +++ b/src/Angor/Shared/DerivationOperations.cs @@ -294,6 +294,36 @@ public Key DeriveFounderRecoveryPrivateKey(WalletWords walletWords, int index) return extKey.PrivateKey; } + + public Key DeriveProjectNostrPrivateKey(WalletWords walletWords, int index) + { + // founder key is derived from the path m/5' + + + Network network = _networkConfiguration.GetNetwork(); + + + ExtKey extendedKey; + try + { + extendedKey = _hdOperations.GetExtendedKey(walletWords.Words, walletWords.Passphrase); + } + catch (NotSupportedException ex) + { + _logger.LogError("Exception occurred: {0}", ex.ToString()); + + if (ex.Message == "Unknown") + throw new Exception("Please make sure you enter valid mnemonic words."); + + throw; + } + + var path = $"m/44'/1237'/{index}/0/0"; + + ExtKey extKey = extendedKey.Derive(new KeyPath(path)); + + return extKey.PrivateKey; + } public uint DeriveProjectId(string founderKey) { diff --git a/src/Angor/Shared/IDerivationOperations.cs b/src/Angor/Shared/IDerivationOperations.cs index 00477523..1e24cb9b 100644 --- a/src/Angor/Shared/IDerivationOperations.cs +++ b/src/Angor/Shared/IDerivationOperations.cs @@ -19,4 +19,5 @@ public interface IDerivationOperations Key DeriveFounderPrivateKey(WalletWords walletWords, int index); Key DeriveFounderRecoveryPrivateKey(WalletWords walletWords, int index); + Key DeriveProjectNostrPrivateKey(WalletWords walletWords, int index); } \ No newline at end of file diff --git a/src/Angor/Shared/Services/IndexerService.cs b/src/Angor/Shared/Services/IndexerService.cs index 1dccb85b..29cfa200 100644 --- a/src/Angor/Shared/Services/IndexerService.cs +++ b/src/Angor/Shared/Services/IndexerService.cs @@ -23,6 +23,7 @@ public class ProjectIndexerData public string FounderKey { get; set; } public string ProjectIdentifier { get; set; } public string TrxId { get; set; } + public string NostrPubKey { get; set; } } public class ProjectInvestment diff --git a/src/Angor/Shared/Services/RelayService.cs b/src/Angor/Shared/Services/RelayService.cs index a1caa74c..0ec7e3e0 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -14,36 +14,84 @@ namespace Angor.Shared.Services { public interface IRelayService { + Task ConnectToRelaysAsync(); + Task AddProjectAsync(ProjectInfo project, string nsec); Task GetProjectAsync(string projectId); + + Task RequestProjectDataAsync(string nostrPubKey); } public class RelayService : IRelayService { - private readonly INostrClient _nostrClient; - // private const string nsec = "nsec1l0a7m5dlg4h9wurhnmgsq5nv9cqyvdwsutk4yf3w4fzzaqw7n80ssdfzkg"; - private readonly string _baseUrl = "/api/Test"; // "https://your-base-url/api/test"; + private INostrClient _nostrClient; + private INostrCommunicator _nostrCommunicator; private readonly ISessionStorage _storage; private ILogger _logger; - public RelayService(INostrClient httpClient, ISessionStorage storage, ILogger logger) + private ILogger _clientLogger; + + public RelayService(ISessionStorage storage, ILogger logger, + ILogger clientLogger) { - _nostrClient = httpClient; _storage = storage; _logger = logger; + //_nostrCommunicator = nostrCommunicator; + _clientLogger = clientLogger; } - public async Task RequestProjectDataAsync(string nostrPubKey) + public async Task ConnectToRelaysAsync() { + _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")); - _nostrClient.Send(new NostrFilter - { Authors = new[] { nostrPubKey }, Kinds = new[] { NostrKind.Metadata } }); + _nostrCommunicator.Name = "angor-relay.test"; - var url = new Uri("wss://relay.damus.io"); + _nostrCommunicator.DisconnectionHappened.Subscribe(info => _logger.LogError(info.Exception,"[{relay}] Disconnected, type: {type}, reason: {reason}", "test", info.Type, info.CloseStatus)); + + await _nostrCommunicator.StartOrFail(); + + _nostrClient = + new NostrWebsocketClient(_nostrCommunicator, _clientLogger); - using var communicator = new NostrWebsocketCommunicator(url); + _nostrClient.Streams.UnknownMessageStream.Subscribe(_ => + _logger.LogError(_.ToString(), "unknown event")); + + _nostrClient.Send(new NostrFilter + { Authors = new[] { "c62a0e7f62d990eca33b9a56799137d55de4b7fb65e5fcc307ec010c01dc1b5c" }, Kinds = new[] { NostrKind.Metadata } }); + + + // _nostrCommunicator.DisconnectionHappened.Subscribe(_ => _logger.LogError(_.Exception, "failed to connect")); + // + // await _nostrCommunicator.StartOrFail(); + // using var communicator = new NostrWebsocketCommunicator(new Uri("wss://angor-relay-web.test")); + // + // using ( var client = new NostrWebsocketClient(communicator,_clientLogger)) + // { + // client.Communicator.DisconnectionHappened.Subscribe(_ => _logger.LogError(_.Exception, "failed to connect")); + // + // client.Streams.EventStream.Subscribe(_ => _logger.LogInformation(_.Event.Content)); + // + // await client.Communicator.StartOrFail(); + // + // client.Send(new NostrFilter + // { Authors = new[] { "c62a0e7f62d990eca33b9a56799137d55de4b7fb65e5fcc307ec010c01dc1b5c" }, Kinds = new[] { NostrKind.Metadata } }); + // } + // + // + // var test = + // new NostrWebsocketClient(new NostrWebsocketCommunicator(new Uri("wss://localhost:3000")),_clientLogger); + // + // test.Communicator.DisconnectionHappened.Subscribe(_ => _logger.LogError(_.Exception, "failed to connect")); + // + // await test.Communicator.StartOrFail(); + } + + public Task RequestProjectDataAsync(string nostrPubKey) + { + _nostrClient.Send(new NostrFilter + { Authors = new[] { nostrPubKey }, Kinds = new[] { NostrKind.Metadata } }); _nostrClient.Streams.EventStream.Subscribe(response => { @@ -55,13 +103,14 @@ public async Task RequestProjectDataAsync(string nostrPubKey) var projectInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(ev.Content); _storage.StoreProjectInfo(projectInfo); - _logger.LogInformation("Name: {name}, about: {about}", evm.Metadata?.Name, evm.Metadata?.About + _logger.LogInformation("Name: {name}, about: {about}", evm.Metadata?.Name, evm.Metadata?.About); }); - await communicator.Start(); + return Task.CompletedTask; + //await _nostrCommunicator.Start(); } - - public Task AddProjectAsync(ProjectInfo project, string nsec) + + public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) { var content = Newtonsoft.Json.JsonConvert.SerializeObject(project); @@ -73,8 +122,8 @@ public Task AddProjectAsync(ProjectInfo project, string nsec) Pubkey = project.NostrPubKey, Tags = new NostrEventTags(new NostrEventTag("ProjectDeclaration")) }; - - var key = NostrPrivateKey.FromBech32(nsec); + + var key = NostrPrivateKey.FromHex(hexPrivateKey); var signed = ev.Sign(key); _nostrClient.Send(new NostrEventRequest(signed)); From 94a81d0a4c8a2c909bdbe2334d57ec6cdfb9018f Mon Sep 17 00:00:00 2001 From: TheDude Date: Mon, 16 Oct 2023 16:30:17 +0100 Subject: [PATCH 4/7] Work on communication with the nostr relay --- src/Angor/Client/Pages/Browse.razor | 28 +++--- src/Angor/Client/Program.cs | 3 + .../Client/Storage/LocalSessionStorage.cs | 18 ++++ src/Angor/Shared/DerivationOperations.cs | 4 +- .../Scripts/ProjectScriptsBuilder.cs | 2 +- src/Angor/Shared/Services/ISessionStorage.cs | 2 + src/Angor/Shared/Services/RelayService.cs | 95 ++++++++----------- 7 files changed, 79 insertions(+), 73 deletions(-) diff --git a/src/Angor/Client/Pages/Browse.razor b/src/Angor/Client/Pages/Browse.razor index 310059a9..f5a2ce25 100644 --- a/src/Angor/Client/Pages/Browse.razor +++ b/src/Angor/Client/Pages/Browse.razor @@ -1,7 +1,6 @@ @page "/browse" @using Angor.Client.Storage @using Angor.Client.Services -@using Angor.Shared.Models @using Angor.Shared.Services @inject HttpClient Http @inject IClientStorage storage; @@ -37,7 +36,7 @@
@project.ProjectIdentifier

Nostr ID: @(new Blockcore.NBitcoin.Key().PubKey.ToHex())

- +
} @@ -49,7 +48,7 @@ @code { private string searchQuery; - private List projects= new(); + private List projects= new(); protected override async Task OnInitializedAsync() { @@ -60,26 +59,21 @@ { var blockchainProjects = await _IndexerService.GetProjectsAsync(); - // foreach (var blockchainProject in blockchainProjects) - // { - // _RelayService.RequestProjectDataAsync(blockchainProject.) - // } - foreach (var blockchainProject in blockchainProjects) { - var project = await _RelayService.GetProjectAsync(blockchainProject.ProjectIdentifier); + await _RelayService.RequestProjectDataAsync(blockchainProject.NostrPubKey); - if (project != null) - { - projects.Add(project); - } + projects.Add(blockchainProject); } + StateHasChanged(); } - private void ImportProject(ProjectInfo project) + private void ImportProject(string projectIdentifier) { - storage.AddBrowseProject(project); - - NavigationManager.NavigateTo($"/view/{project.ProjectIdentifier}"); + //storage.AddBrowseProject(project); + + //var project = await _RelayService.GetProjectAsync(projectIdentifier); + + NavigationManager.NavigateTo($"/view/{projectIdentifier}"); } } \ No newline at end of file diff --git a/src/Angor/Client/Program.cs b/src/Angor/Client/Program.cs index f976fe6d..c5370dd0 100644 --- a/src/Angor/Client/Program.cs +++ b/src/Angor/Client/Program.cs @@ -8,6 +8,7 @@ using Angor.Shared.ProtocolNew.TransactionBuilders; using Angor.Shared.Services; using Blazored.LocalStorage; +using Blazored.SessionStorage; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -18,11 +19,13 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddBlazoredLocalStorage(); +builder.Services.AddBlazoredSessionStorage(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient (); builder.Services.AddTransient (); +builder.Services.AddScoped(); builder.Services.AddTransient(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/Angor/Client/Storage/LocalSessionStorage.cs b/src/Angor/Client/Storage/LocalSessionStorage.cs index 5846beec..a1a31d91 100644 --- a/src/Angor/Client/Storage/LocalSessionStorage.cs +++ b/src/Angor/Client/Storage/LocalSessionStorage.cs @@ -8,6 +8,8 @@ public class LocalSessionStorage : ISessionStorage { private ISyncSessionStorageService _sessionStorageService; + private const string NostrStreamSubscriptions = "subscriptions"; + public LocalSessionStorage(ISyncSessionStorageService sessionStorageService) { _sessionStorageService = sessionStorageService; @@ -18,6 +20,22 @@ public void StoreProjectInfo(ProjectInfo project) _sessionStorageService.SetItem(project.ProjectIdentifier,project); } + public void AddProjectToSubscribedList(string nostrPubKey) + { + var list = _sessionStorageService.GetItem>(NostrStreamSubscriptions) ?? new List(); + + list.Add(nostrPubKey); + + _sessionStorageService.SetItem(NostrStreamSubscriptions, list); + } + + public bool IsProjectInSubscribedList(string nostrPubKey) + { + var list = _sessionStorageService.GetItem>(NostrStreamSubscriptions) ?? new List(); + + return list.Contains(nostrPubKey); + } + public ProjectInfo? GetProjectById(string projectId) { return _sessionStorageService.GetItem(projectId); diff --git a/src/Angor/Shared/DerivationOperations.cs b/src/Angor/Shared/DerivationOperations.cs index abe19e86..8ea82267 100644 --- a/src/Angor/Shared/DerivationOperations.cs +++ b/src/Angor/Shared/DerivationOperations.cs @@ -32,7 +32,7 @@ public FounderKeyCollection DeriveProjectKeys(WalletWords walletWords, string an var founderKey = DeriveFounderKey(walletWords, i); var founderRecoveryKey = DeriveFounderRecoveryKey(walletWords, i); var projectIdentifier = DeriveAngorKey(founderKey, angorTestKey); - var nostrPubKey = DeriveNostrId(walletWords, i); + var nostrPubKey = DeriveNostrPubKey(walletWords, i); founderKeyCollection.Keys.Add(new FounderKeys { @@ -179,7 +179,7 @@ public string DeriveFounderKey(WalletWords walletWords, int index) return extPubKey.PubKey.ToHex(); } - public string DeriveNostrId(WalletWords walletWords, int index) + public string DeriveNostrPubKey(WalletWords walletWords, int index) { // founder key is derived from the path m/5' diff --git a/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs b/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs index e8212dbe..6710be11 100644 --- a/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs +++ b/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs @@ -27,7 +27,7 @@ public Script BuildFounderInfoScript(string founderKey, string nostrPuKey) { return new Script(OpcodeType.OP_RETURN, Op.GetPushOp(new PubKey(founderKey).ToBytes()), - Op.GetPushOp(new PubKey(nostrPuKey).ToBytes())); + Op.GetPushOp(new NBitcoin.PubKey(nostrPuKey).GetTaprootFullPubKey().ToBytes())); } public Script BuildSeederInfoScript(string investorKey, uint256 secretHash) diff --git a/src/Angor/Shared/Services/ISessionStorage.cs b/src/Angor/Shared/Services/ISessionStorage.cs index 4c5b0de1..94cfe20a 100644 --- a/src/Angor/Shared/Services/ISessionStorage.cs +++ b/src/Angor/Shared/Services/ISessionStorage.cs @@ -5,5 +5,7 @@ namespace Angor.Client.Services; public interface ISessionStorage { void StoreProjectInfo(ProjectInfo project); + void AddProjectToSubscribedList(string nostrPubKey); + bool IsProjectInSubscribedList(string nostrPubKey); ProjectInfo? GetProjectById(string projectId); } \ No newline at end of file diff --git a/src/Angor/Shared/Services/RelayService.cs b/src/Angor/Shared/Services/RelayService.cs index 0ec7e3e0..69f2f818 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -1,4 +1,5 @@ -using Angor.Client.Services; +using System.Reactive.Linq; +using Angor.Client.Services; using Angor.Shared.Models; using Nostr.Client.Messages.Metadata; using Nostr.Client.Requests; @@ -7,8 +8,6 @@ using Nostr.Client.Communicator; using Nostr.Client.Keys; using Nostr.Client.Messages; -using Nostr.Client.Messages.Metadata; -using Nostr.Client.Requests; namespace Angor.Shared.Services { @@ -25,8 +24,8 @@ public interface IRelayService public class RelayService : IRelayService { - private INostrClient _nostrClient; - private INostrCommunicator _nostrCommunicator; + private static INostrClient _nostrClient; + private static INostrCommunicator _nostrCommunicator; private readonly ISessionStorage _storage; private ILogger _logger; @@ -44,70 +43,60 @@ public RelayService(ISessionStorage storage, ILogger logger, public async Task ConnectToRelaysAsync() { + if (_nostrCommunicator != null) + return; + _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")); _nostrCommunicator.Name = "angor-relay.test"; + _nostrCommunicator.ReconnectTimeout = null; - _nostrCommunicator.DisconnectionHappened.Subscribe(info => _logger.LogError(info.Exception,"[{relay}] Disconnected, type: {type}, reason: {reason}", "test", info.Type, info.CloseStatus)); + _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)); await _nostrCommunicator.StartOrFail(); - _nostrClient = - new NostrWebsocketClient(_nostrCommunicator, _clientLogger); + _nostrClient = new NostrWebsocketClient(_nostrCommunicator, _clientLogger); - _nostrClient.Streams.UnknownMessageStream.Subscribe(_ => - _logger.LogError(_.ToString(), "unknown event")); - - _nostrClient.Send(new NostrFilter - { Authors = new[] { "c62a0e7f62d990eca33b9a56799137d55de4b7fb65e5fcc307ec010c01dc1b5c" }, Kinds = new[] { NostrKind.Metadata } }); + _nostrClient.Send(new NostrRequest("default",new NostrFilter { Kinds = new[] { NostrKind.Metadata } })); - - // _nostrCommunicator.DisconnectionHappened.Subscribe(_ => _logger.LogError(_.Exception, "failed to connect")); - // - // await _nostrCommunicator.StartOrFail(); - // using var communicator = new NostrWebsocketCommunicator(new Uri("wss://angor-relay-web.test")); - // - // using ( var client = new NostrWebsocketClient(communicator,_clientLogger)) - // { - // client.Communicator.DisconnectionHappened.Subscribe(_ => _logger.LogError(_.Exception, "failed to connect")); - // - // client.Streams.EventStream.Subscribe(_ => _logger.LogInformation(_.Event.Content)); - // - // await client.Communicator.StartOrFail(); - // - // client.Send(new NostrFilter - // { Authors = new[] { "c62a0e7f62d990eca33b9a56799137d55de4b7fb65e5fcc307ec010c01dc1b5c" }, Kinds = new[] { NostrKind.Metadata } }); - // } - // - // - // var test = - // new NostrWebsocketClient(new NostrWebsocketCommunicator(new Uri("wss://localhost:3000")),_clientLogger); - // - // test.Communicator.DisconnectionHappened.Subscribe(_ => _logger.LogError(_.Exception, "failed to connect")); - // - // await test.Communicator.StartOrFail(); - } + _nostrClient.Streams.UnknownMessageStream.Subscribe(_ => _logger.LogInformation($"UnknownMessageStream {_}",_.MessageType)); + _nostrClient.Streams.EventStream.Subscribe(_ => _logger.LogInformation($"EventStream {_.Subscription}", _.AdditionalData)); + _nostrClient.Streams.EoseStream.Subscribe(_ => _logger.LogInformation($"EoseStream on subscription - {_.Subscription}", _.AdditionalData)); + _nostrClient.Streams.OkStream.Subscribe(_ => _logger.LogInformation($"OkStream {_}", _.MessageType)); + _nostrClient.Streams.NoticeStream.Subscribe(_ => _logger.LogInformation($"NoticeStream {_}", _.MessageType)); + _nostrClient.Streams.UnknownRawStream.Subscribe(_ => _logger.LogInformation($"UnknownRawStream {_}", _.Message.MessageType.ToString())); + } public Task RequestProjectDataAsync(string nostrPubKey) { - _nostrClient.Send(new NostrFilter - { Authors = new[] { nostrPubKey }, Kinds = new[] { NostrKind.Metadata } }); + if (_storage.IsProjectInSubscribedList(nostrPubKey)) + return Task.CompletedTask; - _nostrClient.Streams.EventStream.Subscribe(response => - { - var ev = response.Event; - _logger.LogInformation("{kind}: {content}", ev?.Kind, ev?.Content); + _nostrClient.Send( new NostrRequest(nostrPubKey, new NostrFilter + { Authors = new[] { nostrPubKey }, Kinds = new[] { NostrKind.Metadata } })); - if (ev is not NostrMetadataEvent evm) - return; + _nostrClient.Streams.EventStream.Where(_ => _.Subscription == nostrPubKey) + .Select(_ => _.Event) + .Subscribe(ev => + { + _logger.LogInformation("{kind}: {content}", ev?.Kind, ev?.Content); - var projectInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(ev.Content); - _storage.StoreProjectInfo(projectInfo); - _logger.LogInformation("Name: {name}, about: {about}", evm.Metadata?.Name, evm.Metadata?.About); - }); + if (ev is not NostrMetadataEvent evm) + return; + + var projectInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(evm.Content); + _storage.StoreProjectInfo(projectInfo); + _logger.LogInformation($"Updated storage with project from nostr {projectInfo.ProjectIdentifier}"); + }); + _storage.AddProjectToSubscribedList(nostrPubKey); + return Task.CompletedTask; - //await _nostrCommunicator.Start(); } public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) @@ -120,7 +109,7 @@ public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) CreatedAt = DateTime.UtcNow, Content = content, Pubkey = project.NostrPubKey, - Tags = new NostrEventTags(new NostrEventTag("ProjectDeclaration")) + Tags = new NostrEventTags(new NostrEventTag("ProjectDeclaration")) //TODO need to find the correct tags for the event }; var key = NostrPrivateKey.FromHex(hexPrivateKey); From 30b77fbe5abe47e263e8b44a5b77b01e54a7e814 Mon Sep 17 00:00:00 2001 From: TheDude Date: Fri, 20 Oct 2023 12:12:32 +0100 Subject: [PATCH 5/7] More work on nostr, changed the subscription handling to be injected from the UI. --- src/Angor/Client/Pages/Browse.razor | 73 ++++++-- src/Angor/Client/Pages/Create.razor | 19 +- src/Angor/Client/Pages/View.razor | 4 +- .../Client/Storage/LocalSessionStorage.cs | 33 +++- .../Shared/Models/SignRecoveryRequest.cs | 3 + src/Angor/Shared/Services/ISessionStorage.cs | 5 + src/Angor/Shared/Services/RelayService.cs | 162 +++++++++++------- src/Angor/Shared/Services/SignService.cs | 55 ++++-- 8 files changed, 256 insertions(+), 98 deletions(-) diff --git a/src/Angor/Client/Pages/Browse.razor b/src/Angor/Client/Pages/Browse.razor index f5a2ce25..be9d5809 100644 --- a/src/Angor/Client/Pages/Browse.razor +++ b/src/Angor/Client/Pages/Browse.razor @@ -1,9 +1,9 @@ @page "/browse" -@using Angor.Client.Storage @using Angor.Client.Services +@using Angor.Shared.Models @using Angor.Shared.Services -@inject HttpClient Http -@inject IClientStorage storage; +@using Nostr.Client.Keys +@inject ISessionStorage SessionStorage; @inject NavigationManager NavigationManager @inject IRelayService _RelayService @inject IIndexerService _IndexerService @@ -13,20 +13,29 @@

Browse Projects

+ +
- +

@if (projects.Count == 0) { -

No projects found.

+ @if (searchInProgress) + { +
+ } + else + { +

No projects found.

+ } } else { @@ -35,8 +44,15 @@
@project.ProjectIdentifier
-

Nostr ID: @(new Blockcore.NBitcoin.Key().PubKey.ToHex())

- +

Nostr ID: @(NostrPublicKey.FromHex(project.NostrPubKey).Bech32)

+ @if (SessionStorage.IsProjectInStorageById(project.ProjectIdentifier)) + { + + } + else + { +
+ }
} @@ -46,7 +62,9 @@
@code { + NotificationComponent notificationComponent; private string searchQuery; + bool searchInProgress = false; private List projects= new(); @@ -57,23 +75,48 @@ private async Task SearchProjects() { + searchInProgress = true; var blockchainProjects = await _IndexerService.GetProjectsAsync(); + + var projectCreators = SessionStorage.GetProjectSubscribedList(); + projectCreators.AddRange( + blockchainProjects + .Select(_ => _.NostrPubKey) + .Where(nostrPubKey => !projectCreators.Contains(nostrPubKey))); + + await _RelayService.RequestProjectDataAsync(_ => + { + if (!SessionStorage.IsProjectInStorageById(_.ProjectIdentifier)) + SessionStorage.StoreProjectInfo(_); + StateHasChanged(); + }, + projectCreators.ToArray()); + + SessionStorage.SetProjectSubscribedList(projectCreators); + foreach (var blockchainProject in blockchainProjects) { - await _RelayService.RequestProjectDataAsync(blockchainProject.NostrPubKey); - - projects.Add(blockchainProject); + if (projects.All(_ => _.ProjectIdentifier != blockchainProject.ProjectIdentifier)) + { + projects.Add(blockchainProject); + } } + + searchInProgress = false; StateHasChanged(); } private void ImportProject(string projectIdentifier) { - //storage.AddBrowseProject(project); - - //var project = await _RelayService.GetProjectAsync(projectIdentifier); - - NavigationManager.NavigateTo($"/view/{projectIdentifier}"); + if (SessionStorage.IsProjectInStorageById(projectIdentifier)) + { + _RelayService.CloseConnection(); + NavigationManager.NavigateTo($"/view/{projectIdentifier}"); + } + else + { + notificationComponent.ShowNotificationMessage("The project was not loaded from the relay yet"); + } } } \ No newline at end of file diff --git a/src/Angor/Client/Pages/Create.razor b/src/Angor/Client/Pages/Create.razor index cc090bad..3034e360 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -2,7 +2,6 @@ @using Angor.Shared.Models @using Angor.Shared @using Angor.Client.Storage -@using Angor.Shared.Protocol @using Blockcore.Consensus.TransactionInfo @using Angor.Client.Services @using Angor.Shared.ProtocolNew @@ -10,7 +9,6 @@ @using Blockcore.NBitcoin @using Blockcore.NBitcoin.DataEncoders -@inject HttpClient Http @inject IDerivationOperations _derivationOperations @inject IWalletStorage _walletStorage; @inject IClientStorage storage; @@ -18,7 +16,6 @@ @inject IWalletOperations _WalletOperations @inject INetworkConfiguration _NetworkConfiguration @inject IRelayService _RelayService -@inject IIndexerService _IndexerService @inject ISignService _SignService @@ -259,11 +256,21 @@ if (!response.Success) return response; + storage.AddFounderProject(project); + var nostrKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); - await _RelayService.AddProjectAsync(project, NBitcoin.DataEncoders.Encoders.Hex.EncodeData(nostrKey.ToBytes())); + var resultId = await _RelayService.AddProjectAsync(project, NBitcoin.DataEncoders.Encoders.Hex.EncodeData(nostrKey.ToBytes())); + + _RelayService.RegisterOKMessageHandler(_ => + { + if (_.EventId != resultId) + return; + if (!_.Accepted) + notificationComponent.ShowErrorMessage("Failed to store the project information on the relay!!!"); //TODO add export project info + }); - // todo this code must be reviewed again as we send the recovery private key to the signing server + // todo this code must be reviewed again as we send the recovery private key to the signing server var key = _derivationOperations.DeriveFounderRecoveryPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); @@ -276,7 +283,7 @@ { notificationComponent.ShowNotificationMessage("Project created", 1); - storage.AddFounderProject(project); + //storage.AddFounderProject(project); NavigationManager.NavigateTo($"/view/{project.ProjectIdentifier}"); } diff --git a/src/Angor/Client/Pages/View.razor b/src/Angor/Client/Pages/View.razor index 3f595a90..17270879 100644 --- a/src/Angor/Client/Pages/View.razor +++ b/src/Angor/Client/Pages/View.razor @@ -3,11 +3,13 @@ @using Angor.Client.Storage @using Angor.Shared.Models @using Blockcore.NBitcoin +@using Angor.Client.Services @inject HttpClient Http @inject IDerivationOperations _derivationOperations @inject IWalletStorage _walletStorage; @inject IClientStorage storage; +@inject ISessionStorage SessionStorage; @inject NavigationManager NavigationManager @inject INetworkConfiguration _NetworkConfiguration @@ -218,7 +220,7 @@ } else { - findProject = storage.GetBrowseProjects().FirstOrDefault(p => p.ProjectIdentifier == ProjectId); + findProject = SessionStorage.GetProjectById(ProjectId); if (findProject != null) { diff --git a/src/Angor/Client/Storage/LocalSessionStorage.cs b/src/Angor/Client/Storage/LocalSessionStorage.cs index a1a31d91..7a3c44d2 100644 --- a/src/Angor/Client/Storage/LocalSessionStorage.cs +++ b/src/Angor/Client/Storage/LocalSessionStorage.cs @@ -8,7 +8,7 @@ public class LocalSessionStorage : ISessionStorage { private ISyncSessionStorageService _sessionStorageService; - private const string NostrStreamSubscriptions = "subscriptions"; + private const string NostrKeyEventStreamSubscription = "subscriptions"; public LocalSessionStorage(ISyncSessionStorageService sessionStorageService) { @@ -22,16 +22,25 @@ public void StoreProjectInfo(ProjectInfo project) public void AddProjectToSubscribedList(string nostrPubKey) { - var list = _sessionStorageService.GetItem>(NostrStreamSubscriptions) ?? new List(); + var list = _sessionStorageService.GetItem>(NostrKeyEventStreamSubscription) ?? new List(); list.Add(nostrPubKey); - _sessionStorageService.SetItem(NostrStreamSubscriptions, list); + _sessionStorageService.SetItem(NostrKeyEventStreamSubscription, list); + } + + public List GetProjectSubscribedList() + { + return _sessionStorageService.GetItem>(NostrKeyEventStreamSubscription) ?? new List(); + } + public void SetProjectSubscribedList(List list) + { + _sessionStorageService.SetItem>(NostrKeyEventStreamSubscription,list); } public bool IsProjectInSubscribedList(string nostrPubKey) { - var list = _sessionStorageService.GetItem>(NostrStreamSubscriptions) ?? new List(); + var list = _sessionStorageService.GetItem>(NostrKeyEventStreamSubscription) ?? new List(); return list.Contains(nostrPubKey); } @@ -40,4 +49,20 @@ public bool IsProjectInSubscribedList(string nostrPubKey) { return _sessionStorageService.GetItem(projectId); } + public bool IsProjectInStorageById(string projectId) + { + return _sessionStorageService.ContainKey(projectId); + } + + public void StoreProjectInfoEventId(string eventId, string projectInfo) + { + _sessionStorageService.SetItem(eventId,projectInfo); + } + + public ProjectInfo GetProjectInfoByEventId(string eventId) + { + var projectIdentifier = _sessionStorageService.GetItem(eventId); + + return _sessionStorageService.GetItem(projectIdentifier); + } } \ No newline at end of file diff --git a/src/Angor/Shared/Models/SignRecoveryRequest.cs b/src/Angor/Shared/Models/SignRecoveryRequest.cs index 0358d9a0..0cabbfbf 100644 --- a/src/Angor/Shared/Models/SignRecoveryRequest.cs +++ b/src/Angor/Shared/Models/SignRecoveryRequest.cs @@ -4,5 +4,8 @@ public class SignRecoveryRequest { public string ProjectIdentifier { get; set; } + public string InvestorNostrPrivateKey { get; set; } + public string NostrPubKey { get; set; } + public string InvestmentTransaction { get; set; } } \ No newline at end of file diff --git a/src/Angor/Shared/Services/ISessionStorage.cs b/src/Angor/Shared/Services/ISessionStorage.cs index 94cfe20a..e60989c0 100644 --- a/src/Angor/Shared/Services/ISessionStorage.cs +++ b/src/Angor/Shared/Services/ISessionStorage.cs @@ -6,6 +6,11 @@ public interface ISessionStorage { void StoreProjectInfo(ProjectInfo project); void AddProjectToSubscribedList(string nostrPubKey); + List GetProjectSubscribedList(); + void SetProjectSubscribedList(List nostrPubKeys); bool IsProjectInSubscribedList(string nostrPubKey); ProjectInfo? GetProjectById(string projectId); + bool IsProjectInStorageById(string projectId); + void StoreProjectInfoEventId(string eventId, string projectInfo); + ProjectInfo GetProjectInfoByEventId(string eventId); } \ No newline at end of file diff --git a/src/Angor/Shared/Services/RelayService.cs b/src/Angor/Shared/Services/RelayService.cs index 69f2f818..a918c0b5 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -1,44 +1,49 @@ using System.Reactive.Linq; -using Angor.Client.Services; using Angor.Shared.Models; -using Nostr.Client.Messages.Metadata; 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.Responses; namespace Angor.Shared.Services { public interface IRelayService { Task ConnectToRelaysAsync(); + + void RegisterEventMessageHandler(string eventId,Action action); + void RegisterOKMessageHandler(Action action); - Task AddProjectAsync(ProjectInfo project, string nsec); - Task GetProjectAsync(string projectId); + Task AddProjectAsync(ProjectInfo project, string nsec); + + Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKey); - Task RequestProjectDataAsync(string nostrPubKey); + void CloseConnection(); } public class RelayService : IRelayService { - - private static INostrClient _nostrClient; + private static NostrWebsocketClient _nostrClient; private static INostrCommunicator _nostrCommunicator; - - private readonly ISessionStorage _storage; + private ILogger _logger; private ILogger _clientLogger; - - public RelayService(ISessionStorage storage, ILogger logger, - ILogger clientLogger) + private ILogger _communicatorLogger; + + private Dictionary subscriptions = new(); + + public RelayService( + ILogger logger, + ILogger clientLogger, + ILogger communicatorLogger) { - _storage = storage; _logger = logger; - //_nostrCommunicator = nostrCommunicator; _clientLogger = clientLogger; + _communicatorLogger = communicatorLogger; } public async Task ConnectToRelaysAsync() @@ -49,67 +54,114 @@ public async Task ConnectToRelaysAsync() _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")); _nostrCommunicator.Name = "angor-relay.test"; - _nostrCommunicator.ReconnectTimeout = null; + _nostrCommunicator.ReconnectTimeout = null; //TODO need to check what is the actual best time to set here _nostrCommunicator.DisconnectionHappened.Subscribe(info => { - _logger.LogError(info.Exception, "Relay disconnected, type: {type}, reason: {reason}.", info.Type, info.CloseStatus); - _nostrCommunicator.Start(); + _communicatorLogger.LogError(info.Exception, "Relay disconnected, type: {Type}, reason: {CloseStatus}.", info.Type, info.CloseStatus); }); - _nostrCommunicator.MessageReceived.Subscribe(info => _logger.LogInformation(info.Text, "Relay message received, type: {type}", info.MessageType)); - await _nostrCommunicator.StartOrFail(); + _nostrCommunicator.MessageReceived.Subscribe(info => + { + _communicatorLogger.LogInformation("message received on communicator - {Text} Relay message received, type: {MessageType}",info.Text, info.MessageType); + }); + await _nostrCommunicator.StartOrFail(); + _nostrClient = new NostrWebsocketClient(_nostrCommunicator, _clientLogger); - _nostrClient.Send(new NostrRequest("default",new NostrFilter { Kinds = new[] { NostrKind.Metadata } })); + _nostrClient.Streams.UnknownMessageStream.Subscribe(_ => _clientLogger.LogInformation($"UnknownMessageStream {_}",_.MessageType)); + _nostrClient.Streams.EventStream.Subscribe(_ => _clientLogger.LogInformation($"EventStream {_.Subscription}", _.AdditionalData)); + _nostrClient.Streams.EoseStream.Subscribe(_ => _clientLogger.LogInformation($"EoseStream on subscription - {_.Subscription}", _.AdditionalData)); + + _nostrClient.Streams.OkStream.Subscribe(_ => _clientLogger.LogInformation($"OkStream {_.Accepted} message - {_.Message}")); + + _nostrClient.Streams.EoseStream.Subscribe(_ => + { + if (!subscriptions.ContainsKey(_.Subscription)) + return; + _clientLogger.LogInformation($"Disposing of subscription - {_.Subscription}"); + subscriptions[_.Subscription].Dispose(); + subscriptions.Remove(_.Subscription); + _clientLogger.LogInformation($"subscription disposed - {_.Subscription}"); + }); - _nostrClient.Streams.UnknownMessageStream.Subscribe(_ => _logger.LogInformation($"UnknownMessageStream {_}",_.MessageType)); - _nostrClient.Streams.EventStream.Subscribe(_ => _logger.LogInformation($"EventStream {_.Subscription}", _.AdditionalData)); - _nostrClient.Streams.EoseStream.Subscribe(_ => _logger.LogInformation($"EoseStream on subscription - {_.Subscription}", _.AdditionalData)); - _nostrClient.Streams.OkStream.Subscribe(_ => _logger.LogInformation($"OkStream {_}", _.MessageType)); - _nostrClient.Streams.NoticeStream.Subscribe(_ => _logger.LogInformation($"NoticeStream {_}", _.MessageType)); - _nostrClient.Streams.UnknownRawStream.Subscribe(_ => _logger.LogInformation($"UnknownRawStream {_}", _.Message.MessageType.ToString())); + _nostrClient.Streams.NoticeStream.Subscribe(_ => _clientLogger.LogInformation($"NoticeStream {_.Message}")); + _nostrClient.Streams.UnknownRawStream.Subscribe(_ => _clientLogger.LogInformation($"UnknownRawStream {_.Message}")); + } + + public void RegisterEventMessageHandler(string eventId ,Action action) + { + // var subscription = _nostrClient.Streams.EventStream + // .Where(_ => _.Subscription == "ProjectInfoLookups") + // .Where(_ => _.Event.Id == eventId) + // .Select(_ => _.Event) + // .Subscribe(ev => + // { + // action(Newtonsoft.Json.JsonConvert.DeserializeObject(ev.Content)); + // }); + // + // subscriptions.Add(eventId,subscription); } - public Task RequestProjectDataAsync(string nostrPubKey) + public void RegisterOKMessageHandler(Action action) { - if (_storage.IsProjectInSubscribedList(nostrPubKey)) - return Task.CompletedTask; - - _nostrClient.Send( new NostrRequest(nostrPubKey, new NostrFilter - { Authors = new[] { nostrPubKey }, Kinds = new[] { NostrKind.Metadata } })); - - _nostrClient.Streams.EventStream.Where(_ => _.Subscription == nostrPubKey) - .Select(_ => _.Event) - .Subscribe(ev => - { - _logger.LogInformation("{kind}: {content}", ev?.Kind, ev?.Content); + var subscription =_nostrClient.Streams.OkStream + .Subscribe(ev => { action(ev); }); + + subscriptions.Add("todo",subscription); + } - if (ev is not NostrMetadataEvent evm) - return; + public Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKeys) + { + string subscriptionName = "ProjectInfoLookups"; + _nostrClient.Send(new NostrRequest(subscriptionName, new NostrFilter + { + Authors = nostrPubKeys, + Kinds = new[] { NostrKind.ApplicationSpecificData, NostrKind.Metadata, (NostrKind)30402 }, + })); - var projectInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(evm.Content); - _storage.StoreProjectInfo(projectInfo); - _logger.LogInformation($"Updated storage with project from nostr {projectInfo.ProjectIdentifier}"); - }); + if (!subscriptions.ContainsKey(subscriptionName)) + { + var subscription = _nostrClient.Streams.EventStream + .Where(_ => _.Subscription == subscriptionName) + .Where(_ => nostrPubKeys.Contains(_.Event.Pubkey)) + .Select(_ => _.Event) + .Subscribe(ev => + { + responseDataAction(Newtonsoft.Json.JsonConvert.DeserializeObject(ev.Content)); + }); + + subscriptions.Add(subscriptionName, subscription); + } - _storage.AddProjectToSubscribedList(nostrPubKey); - return Task.CompletedTask; } - public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) + public void CloseConnection() + { + foreach (var subscription in subscriptions.Values) + { + subscription.Dispose(); + } + _nostrClient?.Dispose(); + _nostrCommunicator?.Dispose(); + } + + public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) { var content = Newtonsoft.Json.JsonConvert.SerializeObject(project); var ev = new NostrEvent { - Kind = NostrKind.Metadata, + Kind = NostrKind.ApplicationSpecificData, CreatedAt = DateTime.UtcNow, Content = content, Pubkey = project.NostrPubKey, - Tags = new NostrEventTags(new NostrEventTag("ProjectDeclaration")) //TODO need to find the correct tags for the event + Tags = new NostrEventTags(//TODO need to find the correct tags for the event + new NostrEventTag("d", "AngorApp", "Create a new project event"), + new NostrEventTag("L", "#projectInfo"), + new NostrEventTag("l", "ProjectDeclaration", "#projectInfo")) }; var key = NostrPrivateKey.FromHex(hexPrivateKey); @@ -117,18 +169,8 @@ public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) _nostrClient.Send(new NostrEventRequest(signed)); - _storage.StoreProjectInfo(project); - - return Task.CompletedTask; + return Task.FromResult(ev.Id); } - - public Task GetProjectAsync(string projectId) - { - var projectInfo = _storage.GetProjectById(projectId); - - return Task.FromResult(projectInfo); - } - } } diff --git a/src/Angor/Shared/Services/SignService.cs b/src/Angor/Shared/Services/SignService.cs index d3f5fe24..1e5f6a85 100644 --- a/src/Angor/Shared/Services/SignService.cs +++ b/src/Angor/Shared/Services/SignService.cs @@ -1,5 +1,10 @@ -using System.Net.Http.Json; -using Angor.Shared.Models; +using Angor.Shared.Models; +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 { @@ -12,25 +17,51 @@ public interface ISignService public class SignService : ISignService { - private readonly HttpClient _httpClient; - private readonly string _baseUrl = "/api/TestSign"; // "https://your-base-url/api/test"; + private static INostrClient _nostrClient; + private static INostrCommunicator _nostrCommunicator; - public SignService(HttpClient httpClient) + public SignService(ILogger _logger) { - _httpClient = httpClient; + _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")); + + _nostrCommunicator.Name = "angor-relay.test"; + _nostrCommunicator.ReconnectTimeout = null; + + _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); } public async Task AddSignKeyAsync(ProjectInfo project, string founderRecoveryPrivateKey) { - var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}", new SignData { ProjectIdentifier = project.ProjectIdentifier, FounderRecoveryPrivateKey = founderRecoveryPrivateKey }); - response.EnsureSuccessStatusCode(); + // var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}", new SignData { ProjectIdentifier = project.ProjectIdentifier, FounderRecoveryPrivateKey = founderRecoveryPrivateKey }); + // response.EnsureSuccessStatusCode(); } - public async Task GetInvestmentSigsAsync(SignRecoveryRequest signRecoveryRequest) + public Task GetInvestmentSigsAsync(SignRecoveryRequest signRecoveryRequest) { - var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}/sign", signRecoveryRequest); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadFromJsonAsync(); + var sender = NostrPrivateKey.FromHex(signRecoveryRequest.InvestorNostrPrivateKey); + var receiver = NostrPublicKey.FromHex(signRecoveryRequest.NostrPubKey); + + var ev = new NostrEvent + { + CreatedAt = DateTime.UtcNow, + Content = $"Test private message from C# client" + }; + + var encrypted = ev.EncryptDirect(sender, receiver); + var signed = encrypted.Sign(sender); + + _nostrClient.Send(new NostrEventRequest(signed)); + + return Task.FromResult(new SignatureInfo()); } } } From 93c1ece5353aa35b49c6896324fcd144a41abc1f Mon Sep 17 00:00:00 2001 From: TheDude Date: Fri, 20 Oct 2023 14:05:27 +0100 Subject: [PATCH 6/7] Some code refactoring --- src/Angor/Client/Pages/Create.razor | 6 +- src/Angor/Shared/Services/IRelayService.cs | 13 +++ src/Angor/Shared/Services/RelayService.cs | 111 +++++++++++---------- 3 files changed, 77 insertions(+), 53 deletions(-) create mode 100644 src/Angor/Shared/Services/IRelayService.cs diff --git a/src/Angor/Client/Pages/Create.razor b/src/Angor/Client/Pages/Create.razor index 3034e360..8e2f1325 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -166,7 +166,7 @@ } }; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { hasWallet = _walletStorage.HasWallet(); @@ -184,7 +184,7 @@ project.NostrPubKey = projectsKeys.NostrPubKey; } - return Task.CompletedTask; + await _RelayService.ConnectToRelaysAsync(); } private async Task CreatProject() @@ -262,7 +262,7 @@ var resultId = await _RelayService.AddProjectAsync(project, NBitcoin.DataEncoders.Encoders.Hex.EncodeData(nostrKey.ToBytes())); - _RelayService.RegisterOKMessageHandler(_ => + _RelayService.RegisterOKMessageHandler(resultId, _ => { if (_.EventId != resultId) return; diff --git a/src/Angor/Shared/Services/IRelayService.cs b/src/Angor/Shared/Services/IRelayService.cs new file mode 100644 index 00000000..331e4a12 --- /dev/null +++ b/src/Angor/Shared/Services/IRelayService.cs @@ -0,0 +1,13 @@ +using Angor.Shared.Models; +using Nostr.Client.Responses; + +namespace Angor.Shared.Services; + +public interface IRelayService +{ + Task ConnectToRelaysAsync(); + void RegisterOKMessageHandler(string eventId, Action action); + Task AddProjectAsync(ProjectInfo project, string nsec); + Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKey); + void CloseConnection(); +} \ No newline at end of file diff --git a/src/Angor/Shared/Services/RelayService.cs b/src/Angor/Shared/Services/RelayService.cs index a918c0b5..72a50df6 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -10,20 +10,6 @@ namespace Angor.Shared.Services { - public interface IRelayService - { - Task ConnectToRelaysAsync(); - - void RegisterEventMessageHandler(string eventId,Action action); - void RegisterOKMessageHandler(Action action); - - Task AddProjectAsync(ProjectInfo project, string nsec); - - Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKey); - - void CloseConnection(); - } - public class RelayService : IRelayService { private static NostrWebsocketClient _nostrClient; @@ -35,6 +21,7 @@ public class RelayService : IRelayService private ILogger _communicatorLogger; private Dictionary subscriptions = new(); + //private Dictionary> OkVerificationActions = new(); public RelayService( ILogger logger, @@ -51,14 +38,19 @@ public async Task ConnectToRelaysAsync() if (_nostrCommunicator != null) return; - _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")); + _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")) + { + Name = "angor-relay.test", + ReconnectTimeout = null //TODO need to check what is the actual best time to set here + }; - _nostrCommunicator.Name = "angor-relay.test"; - _nostrCommunicator.ReconnectTimeout = null; //TODO need to check what is the actual best time to set here - _nostrCommunicator.DisconnectionHappened.Subscribe(info => { - _communicatorLogger.LogError(info.Exception, "Relay disconnected, type: {Type}, reason: {CloseStatus}.", info.Type, info.CloseStatus); + 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); + }); _nostrCommunicator.MessageReceived.Subscribe(info => @@ -73,8 +65,17 @@ public async Task ConnectToRelaysAsync() _nostrClient.Streams.UnknownMessageStream.Subscribe(_ => _clientLogger.LogInformation($"UnknownMessageStream {_}",_.MessageType)); _nostrClient.Streams.EventStream.Subscribe(_ => _clientLogger.LogInformation($"EventStream {_.Subscription}", _.AdditionalData)); _nostrClient.Streams.EoseStream.Subscribe(_ => _clientLogger.LogInformation($"EoseStream on subscription - {_.Subscription}", _.AdditionalData)); - - _nostrClient.Streams.OkStream.Subscribe(_ => _clientLogger.LogInformation($"OkStream {_.Accepted} message - {_.Message}")); + _nostrClient.Streams.NoticeStream.Subscribe(_ => _clientLogger.LogInformation($"NoticeStream {_.Message}")); + _nostrClient.Streams.UnknownRawStream.Subscribe(_ => _clientLogger.LogInformation($"UnknownRawStream {_.Message}")); + _nostrClient.Streams.OkStream. Subscribe(_ => + { + _clientLogger.LogInformation($"OkStream {_.Accepted} message - {_.Message}"); + // if(OkVerificationSubscription.ContainsKey(_.EventId)) + // { + // OkVerificationActions[_.EventId](_); + // OkVerificationActions.Remove(_.EventId); + // } + }); _nostrClient.Streams.EoseStream.Subscribe(_ => { @@ -86,30 +87,12 @@ public async Task ConnectToRelaysAsync() _clientLogger.LogInformation($"subscription disposed - {_.Subscription}"); }); - _nostrClient.Streams.NoticeStream.Subscribe(_ => _clientLogger.LogInformation($"NoticeStream {_.Message}")); - _nostrClient.Streams.UnknownRawStream.Subscribe(_ => _clientLogger.LogInformation($"UnknownRawStream {_.Message}")); - } - public void RegisterEventMessageHandler(string eventId ,Action action) - { - // var subscription = _nostrClient.Streams.EventStream - // .Where(_ => _.Subscription == "ProjectInfoLookups") - // .Where(_ => _.Event.Id == eventId) - // .Select(_ => _.Event) - // .Subscribe(ev => - // { - // action(Newtonsoft.Json.JsonConvert.DeserializeObject(ev.Content)); - // }); - // - // subscriptions.Add(eventId,subscription); } - - public void RegisterOKMessageHandler(Action action) + + public void RegisterOKMessageHandler(string eventId, Action action) { - var subscription =_nostrClient.Streams.OkStream - .Subscribe(ev => { action(ev); }); - - subscriptions.Add("todo",subscription); + //OkVerificationActions.Add(eventId,action); } public Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKeys) @@ -152,24 +135,52 @@ public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) { var content = Newtonsoft.Json.JsonConvert.SerializeObject(project); + var key = NostrPrivateKey.FromHex(hexPrivateKey); + + var signed = GetNip78NostrEvent(project, content) + .Sign(key); + + _nostrClient.Send(new NostrEventRequest(signed)); + + return Task.FromResult(signed.Id); + } + + private static NostrEvent GetNip78NostrEvent(ProjectInfo project, string content) + { var ev = new NostrEvent { Kind = NostrKind.ApplicationSpecificData, CreatedAt = DateTime.UtcNow, Content = content, Pubkey = project.NostrPubKey, - Tags = new NostrEventTags(//TODO need to find the correct tags for the event + Tags = new NostrEventTags( //TODO need to find the correct tags for the event new NostrEventTag("d", "AngorApp", "Create a new project event"), new NostrEventTag("L", "#projectInfo"), - new NostrEventTag("l", "ProjectDeclaration", "#projectInfo")) + new NostrEventTag("l", "ProjectDeclaration", "#projectInfo")) + }; + return ev; + } + + private static NostrEvent GetNip99NostrEvent(ProjectInfo project, string content) + { + var ev = new NostrEvent + { + Kind = (NostrKind)30402, + CreatedAt = DateTime.UtcNow, + Content = content, + Pubkey = project.NostrPubKey, + Tags = new NostrEventTags( //TODO need to find the correct tags for the event + new NostrEventTag("d", "AngorApp", "Create a new project event"), + new NostrEventTag("title", "New project :)"), + new NostrEventTag("published_at", DateTime.UtcNow.ToString()), + new NostrEventTag("t","#AngorProjectInfo"), + new NostrEventTag("image",""), + new NostrEventTag("summary","A new project that will save the world"), + new NostrEventTag("location",""), + new NostrEventTag("price","1","BTC")) }; - var key = NostrPrivateKey.FromHex(hexPrivateKey); - var signed = ev.Sign(key); - - _nostrClient.Send(new NostrEventRequest(signed)); - - return Task.FromResult(ev.Id); + return ev; } } From 55519cc793930f2899f9151c7e0a66e32426a553 Mon Sep 17 00:00:00 2001 From: TheDude Date: Mon, 23 Oct 2023 14:08:03 +0100 Subject: [PATCH 7/7] Refactoring and started work on nostr client factory --- src/Angor/Client/Pages/Browse.razor | 2 +- .../Services/INostrCommunicationFactory.cs | 91 ++++++++++++ src/Angor/Shared/Services/RelayService.cs | 133 ++++++++++-------- 3 files changed, 170 insertions(+), 56 deletions(-) create mode 100644 src/Angor/Shared/Services/INostrCommunicationFactory.cs diff --git a/src/Angor/Client/Pages/Browse.razor b/src/Angor/Client/Pages/Browse.razor index be9d5809..290b6be1 100644 --- a/src/Angor/Client/Pages/Browse.razor +++ b/src/Angor/Client/Pages/Browse.razor @@ -39,7 +39,7 @@ } else { - foreach (var project in projects.OrderByDescending(d => d.StartDate)) + foreach (var project in projects) //TODO set order (block created ?) {
diff --git a/src/Angor/Shared/Services/INostrCommunicationFactory.cs b/src/Angor/Shared/Services/INostrCommunicationFactory.cs new file mode 100644 index 00000000..ca035995 --- /dev/null +++ b/src/Angor/Shared/Services/INostrCommunicationFactory.cs @@ -0,0 +1,91 @@ +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; + } +} \ No newline at end of file diff --git a/src/Angor/Shared/Services/RelayService.cs b/src/Angor/Shared/Services/RelayService.cs index 72a50df6..edecdcc4 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -1,4 +1,5 @@ -using System.Reactive.Linq; +using System.Diagnostics; +using System.Reactive.Linq; using Angor.Shared.Models; using Nostr.Client.Requests; using Microsoft.Extensions.Logging; @@ -12,8 +13,8 @@ namespace Angor.Shared.Services { public class RelayService : IRelayService { - private static NostrWebsocketClient _nostrClient; - private static INostrCommunicator _nostrCommunicator; + private static NostrWebsocketClient? _nostrClient; + private static INostrCommunicator? _nostrCommunicator; private ILogger _logger; @@ -21,7 +22,7 @@ public class RelayService : IRelayService private ILogger _communicatorLogger; private Dictionary subscriptions = new(); - //private Dictionary> OkVerificationActions = new(); + private Dictionary> OkVerificationActions = new(); public RelayService( ILogger logger, @@ -35,64 +36,21 @@ public RelayService( public async Task ConnectToRelaysAsync() { - if (_nostrCommunicator != null) - return; - - _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")) - { - Name = "angor-relay.test", - ReconnectTimeout = null //TODO need to check what is the actual best time to set here - }; - - _nostrCommunicator.DisconnectionHappened.Subscribe(info => + if (_nostrCommunicator == null) { - 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); - - }); - - _nostrCommunicator.MessageReceived.Subscribe(info => - { - _communicatorLogger.LogInformation("message received on communicator - {Text} Relay message received, type: {MessageType}",info.Text, info.MessageType); - }); + SetupNostrCommunicator(); + } await _nostrCommunicator.StartOrFail(); - _nostrClient = new NostrWebsocketClient(_nostrCommunicator, _clientLogger); - - _nostrClient.Streams.UnknownMessageStream.Subscribe(_ => _clientLogger.LogInformation($"UnknownMessageStream {_}",_.MessageType)); - _nostrClient.Streams.EventStream.Subscribe(_ => _clientLogger.LogInformation($"EventStream {_.Subscription}", _.AdditionalData)); - _nostrClient.Streams.EoseStream.Subscribe(_ => _clientLogger.LogInformation($"EoseStream on subscription - {_.Subscription}", _.AdditionalData)); - _nostrClient.Streams.NoticeStream.Subscribe(_ => _clientLogger.LogInformation($"NoticeStream {_.Message}")); - _nostrClient.Streams.UnknownRawStream.Subscribe(_ => _clientLogger.LogInformation($"UnknownRawStream {_.Message}")); - _nostrClient.Streams.OkStream. Subscribe(_ => - { - _clientLogger.LogInformation($"OkStream {_.Accepted} message - {_.Message}"); - // if(OkVerificationSubscription.ContainsKey(_.EventId)) - // { - // OkVerificationActions[_.EventId](_); - // OkVerificationActions.Remove(_.EventId); - // } - }); - - _nostrClient.Streams.EoseStream.Subscribe(_ => - { - if (!subscriptions.ContainsKey(_.Subscription)) - return; - _clientLogger.LogInformation($"Disposing of subscription - {_.Subscription}"); - subscriptions[_.Subscription].Dispose(); - subscriptions.Remove(_.Subscription); - _clientLogger.LogInformation($"subscription disposed - {_.Subscription}"); - }); - - + if (_nostrClient != null) + return; + SetupNostrClient(); } public void RegisterOKMessageHandler(string eventId, Action action) { - //OkVerificationActions.Add(eventId,action); + OkVerificationActions.Add(eventId,action); } public Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKeys) @@ -140,8 +98,11 @@ public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) var signed = GetNip78NostrEvent(project, content) .Sign(key); + if (_nostrClient == null) + throw new InvalidOperationException(); + _nostrClient.Send(new NostrEventRequest(signed)); - + return Task.FromResult(signed.Id); } @@ -182,6 +143,68 @@ private static NostrEvent GetNip99NostrEvent(ProjectInfo project, string content return ev; } + + + private void SetupNostrClient() + { + _nostrClient = new NostrWebsocketClient(_nostrCommunicator, _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; + + _clientLogger.LogInformation($"Disposing of subscription - {_.Subscription}"); + subscriptions[_.Subscription].Dispose(); + subscriptions.Remove(_.Subscription); + _clientLogger.LogInformation($"subscription disposed - {_.Subscription}"); + }); + } + + private void SetupNostrCommunicator() + { + _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")) + { + Name = "angor-relay.test", + ReconnectTimeout = null //TODO need to check what is the actual best time to set here + }; + + _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); + }); + + _nostrCommunicator.MessageReceived.Subscribe(info => + { + _communicatorLogger.LogInformation( + "message received on communicator - {Text} Relay message received, type: {MessageType}", + info.Text, info.MessageType); + }); + } } }