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 83fdd505..290b6be1 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 -@inject HttpClient Http -@inject IClientStorage storage; +@using Angor.Shared.Services +@using Nostr.Client.Keys +@inject ISessionStorage SessionStorage; @inject NavigationManager NavigationManager @inject IRelayService _RelayService @inject IIndexerService _IndexerService @@ -13,30 +13,46 @@

Browse Projects

+ +
- +

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

No projects found.

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

No projects found.

+ } } else { - foreach (var project in projects.OrderByDescending(d => d.StartDate)) + foreach (var project in projects) //TODO set order (block created ?) {
@project.ProjectIdentifier
-

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

- +

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

+ @if (SessionStorage.IsProjectInStorageById(project.ProjectIdentifier)) + { + + } + else + { +
+ }
} @@ -46,34 +62,61 @@
@code { + NotificationComponent notificationComponent; private string searchQuery; + bool searchInProgress = false; - private List projects= new(); + private List projects= new(); protected override async Task OnInitializedAsync() { - + await _RelayService.ConnectToRelaysAsync(); } 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) { - var project = await _RelayService.GetProjectAsync(blockchainProject.ProjectIdentifier); - - if (project != null) + if (projects.All(_ => _.ProjectIdentifier != blockchainProject.ProjectIdentifier)) { - projects.Add(project); + projects.Add(blockchainProject); } } + + searchInProgress = false; + StateHasChanged(); } - private void ImportProject(ProjectInfo project) + private void ImportProject(string projectIdentifier) { - storage.AddBrowseProject(project); - - NavigationManager.NavigateTo($"/view/{project.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 0d0c2735..38d7e34f 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -2,16 +2,14 @@ @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 +@using Angor.Shared.Services @using Blockcore.NBitcoin @using Blockcore.NBitcoin.DataEncoders @inherits BaseComponent - -@inject HttpClient Http @inject IDerivationOperations _derivationOperations @inject IWalletStorage _walletStorage; @inject IClientStorage storage; @@ -19,7 +17,6 @@ @inject IWalletOperations _WalletOperations @inject INetworkConfiguration _NetworkConfiguration @inject IRelayService _RelayService -@inject IIndexerService _IndexerService @inject ISignService _SignService @@ -178,7 +175,7 @@ } }; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { hasWallet = _walletStorage.HasWallet(); @@ -193,9 +190,10 @@ project.FounderKey = projectsKeys.FounderKey; project.FounderRecoveryKey = projectsKeys.FounderRecoveryKey; project.ProjectIdentifier = projectsKeys.ProjectIdentifier; + project.NostrPubKey = projectsKeys.NostrPubKey; } - return Task.CompletedTask; + await _RelayService.ConnectToRelaysAsync(); } private async Task CreatProject() @@ -236,7 +234,7 @@ feeData.FeeEstimations.Fees.AddRange(fetchFees); feeData.SelectedFeeEstimation = feeData.FeeEstimations.Fees.First(); - unsignedTransaction = _founderTransactionActions.CreateNewProjectTransaction(project.FounderKey, _derivationOperations.AngorKeyToScript(project.ProjectIdentifier), 10000); + unsignedTransaction = _founderTransactionActions.CreateNewProjectTransaction(project.FounderKey, _derivationOperations.AngorKeyToScript(project.ProjectIdentifier), 10000, project.NostrPubKey); signedTransaction = _WalletOperations.AddInputsAndSignTransaction(accountInfo.GetNextChangeReceiveAddress(), unsignedTransaction, _walletStorage.GetWallet(), accountInfo, feeData.SelectedFeeEstimation); @@ -289,9 +287,21 @@ if (!response.Success) return response; - await _RelayService.AddProjectAsync(project); + storage.AddFounderProject(project); + + var nostrKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); + + var resultId = 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 + _RelayService.RegisterOKMessageHandler(resultId, _ => + { + 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 var key = _derivationOperations.DeriveFounderRecoveryPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); @@ -304,7 +314,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/Program.cs b/src/Angor/Client/Program.cs index b01b1d1b..c5370dd0 100644 --- a/src/Angor/Client/Program.cs +++ b/src/Angor/Client/Program.cs @@ -6,7 +6,9 @@ using Angor.Shared.ProtocolNew; using Angor.Shared.ProtocolNew.Scripts; 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; @@ -17,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 new file mode 100644 index 00000000..7a3c44d2 --- /dev/null +++ b/src/Angor/Client/Storage/LocalSessionStorage.cs @@ -0,0 +1,68 @@ +using Angor.Client.Services; +using Angor.Shared.Models; +using Blazored.SessionStorage; + +namespace Angor.Client.Storage; + +public class LocalSessionStorage : ISessionStorage +{ + private ISyncSessionStorageService _sessionStorageService; + + private const string NostrKeyEventStreamSubscription = "subscriptions"; + + public LocalSessionStorage(ISyncSessionStorageService sessionStorageService) + { + _sessionStorageService = sessionStorageService; + } + + public void StoreProjectInfo(ProjectInfo project) + { + _sessionStorageService.SetItem(project.ProjectIdentifier,project); + } + + public void AddProjectToSubscribedList(string nostrPubKey) + { + var list = _sessionStorageService.GetItem>(NostrKeyEventStreamSubscription) ?? new List(); + + list.Add(nostrPubKey); + + _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>(NostrKeyEventStreamSubscription) ?? new List(); + + return list.Contains(nostrPubKey); + } + + public ProjectInfo? GetProjectById(string projectId) + { + 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/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/DerivationOperations.cs b/src/Angor/Shared/DerivationOperations.cs index 686cde27..8ea82267 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 = DeriveNostrPubKey(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 DeriveNostrPubKey(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) { @@ -264,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/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/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/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..6710be11 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 NBitcoin.PubKey(nostrPuKey).GetTaprootFullPubKey().ToBytes())); } public Script BuildSeederInfoScript(string investorKey, uint256 secretHash) 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/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/ISessionStorage.cs b/src/Angor/Shared/Services/ISessionStorage.cs new file mode 100644 index 00000000..e60989c0 --- /dev/null +++ b/src/Angor/Shared/Services/ISessionStorage.cs @@ -0,0 +1,16 @@ +using Angor.Shared.Models; + +namespace Angor.Client.Services; + +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/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 1879608d..edecdcc4 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -1,48 +1,210 @@ -using System.Net.Http.Json; +using System.Diagnostics; +using System.Reactive.Linq; using Angor.Shared.Models; +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.Client.Services +namespace Angor.Shared.Services { - public interface IRelayService - { - Task> GetProjectsAsync(); - Task AddProjectAsync(ProjectInfo project); - Task GetProjectAsync(string projectId); - } - public class RelayService : IRelayService { + private static NostrWebsocketClient? _nostrClient; + private static INostrCommunicator? _nostrCommunicator; + + private ILogger _logger; + + private ILogger _clientLogger; + private ILogger _communicatorLogger; + + private Dictionary subscriptions = new(); + private Dictionary> OkVerificationActions = new(); + + public RelayService( + ILogger logger, + ILogger clientLogger, + ILogger communicatorLogger) + { + _logger = logger; + _clientLogger = clientLogger; + _communicatorLogger = communicatorLogger; + } + + public async Task ConnectToRelaysAsync() + { + if (_nostrCommunicator == null) + { + SetupNostrCommunicator(); + } + + await _nostrCommunicator.StartOrFail(); - private readonly HttpClient _httpClient; - private readonly string _baseUrl = "/api/Test"; // "https://your-base-url/api/test"; + if (_nostrClient != null) + return; + SetupNostrClient(); + } + + public void RegisterOKMessageHandler(string eventId, Action action) + { + OkVerificationActions.Add(eventId,action); + } - public RelayService(HttpClient httpClient) + public Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKeys) { - _httpClient = httpClient; + string subscriptionName = "ProjectInfoLookups"; + _nostrClient.Send(new NostrRequest(subscriptionName, new NostrFilter + { + Authors = nostrPubKeys, + Kinds = new[] { NostrKind.ApplicationSpecificData, NostrKind.Metadata, (NostrKind)30402 }, + })); + + 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); + } + + return Task.CompletedTask; } - public async Task> GetProjectsAsync() + public void CloseConnection() { - var response = await _httpClient.GetAsync($"{_baseUrl}"); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadFromJsonAsync>(); + foreach (var subscription in subscriptions.Values) + { + subscription.Dispose(); + } + _nostrClient?.Dispose(); + _nostrCommunicator?.Dispose(); } - public async Task AddProjectAsync(ProjectInfo project) + public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) { - var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}", project); - response.EnsureSuccessStatusCode(); + var content = Newtonsoft.Json.JsonConvert.SerializeObject(project); + + var key = NostrPrivateKey.FromHex(hexPrivateKey); + + var signed = GetNip78NostrEvent(project, content) + .Sign(key); + + if (_nostrClient == null) + throw new InvalidOperationException(); + + _nostrClient.Send(new NostrEventRequest(signed)); + + return Task.FromResult(signed.Id); } - public async Task GetProjectAsync(string projectId) + 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 + new NostrEventTag("d", "AngorApp", "Create a new project event"), + new NostrEventTag("L", "#projectInfo"), + new NostrEventTag("l", "ProjectDeclaration", "#projectInfo")) + }; + return ev; + } + + private static NostrEvent GetNip99NostrEvent(ProjectInfo project, string content) { - var response = await _httpClient.GetAsync($"{_baseUrl}/project/{projectId}"); + 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")) + }; + + 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}"); - return response.IsSuccessStatusCode - ? await response.Content.ReadFromJsonAsync() - : null; + 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); + }); + } } } 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()); } } }