diff --git a/src/Angor/Client/Models/FounderProject.cs b/src/Angor/Client/Models/FounderProject.cs index a755b6d2..f5a7d7f1 100644 --- a/src/Angor/Client/Models/FounderProject.cs +++ b/src/Angor/Client/Models/FounderProject.cs @@ -7,6 +7,8 @@ public class FounderProject : Project public int ProjectIndex { get; set; } public DateTime? LastRequestForSignaturesTime { get; set; } + public string ProjectInfoEventId { get; set; } + public bool NostrMetadataCreated() { return !string.IsNullOrEmpty(Metadata?.Name); @@ -14,6 +16,6 @@ public bool NostrMetadataCreated() public bool NostrApplicationSpecificDataCreated() { - return ProjectInfo?.Stages.Any() ?? false; + return !string.IsNullOrEmpty(ProjectInfoEventId); } } \ No newline at end of file diff --git a/src/Angor/Client/Models/InvestmentState.cs b/src/Angor/Client/Models/InvestmentState.cs index 3b74f543..311ffb1c 100644 --- a/src/Angor/Client/Models/InvestmentState.cs +++ b/src/Angor/Client/Models/InvestmentState.cs @@ -4,4 +4,5 @@ public class InvestmentState { public string ProjectIdentifier { get; set; } public string InvestmentTransactionHash { get; set; } + public string investorPubKey { get; set; } } \ No newline at end of file diff --git a/src/Angor/Client/Models/InvestorProject.cs b/src/Angor/Client/Models/InvestorProject.cs index 92a09823..590e14c7 100644 --- a/src/Angor/Client/Models/InvestorProject.cs +++ b/src/Angor/Client/Models/InvestorProject.cs @@ -15,6 +15,9 @@ public class InvestorProject : Project public long? AmountInvested { get; set; } public long? AmountInRecovery { get; set; } + public string InvestorPublicKey { get; set; } + public string InvestorNPub { get; set; } + public bool WaitingForFounderResponse() { return ReceivedFounderSignatures() == false && SignaturesInfo?.TimeOfSignatureRequest != null; diff --git a/src/Angor/Client/Models/Project.cs b/src/Angor/Client/Models/Project.cs index dcc6d5f2..446da2e1 100644 --- a/src/Angor/Client/Models/Project.cs +++ b/src/Angor/Client/Models/Project.cs @@ -1,25 +1,9 @@ using Angor.Shared.Models; -using Angor.Shared.Services; namespace Angor.Client.Models; public class Project { - public Project() - { } - - public Project(ProjectIndexerData indexerData) - { - ProjectInfo = new ProjectInfo - { - ProjectIdentifier = indexerData.ProjectIdentifier, - FounderKey = indexerData.FounderKey, - NostrPubKey = indexerData.NostrPubKey, - }; - - CreationTransactionId = indexerData.TrxId; - } - public ProjectMetadata? Metadata { get; set; } public ProjectInfo ProjectInfo { get; set; } diff --git a/src/Angor/Client/NetworkConfiguration.cs b/src/Angor/Client/NetworkConfiguration.cs index 75155f22..71dafd69 100644 --- a/src/Angor/Client/NetworkConfiguration.cs +++ b/src/Angor/Client/NetworkConfiguration.cs @@ -1,7 +1,5 @@ -using System.Reflection.Metadata.Ecma335; using Angor.Shared; using Angor.Shared.Models; -using Angor.Shared.Networks; using Blockcore.Networks; namespace Angor.Client; @@ -11,8 +9,9 @@ public class NetworkConfiguration : INetworkConfiguration public static string AngorTestKey = "tpubD8JfN1evVWPoJmLgVg6Usq2HEW9tLqm6CyECAADnH5tyQosrL6NuhpL9X1cQCbSmndVrgLSGGdbRqLfUbE6cRqUbrHtDJgSyQEY2Uu7WwTL"; public static string AngorMainKey = "xpub661MyMwAqRbcGNxKe9aFkPisf3h32gHLJm8f9XAqx8FB1Nk6KngCY8hkhGqxFr2Gyb6yfUaQVbodxLoC1f3K5HU9LM1CXE59gkEXSGCCZ1B"; - public static long AngorCreateFeeSats = 10000; + public static long AngorCreateFeeSats = 10001; // versioning :) public static int AngorInvestFeePercentage = 1; + public static short NostrEventIdKeyType = 1; //TODO David use an enum for this? public int GetAngorInvestFeePercentage => AngorInvestFeePercentage; diff --git a/src/Angor/Client/Pages/Browse.razor b/src/Angor/Client/Pages/Browse.razor index 9f32d7ce..607e84f8 100644 --- a/src/Angor/Client/Pages/Browse.razor +++ b/src/Angor/Client/Pages/Browse.razor @@ -380,7 +380,34 @@ else if (findProject != null) { - _RelayService.RequestProjectCreateEventsByPubKey(HandleProjectEvents(), StateHasChanged, new[] { findProject.NostrPubKey }); + + var projectNpub = string.Empty; + _RelayService.LookupProjectsInfoByEventIds(p => + { + if (SessionStorage.IsProjectInStorageById(p.ProjectIdentifier)) + return; + SessionStorage.StoreProject(new Project { ProjectInfo = p, CreationTransactionId = findProject.TrxId }); + projectNpub = p.NostrPubKey; + }, + () => + { + _RelayService.LookupNostrProfileForNPub((npub, nostrMetadata) => + { + var project = SessionStorage.GetProjectById(findProject.ProjectIdentifier); + + if (project!.Metadata != null) + return; + + project.Metadata = nostrMetadata; + SessionStorage.StoreProject(project); + }, () => + { + searchInProgress = false; + StateHasChanged(); + }, + projectNpub); + }, + findProject.NostrEventId); projects = new List { findProject }; } else @@ -412,80 +439,54 @@ else SessionStorage.SetProjectIndexerData(projects); var projectsForLookup = projectsNotInList - .Where(_ => _.NostrPubKey != null) - .Select(_ => _.NostrPubKey) + .Where(_ => !string.IsNullOrEmpty(_.NostrEventId)) + .Select(_ => _.NostrEventId) .ToArray(); nostrSearchInProgress = true; if (projectsForLookup.Any()) - _RelayService.RequestProjectCreateEventsByPubKey(HandleProjectEvents(), () => - { - nostrSearchInProgress = false; - StateHasChanged(); - }, projectsForLookup); - - StateHasChanged(); - } + { + var projectNpubsForLookup = new Dictionary(); - searchInProgress = false; - } + _RelayService.LookupProjectsInfoByEventIds(x => + { + if (SessionStorage.IsProjectInStorageById(x.ProjectIdentifier)) + return; - private Action HandleProjectEvents() - { - return e => - { - var projectIndexerData = projects.FirstOrDefault(x => x.NostrPubKey == e.Pubkey); + var projectIndexerData = projectsNotInList.First(p => p.ProjectIdentifier == x.ProjectIdentifier); - if (projectIndexerData == null && findProject?.NostrPubKey == e.Pubkey) - { - projectIndexerData = findProject; - } + SessionStorage.StoreProject(new Project { ProjectInfo = x, CreationTransactionId = projectIndexerData.TrxId }); - switch (e) - { - case { Kind: NostrKind.Metadata }: + projectNpubsForLookup.Add(x.NostrPubKey, projectIndexerData.ProjectIdentifier); - try - { - var nostrMetadata = serializer.Deserialize(e.Content); - if (projectIndexerData != null) - { - var project = SessionStorage.GetProjectById(projectIndexerData.ProjectIdentifier); - if (project != null) + }, () => + _RelayService.LookupNostrProfileForNPub((npub, nostrMetadata) => { - project.Metadata = nostrMetadata; - SessionStorage.StoreProject(project); - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, $"error parsing the result of kind {NostrKind.Metadata} from relay, ProjectIdentifier = {projectIndexerData?.ProjectIdentifier}"); - } + var project = SessionStorage.GetProjectById(projectNpubsForLookup[npub]); + if (project == null) + { + throw new ArgumentOutOfRangeException("Unable to find the project in storage for requested metadata " + npub); + } - break; - case { Kind: NostrKind.ApplicationSpecificData }: + if (project.Metadata != null) + return; - try - { - var projectInfo = serializer.Deserialize(e.Content); - if (projectInfo != null && projectIndexerData != null) - { - if (!SessionStorage.IsProjectInStorageById(projectInfo.ProjectIdentifier)) + project.Metadata = nostrMetadata; + SessionStorage.StoreProject(project); + }, () => { - SessionStorage.StoreProject(new Project { ProjectInfo = projectInfo, CreationTransactionId = projectIndexerData.TrxId }); - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, $"error parsing the result of kind {NostrKind.ApplicationSpecificData} from relay, ProjectIdentifier = {projectIndexerData?.ProjectIdentifier}"); - } - - break; + nostrSearchInProgress = false; + StateHasChanged(); + }, + projectNpubsForLookup.Keys.ToArray()), + projectsForLookup); } - }; + + StateHasChanged(); + } + + searchInProgress = false; } private void ViewProjectDetails(string projectIdentifier) diff --git a/src/Angor/Client/Pages/Create.razor b/src/Angor/Client/Pages/Create.razor index d05ce7e1..cefe1947 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -735,6 +735,7 @@ if (nostrApplicationSpecificDataCreated == false) { nostrApplicationSpecificDataCreated = true; + project.ProjectInfoEventId = _.EventId; storage.UpdateFounderProject(project); } @@ -792,7 +793,7 @@ feeData.FeeEstimations.Fees.AddRange(fetchFees); feeData.SelectedFeeEstimation = feeData.FeeEstimations.Fees.First(); - unsignedTransaction = _founderTransactionActions.CreateNewProjectTransaction(project.ProjectInfo.FounderKey, _derivationOperations.AngorKeyToScript(project.ProjectInfo.ProjectIdentifier), NetworkConfiguration.AngorCreateFeeSats, project.ProjectInfo.NostrPubKey); + unsignedTransaction = _founderTransactionActions.CreateNewProjectTransaction(project.ProjectInfo.FounderKey, _derivationOperations.AngorKeyToScript(project.ProjectInfo.ProjectIdentifier), NetworkConfiguration.AngorCreateFeeSats, NetworkConfiguration.NostrEventIdKeyType, project.ProjectInfoEventId); signedTransaction = _WalletOperations.AddInputsAndSignTransaction(accountInfo.GetNextChangeReceiveAddress(), unsignedTransaction, words, accountInfo, feeData.SelectedFeeEstimation); diff --git a/src/Angor/Client/Pages/Invest.razor b/src/Angor/Client/Pages/Invest.razor index 15f3c501..c49f9dec 100644 --- a/src/Angor/Client/Pages/Invest.razor +++ b/src/Angor/Client/Pages/Invest.razor @@ -372,7 +372,8 @@ else private bool showCreateModal; TransactionInfo? signedTransaction; Transaction unSignedTransaction; - + string InvestorPubKey; + private FeeData feeData = new(); @@ -465,9 +466,11 @@ else var nostrPrivateKeyHex = Encoders.Hex.EncodeData(nostrPrivateKey.ToBytes()); _SignService.LookupSignatureForInvestmentRequest( - NostrPrivateKey.FromHex(nostrPrivateKeyHex).DerivePublicKey().Hex - , project.ProjectInfo.NostrPubKey, investmentProject.SignaturesInfo!.TimeOfSignatureRequest!.Value, investmentProject.SignaturesInfo!.SignatureRequestEventId!, - async _ => await HandleSignatureReceivedAsync(nostrPrivateKeyHex, _)); + investmentProject.InvestorNPub, + project.ProjectInfo.NostrPubKey, + investmentProject.SignaturesInfo!.TimeOfSignatureRequest!.Value, + investmentProject.SignaturesInfo!.SignatureRequestEventId!, + signatures => HandleSignatureReceivedAsync(nostrPrivateKeyHex, signatures)); } catch (Exception e) { @@ -589,14 +592,14 @@ else var words = await passwordComponent.GetWalletAsync(); - var investorKey = _derivationOperations.DeriveInvestorKey(words, project.ProjectInfo.FounderKey); + InvestorPubKey = _derivationOperations.DeriveInvestorKey(words, project.ProjectInfo.FounderKey); if (Investment.IsSeeder) { var seederHash = _derivationOperations.DeriveSeederSecretHash(words, project.ProjectInfo.FounderKey); } - unSignedTransaction = _InvestorTransactionActions.CreateInvestmentTransaction(project.ProjectInfo, investorKey, Money.Coins(Investment.InvestmentAmount).Satoshi); + unSignedTransaction = _InvestorTransactionActions.CreateInvestmentTransaction(project.ProjectInfo, InvestorPubKey, Money.Coins(Investment.InvestmentAmount).Satoshi); signedTransaction = _WalletOperations.AddInputsAndSignTransaction(accountInfo.GetNextChangeReceiveAddress(), unSignedTransaction, words, accountInfo, feeData.SelectedFeeEstimation); @@ -639,7 +642,7 @@ else var accountInfo = storage.GetAccountInfo(network.Name); signedTransaction = _WalletOperations.AddInputsAndSignTransaction(accountInfo.GetNextChangeReceiveAddress(), unSignedTransaction, words, accountInfo, feeData.SelectedFeeEstimation); - + StateHasChanged(); } } @@ -670,7 +673,8 @@ else Metadata = project.Metadata, SignedTransactionHex = signedTransaction!.Transaction!.ToHex(), CreationTransactionId = project.CreationTransactionId, - AmountInvested = new Money(Investment.InvestmentAmount, MoneyUnit.BTC).Satoshi + AmountInvested = new Money(Investment.InvestmentAmount, MoneyUnit.BTC).Satoshi, + InvestorPublicKey = InvestorPubKey ?? throw new ArgumentNullException("The investor pub key is not populated") }; var investorProject = (InvestorProject)project; @@ -706,11 +710,9 @@ else investorProject.SignaturesInfo!.TimeOfSignatureRequest = investmentSigsRequest.eventTime; investorProject.SignaturesInfo!.SignatureRequestEventId = investmentSigsRequest.eventId; - + investorProject.InvestorNPub = NostrPrivateKey.FromHex(nostrPrivateKeyHex).DerivePublicKey().Hex; + storage.AddInvestmentProject(investorProject); - storage.SetNostrPublicKeyPerProject(project.ProjectInfo.ProjectIdentifier, nostrPrivateKey.PubKey.ToHex()[2..]); - - foreach (var input in strippedInvestmentTransaction.Inputs) accountInfo.UtxoReservedForInvestment.Add(input.PrevOut.ToString()); @@ -718,11 +720,11 @@ else storage.SetAccountInfo(network.Name, accountInfo); _SignService.LookupSignatureForInvestmentRequest( - NostrPrivateKey.FromHex(nostrPrivateKeyHex).DerivePublicKey().Hex, + investorProject.InvestorNPub, investorProject.ProjectInfo.NostrPubKey, investorProject.SignaturesInfo.TimeOfSignatureRequest.Value, investorProject.SignaturesInfo.SignatureRequestEventId, - async _ => await HandleSignatureReceivedAsync(nostrPrivateKeyHex, _)); + signatures => HandleSignatureReceivedAsync(nostrPrivateKeyHex, signatures)); notificationComponent.ShowNotificationMessage("Signature request sent", 5); } @@ -869,7 +871,7 @@ else { ProjectIdentifiers = storage.GetInvestmentProjects() .Where(x => x.InvestedInProject()) - .Select(x => new InvestmentState { ProjectIdentifier = x.ProjectInfo.ProjectIdentifier }) + .Select(x => new InvestmentState { ProjectIdentifier = x.ProjectInfo.ProjectIdentifier, investorPubKey = x.InvestorPublicKey, InvestmentTransactionHash = x.TransactionId}) .ToList() }; diff --git a/src/Angor/Client/Pages/Investor.razor b/src/Angor/Client/Pages/Investor.razor index d03d5e66..c73bba26 100644 --- a/src/Angor/Client/Pages/Investor.razor +++ b/src/Angor/Client/Pages/Investor.razor @@ -287,8 +287,9 @@ TotalWallet = abi.TotalBalance; TotalInRecovery = projects.Sum(s => s.AmountInRecovery ?? 0); - await RefreshBalance(); - await checkSignatureFromFounder(); + var refreshTask = RefreshBalance(); + CheckSignatureFromFounder(); + await refreshTask; } } @@ -333,33 +334,30 @@ } - private async Task HandleSignatureReceivedAsync(string nostrPubKey, string signatureContent) + private Task HandleSignatureReceivedAsync(string nostrPubKey, string signatureContent) { if (investmentRequestsMap.ContainsKey(nostrPubKey)) { investmentRequestsMap[nostrPubKey] = true; StateHasChanged(); } + + return Task.CompletedTask; } - private async Task checkSignatureFromFounder() + private void CheckSignatureFromFounder() { foreach (var project in projects) { investmentRequestsMap[project.ProjectInfo.NostrPubKey] = false; - var investorNostrPubKey = storage.GetNostrPublicKeyPerProject(project.ProjectInfo.ProjectIdentifier); - - if (!string.IsNullOrEmpty(investorNostrPubKey)) - { - _SignService.LookupSignatureForInvestmentRequest( - investorNostrPubKey, - project.ProjectInfo.NostrPubKey, - project.SignaturesInfo.TimeOfSignatureRequest.Value, - project.SignaturesInfo.SignatureRequestEventId, - async signatureContent => await HandleSignatureReceivedAsync(project.ProjectInfo.NostrPubKey, signatureContent) - ); - } + _SignService.LookupSignatureForInvestmentRequest( + project.InvestorNPub, + project.ProjectInfo.NostrPubKey, + project.SignaturesInfo.TimeOfSignatureRequest.Value, + project.SignaturesInfo.SignatureRequestEventId, + signatureContent => HandleSignatureReceivedAsync(project.ProjectInfo.NostrPubKey, signatureContent) + ); } } @@ -402,7 +400,7 @@ var NostrDMPrivateKeyHex = Encoders.Hex.EncodeData(NostrDMPrivateKey.ToBytes()); var NostrDMPubkey = _DerivationOperations.DeriveNostrPubKey(words, 1); - await checkSignatureFromFounder(); + CheckSignatureFromFounder(); var rootNostrPubKeyHex = _DerivationOperations.DeriveNostrPubKey(words, 0); @@ -442,95 +440,122 @@ }, rootNostrPubKeyHex); } + + //TODO David check if we should replace the logic to get all projects first and then get signatures for them? + private void FetchProjectsData(params string[] eventIds) + { + _RelayService.LookupProjectsInfoByEventIds(x => + { + var projectInfo = serializer.Deserialize(x.Content!) ?? + throw new Exception("The project info must be in the application specific data event"); + + if (projects.Any(x => x.ProjectInfo.ProjectIdentifier == projectInfo.ProjectIdentifier)) + return; + + projects.Add(new InvestorProject { ProjectInfo = projectInfo }); + }, + () => + { + _RelayService.LookupNostrProfileForNPub( + (projectNpub, metadata) => + { + var project = projects.FirstOrDefault(x => x.ProjectInfo.NostrPubKey == projectNpub); + if (project is { Metadata: null }) { project.Metadata = metadata; } + }, + () => + { + if (eventIds.Length != projects.Count) + { + notificationComponent.ShowErrorMessage("Unable to pull the information for all projects invested (try adding relays)"); + } + StateHasChanged(); + }, + projects.Select(x => x.ProjectInfo.NostrPubKey).ToArray()); + },eventIds); + } + private async Task GetInvestmentProjectDataAsync(InvestmentState investmentState) { - var project = await _IndexerService.GetProjectByIdAsync(investmentState.ProjectIdentifier); + var projectIndexerData = await _IndexerService.GetProjectByIdAsync(investmentState.ProjectIdentifier); - if (project == null) + if (projectIndexerData == null) return; var words = await passwordComponent.GetWalletAsync(); - var investmentPubKey = _DerivationOperations.DeriveInvestorKey(words, project.FounderKey); - var investments = await _IndexerService.GetInvestmentsAsync(investmentState.ProjectIdentifier); - var investment = investments.SingleOrDefault(x => x.InvestorPublicKey == investmentPubKey); + var investment = await _IndexerService.GetInvestmentAsync(investmentState.ProjectIdentifier, investmentState.investorPubKey); if (investment == null) return; - var investorNostrPrivateKey = _DerivationOperations.DeriveProjectNostrInvestorPrivateKey(words, project.ProjectIdentifier); - var investorNostrPubKey = investorNostrPrivateKey.PubKey.ToHex()[2..]; + var investorNostrPrivateKey = _DerivationOperations.DeriveProjectNostrInvestorPrivateKey(words, projectIndexerData.ProjectIdentifier); var investorProject = new InvestorProject - { - TransactionId = investment.TransactionId, - AmountInvested = investment.TotalAmount, - }; + { + TransactionId = investment.TransactionId, + AmountInvested = investment.TotalAmount, + InvestorNPub = investorNostrPrivateKey.PubKey.ToHex()[2..] + }; DateTime? createdAt = null; string? eventId = null; - await _SignService.LookupInvestmentRequestsAsync(project.NostrPubKey, null, - (id, publisherPubKey, content, eventTime) => + _RelayService.LookupProjectsInfoByEventIds( + x => { - if ((createdAt == null || createdAt < eventTime) && publisherPubKey == investorNostrPubKey) + if (investorProject.ProjectInfo == null) { - createdAt = eventTime; - eventId = id; + investorProject.ProjectInfo = serializer.Deserialize(x.Content!) ?? + throw new Exception("The project info must be in the application specific data event"); } - }, () => + }, + () => { - _RelayService.RequestProjectCreateEventsByPubKey(x => - { - switch (x.Kind) - { - case NostrKind.Metadata: - investorProject.Metadata ??= - serializer.Deserialize(x.Content!); - break; - case NostrKind.ApplicationSpecificData: - investorProject.ProjectInfo ??= serializer.Deserialize(x.Content!) ?? - throw new Exception("The project info must be in the application specific data event"); - break; - default: - throw new ArgumentOutOfRangeException($"{x.Kind}"); - } - }, () => + _RelayService.LookupNostrProfileForNPub( + (_, metadata) => { investorProject.Metadata ??= metadata; }, + () => { - _SignService.LookupSignatureForInvestmentRequest(investorNostrPubKey, project.NostrPubKey, - createdAt!.Value, eventId!, async encryptedSignatures => + _SignService.LookupInvestmentRequestsAsync(investorProject.ProjectInfo.NostrPubKey, investorProject.InvestorNPub, null, + (id, publisherPubKey, content, eventTime) => { - //TODO decrypt the signatures and add to project - - if (investorProject.ReceivedFounderSignatures()) //multiple relays for the same message - return; - - var signatureJson = await _encryptionService.DecryptNostrContentAsync( - Encoders.Hex.EncodeData(investorNostrPrivateKey.ToBytes()), project.NostrPubKey, encryptedSignatures); - - var res = serializer.Deserialize(signatureJson); - - if (res.ProjectIdentifier == investorProject.ProjectInfo.ProjectIdentifier) + if (createdAt == null || createdAt < eventTime) { - investorProject.SignaturesInfo = res; + createdAt = eventTime; + eventId = id; } + }, () => + { + _SignService.LookupSignatureForInvestmentRequest(investorProject.InvestorNPub, investorProject.ProjectInfo.NostrPubKey, + createdAt!.Value, eventId!, + async encryptedSignatures => + { + if (investorProject.ReceivedFounderSignatures()) //multiple relays for the same message + return; - if (projects.All(x => x.ProjectInfo.ProjectIdentifier != investorProject.ProjectInfo.ProjectIdentifier)) - { - projects.Add(investorProject); - storage.AddInvestmentProject(investorProject); + var signatureJson = await _encryptionService.DecryptNostrContentAsync( + Encoders.Hex.EncodeData(investorNostrPrivateKey.ToBytes()), investorProject.ProjectInfo.NostrPubKey, encryptedSignatures); - // todo: David to check this, not sure this is correct. - // storage.SetNostrPublicKeyPerProject(investorProject.ProjectInfo.ProjectIdentifier, investorNostrPrivateKey.PubKey.ToHex()[2..]); + var res = serializer.Deserialize(signatureJson); - RefreshBalanceTriggered = false; - StateHasChanged(); - } - }); - }, - project.NostrPubKey); - }); + if (res.ProjectIdentifier == investorProject.ProjectInfo.ProjectIdentifier) + { + investorProject.SignaturesInfo = res; + } + if (projects.All(x => x.ProjectInfo.ProjectIdentifier != investorProject.ProjectInfo.ProjectIdentifier)) + { + projects.Add(investorProject); + storage.AddInvestmentProject(investorProject); + + RefreshBalanceTriggered = false; + StateHasChanged(); + } + }); + }).GetAwaiter().GetResult(); + }, + investorProject.ProjectInfo.NostrPubKey); + }, + projectIndexerData.NostrEventId); } private void NavigateToPenalties() diff --git a/src/Angor/Client/Pages/Signatures.razor b/src/Angor/Client/Pages/Signatures.razor index bd3eb20a..932a978a 100644 --- a/src/Angor/Client/Pages/Signatures.razor +++ b/src/Angor/Client/Pages/Signatures.razor @@ -313,33 +313,31 @@ private async Task FetchPendingSignatures(FounderProject project) { - await SignService.LookupInvestmentRequestsAsync(project.ProjectInfo.NostrPubKey, null,// project.LastRequestForSignaturesTime , async + await SignService.LookupInvestmentRequestsAsync(project.ProjectInfo.NostrPubKey, null, null,// project.LastRequestForSignaturesTime , async (eventId, investorNostrPubKey, encryptedMessage, timeArrived) => - { - Logger.LogDebug($"Sig request event received investorNostrPubKey: {investorNostrPubKey} - timeArrived: {timeArrived}"); + { + Logger.LogDebug($"Sig request event received investorNostrPubKey: {investorNostrPubKey} - timeArrived: {timeArrived}"); - var sigReq = signaturesRequests.FirstOrDefault(_ => _.investorNostrPubKey == investorNostrPubKey); + var sigReq = signaturesRequests.FirstOrDefault(_ => _.investorNostrPubKey == investorNostrPubKey); - if (sigReq != null) - { - if (sigReq.TimeArrived < timeArrived) + if (sigReq != null) { + if (sigReq.TimeArrived >= timeArrived) + { + return; //multiple relays could mean the same massage multiple times + } + Logger.LogDebug($"Sig request event received is replaced"); // this is a newer sig request so replace it signaturesRequests.Remove(sigReq); } - else - { - return; //multiple relays could mean the same massage multiple times - } - } - Logger.LogDebug($"Sig request event received is new"); + Logger.LogDebug($"Sig request event received is new"); - messagesReceived = true; + messagesReceived = true; - var signatureRequest = new SignatureRequest + var signatureRequest = new SignatureRequest { investorNostrPubKey = investorNostrPubKey, TimeArrived = timeArrived, @@ -347,9 +345,9 @@ EventId = eventId }; - signaturesRequests.Add(signatureRequest); - Logger.LogDebug($"Added to pendingSignatures"); - }, + signaturesRequests.Add(signatureRequest); + Logger.LogDebug($"Added to pendingSignatures"); + }, () => { Logger.LogDebug($"End of messages"); diff --git a/src/Angor/Client/Pages/View.razor b/src/Angor/Client/Pages/View.razor index d04a3195..c4b79c36 100644 --- a/src/Angor/Client/Pages/View.razor +++ b/src/Angor/Client/Pages/View.razor @@ -495,37 +495,36 @@ if (projectIndexerData != null) { project = new Project { CreationTransactionId = projectIndexerData.TrxId }; - _RelayService.RequestProjectCreateEventsByPubKey(e => - { - if (project != null) - { - switch (e) - { - case { Kind: NostrKind.Metadata }: - var nostrMetadata = serializer.Deserialize(e.Content); - project.Metadata ??= nostrMetadata; - break; - case { Kind: NostrKind.ApplicationSpecificData }: - var projectInfo = serializer.Deserialize(e.Content); - project.ProjectInfo ??= projectInfo; - break; - } - } - }, () => - { - findInProgress = false; - if (project?.ProjectInfo != null) + + _RelayService.LookupProjectsInfoByEventIds(x => { - SessionStorage.StoreProject(project); - } - else + if (project is { ProjectInfo : null }) + project.ProjectInfo = serializer.Deserialize(x.Content!) ?? + throw new Exception("The project info must be in the application specific data event"); + }, + () => { - // Handle case where project info is not available - error = "Project not found..."; - } - StateHasChanged(); - }, - new[] { projectIndexerData.NostrPubKey }); + _RelayService.LookupNostrProfileForNPub( + (projectNpub, metadata) => + { + if (project is { Metadata: null }) { project.Metadata = metadata; } + }, + () => + { + findInProgress = false; + if (project?.ProjectInfo != null) + { + SessionStorage.StoreProject(project); + } + else + { + // Handle case where project info is not available + error = "Project not found..."; + } + StateHasChanged(); + }, + project.ProjectInfo.NostrPubKey); + },projectIndexerData.NostrEventId); } else { @@ -543,7 +542,8 @@ await RefreshBalance(); } } - + + private async Task RefreshBalance() { try diff --git a/src/Angor/Client/Storage/ClientStorage.cs b/src/Angor/Client/Storage/ClientStorage.cs index c3b0b5be..812cdb6a 100644 --- a/src/Angor/Client/Storage/ClientStorage.cs +++ b/src/Angor/Client/Storage/ClientStorage.cs @@ -218,16 +218,6 @@ public string GetNetwork() return _storage.GetItem("network"); } - public void SetNostrPublicKeyPerProject(string projectId,string nostrPubKey) - { - _storage.SetItem($"project:{projectId}:nostrKey", nostrPubKey); - } - - public string GetNostrPublicKeyPerProject(string projectId) - { - return _storage.GetItem($"project:{projectId}:nostrKey"); - } - public string GetCurrencyDisplaySetting() { return _storage.GetItem(CurrencyDisplaySettingKey) ?? "BTC"; diff --git a/src/Angor/Client/Storage/IClientStorage.cs b/src/Angor/Client/Storage/IClientStorage.cs index 8eb5fa8a..68f8e55d 100644 --- a/src/Angor/Client/Storage/IClientStorage.cs +++ b/src/Angor/Client/Storage/IClientStorage.cs @@ -23,13 +23,6 @@ public interface IClientStorage void DeleteFounderProjects(); - // void AddOrUpdateSignatures(SignatureInfo signatureInfo); - // void RemoveSignatures(SignatureInfo signatureInfo); - // List GetSignatures(); - // void DeleteSignatures(); - void SetNostrPublicKeyPerProject(string projectId, string nostrPubKey); - string GetNostrPublicKeyPerProject(string projectId); - SettingsInfo GetSettingsInfo(); void SetSettingsInfo(SettingsInfo settingsInfo); void WipeStorage(); diff --git a/src/Angor/Server/TestController.cs b/src/Angor/Server/TestController.cs index 8da2fde4..e95ccb58 100644 --- a/src/Angor/Server/TestController.cs +++ b/src/Angor/Server/TestController.cs @@ -2,8 +2,8 @@ using Angor.Shared; using Angor.Shared.Models; using Angor.Shared.ProtocolNew; -using Blockcore.Consensus.TransactionInfo; using Microsoft.AspNetCore.Mvc; +using ProjectInvestment = Angor.Server.ProjectInvestment; namespace Blockcore.AtomicSwaps.Server.Controllers { diff --git a/src/Angor/Shared/DerivationOperations.cs b/src/Angor/Shared/DerivationOperations.cs index 1efb54a3..e887cb1f 100644 --- a/src/Angor/Shared/DerivationOperations.cs +++ b/src/Angor/Shared/DerivationOperations.cs @@ -245,25 +245,20 @@ public async Task DeriveProjectNostrPrivateKeyAsync(WalletWords walletWords public uint DeriveProjectId(string founderKey) { ExtKey.UseBCForHMACSHA512 = true; - Blockcore.NBitcoin.Crypto.Hashes.UseBCForHMACSHA512 = true; - - Network network = _networkConfiguration.GetNetwork(); + Hashes.UseBCForHMACSHA512 = true; var key = new PubKey(founderKey); var hashOfid = Hashes.Hash256(key.ToBytes()); - var projectid = hashOfid.GetLow32(); - - var ret = projectid / 2; // the max size of bip32 derivation range is 2,147,483,648 (2^31) the max number of uint is 4,294,967,295 so we must divide by two - - _logger.LogInformation($"DeriveProjectId - founderKey = {founderKey}, hashOfFounderKey = {hashOfid}, hashOfFounderKeyCastToInt = {projectid}, hashOfFounderKeyCastToIntDivided = {ret}"); - - - if (ret >= 2_147_483_648) + var projectId = (uint)(hashOfid.GetLow64() & int.MaxValue); + + _logger.LogInformation($"DeriveProjectId - founderKey = {founderKey}, hashOfFounderKey = {hashOfid}, hashOfFounderKeyCastToInt = {projectId}"); + + if (projectId >= 2_147_483_648) throw new Exception(); - - return ret; + + return projectId; } public string DeriveAngorKey(string founderKey, string angorRootKey) diff --git a/src/Angor/Shared/Models/ProjectIndexerData.cs b/src/Angor/Shared/Models/ProjectIndexerData.cs new file mode 100644 index 00000000..740ac315 --- /dev/null +++ b/src/Angor/Shared/Models/ProjectIndexerData.cs @@ -0,0 +1,12 @@ +namespace Angor.Shared.Services; + +public class ProjectIndexerData +{ + public string FounderKey { get; set; } + public string ProjectIdentifier { get; set; } + public long CreatedOnBlock { get; set; } + public string NostrEventId { get; set; } + + public string TrxId { get; set; } + public long? TotalInvestmentsCount { get; set; } +} \ No newline at end of file diff --git a/src/Angor/Shared/Models/ProjectInvestment.cs b/src/Angor/Shared/Models/ProjectInvestment.cs new file mode 100644 index 00000000..e6e712f9 --- /dev/null +++ b/src/Angor/Shared/Models/ProjectInvestment.cs @@ -0,0 +1,14 @@ +namespace Angor.Shared.Models; + +public class ProjectInvestment +{ + public string TransactionId { get; set; } + + public string InvestorPublicKey { get; set; } + + public long TotalAmount { get; set; } + + public string HashOfSecret { get; set; } + + public bool IsSeeder { get; set; } +} \ No newline at end of file diff --git a/src/Angor/Shared/Models/ProjectMetadata.cs b/src/Angor/Shared/Models/ProjectMetadata.cs index 06c64a7f..55549ef2 100644 --- a/src/Angor/Shared/Models/ProjectMetadata.cs +++ b/src/Angor/Shared/Models/ProjectMetadata.cs @@ -24,7 +24,8 @@ public static ProjectMetadata Parse(NostrMetadata nostrMetadata) Nip05 = nostrMetadata.Nip57, About = nostrMetadata.About, Banner = nostrMetadata.Banner, - Picture = nostrMetadata.Picture + Picture = nostrMetadata.Picture, + Name = nostrMetadata.Name }; if (nostrMetadata.AdditionalData.ContainsKey(nameof(project.Website))) project.Website = nostrMetadata.AdditionalData[nameof(project.Website)].ToString(); diff --git a/src/Angor/Shared/Models/ProjectStats.cs b/src/Angor/Shared/Models/ProjectStats.cs new file mode 100644 index 00000000..b79c189a --- /dev/null +++ b/src/Angor/Shared/Models/ProjectStats.cs @@ -0,0 +1,9 @@ +namespace Angor.Shared.Models; + +public class ProjectStats +{ + public long InvestorCount { get; set; } + public long AmountInvested { get; set; } + public long AmountInPenalties { get; set; } + public long CountInPenalties { 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 009bd72d..05751ad2 100644 --- a/src/Angor/Shared/ProtocolNew/FounderTransactionActions.cs +++ b/src/Angor/Shared/ProtocolNew/FounderTransactionActions.cs @@ -139,7 +139,7 @@ public TransactionInfo SpendFounderStage(ProjectInfo projectInfo, IEnumerable i int stageNumber, Script founderRecieveAddress, string founderPrivateKey, FeeEstimation fee); - Transaction CreateNewProjectTransaction(string founderKey, Script angorKey, long angorFeeSatoshis, string nostrPubKey); + Transaction CreateNewProjectTransaction(string founderKey, Script angorKey, long angorFeeSatoshis, short keyType, + 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 a14db20a..7bf03a21 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, string nostrPubKey); + Script BuildFounderInfoScript(string founderKey, short keyType, 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 090f4401..0fc0d6d1 100644 --- a/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs +++ b/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs @@ -24,10 +24,11 @@ public Script BuildInvestorInfoScript(string investorKey) Op.GetPushOp(new PubKey(investorKey).ToBytes())); } - public Script BuildFounderInfoScript(string founderKey, string nostrPuKey) + public Script BuildFounderInfoScript(string founderKey, short keyType , string nostrPuKey) { return new Script(OpcodeType.OP_RETURN, Op.GetPushOp(new PubKey(founderKey).ToBytes()), + Op.GetPushOp(BitConverter.GetBytes(keyType)), Op.GetPushOp(Encoders.Hex.DecodeData(nostrPuKey))); } diff --git a/src/Angor/Shared/Services/IIndexerService.cs b/src/Angor/Shared/Services/IIndexerService.cs new file mode 100644 index 00000000..ba1bd23c --- /dev/null +++ b/src/Angor/Shared/Services/IIndexerService.cs @@ -0,0 +1,22 @@ +using Angor.Shared.Models; + +namespace Angor.Shared.Services; + +public interface IIndexerService +{ + Task> GetProjectsAsync(int? offset, int limit); + Task GetProjectByIdAsync(string projectId); + Task GetProjectStatsAsync(string projectId); + Task> GetInvestmentsAsync(string projectId); + Task GetInvestmentAsync(string projectId, string investorPubKey); + + + Task PublishTransactionAsync(string trxHex); + Task GetAdressBalancesAsync(List data, bool includeUnconfirmed = false); + Task?> FetchUtxoAsync(string address, int limit, int offset); + Task GetFeeEstimationAsync(int[] confirmations); + + Task GetTransactionHexByIdAsync(string transactionId); + + Task GetTransactionInfoByIdAsync(string transactionId); +} \ No newline at end of file diff --git a/src/Angor/Shared/Services/IRelayService.cs b/src/Angor/Shared/Services/IRelayService.cs index 4066b10e..31028a2d 100644 --- a/src/Angor/Shared/Services/IRelayService.cs +++ b/src/Angor/Shared/Services/IRelayService.cs @@ -7,12 +7,11 @@ namespace Angor.Shared.Services; public interface IRelayService { + void LookupNostrProfileForNPub(Action onResponse, Action onEndOfStream, params string[] npub); Task AddProjectAsync(ProjectInfo project, string nsec,Action action); - Task CreateNostrProfileAsync(NostrMetadata metadata, string nsec, - Action action); + Task CreateNostrProfileAsync(NostrMetadata metadata, string nsec, Action action); Task DeleteProjectAsync(string eventId, string hexPrivateKey); - void LookupProjectsInfoByPubKeys(Action responseDataAction, Action? OnEndOfStreamAction, - params string[] nostrPubKey); + void LookupProjectsInfoByEventIds(Action responseDataAction, Action? OnEndOfStreamAction, params string[] nostrEventIds); void RequestProjectCreateEventsByPubKey(Action onResponseAction, Action? onEoseAction,params string[] nostrPubKeys); Task LookupSignaturesDirectMessagesForPubKeyAsync(string nostrPubKey, DateTime? since, int? limit, Action onResponseAction); diff --git a/src/Angor/Shared/Services/ISignService.cs b/src/Angor/Shared/Services/ISignService.cs index e6f427b7..f9c29cd6 100644 --- a/src/Angor/Shared/Services/ISignService.cs +++ b/src/Angor/Shared/Services/ISignService.cs @@ -7,7 +7,7 @@ public interface ISignService (DateTime eventTime, string eventId) RequestInvestmentSigs(SignRecoveryRequest signRecoveryRequest); void LookupSignatureForInvestmentRequest(string investorNostrPubKey, string projectNostrPubKey, DateTime sigRequestSentTime, string sigRequestEventId, Func action); - Task LookupInvestmentRequestsAsync(string nostrPubKey, DateTime? since, Action action, + Task LookupInvestmentRequestsAsync(string nostrPubKey, string? senderNpub, DateTime? since, Action action, Action onAllMessagesReceived); void LookupInvestmentRequestApprovals(string nostrPubKey, Action action, diff --git a/src/Angor/Shared/Services/IndexerService.cs b/src/Angor/Shared/Services/IndexerService.cs index 55602206..82d5d82e 100644 --- a/src/Angor/Shared/Services/IndexerService.cs +++ b/src/Angor/Shared/Services/IndexerService.cs @@ -4,54 +4,6 @@ namespace Angor.Shared.Services { - public interface IIndexerService - { - Task> GetProjectsAsync(int? offset, int limit); - Task GetProjectByIdAsync(string projectId); - Task GetProjectStatsAsync(string projectId); - - Task> GetInvestmentsAsync(string projectId); - Task PublishTransactionAsync(string trxHex); - Task GetAdressBalancesAsync(List data, bool includeUnconfirmed = false); - Task?> FetchUtxoAsync(string address, int limit, int offset); - Task GetFeeEstimationAsync(int[] confirmations); - - Task GetTransactionHexByIdAsync(string transactionId); - - Task GetTransactionInfoByIdAsync(string transactionId); - } - public class ProjectIndexerData - { - public string FounderKey { get; set; } - public string ProjectIdentifier { get; set; } - public long CreatedOnBlock { get; set; } - public string NostrPubKey { get; set; } - - public string TrxId { get; set; } - public long? TotalInvestmentsCount { get; set; } - } - - public class ProjectInvestment - { - public string TransactionId { get; set; } - - public string InvestorPublicKey { get; set; } - - public long TotalAmount { get; set; } - - public string HashOfSecret { get; set; } - - public bool IsSeeder { get; set; } - } - - public class ProjectStats - { - public long InvestorCount { get; set; } - public long AmountInvested { get; set; } - public long AmountInPenalties { get; set; } - public long CountInPenalties { get; set; } - } - public class IndexerService : IIndexerService { private readonly INetworkConfiguration _networkConfiguration; @@ -126,6 +78,16 @@ public async Task> GetInvestmentsAsync(string projectId) return await response.Content.ReadFromJsonAsync>(); } + public async Task GetInvestmentAsync(string projectId, string investorPubKey) + { + var indexer = _networkService.GetPrimaryIndexer(); + var response = await _httpClient.GetAsync($"{indexer.Url}/api/query/Angor/projects/{projectId}/investments/{investorPubKey}"); + _networkService.CheckAndHandleError(response); + return response.IsSuccessStatusCode + ? await response.Content.ReadFromJsonAsync() + : null; + } + public async Task PublishTransactionAsync(string trxHex) { var indexer = _networkService.GetPrimaryIndexer(); diff --git a/src/Angor/Shared/Services/RelayService.cs b/src/Angor/Shared/Services/RelayService.cs index 7ecc2f7a..976e96f4 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -27,7 +27,8 @@ public RelayService(ILogger logger, INostrCommunicationFactory com _serializer = serializer; } - public void LookupProjectsInfoByPubKeys(Action responseDataAction, Action? OnEndOfStreamAction,params string[] nostrPubKeys) + public void LookupProjectsInfoByEventIds(Action responseDataAction, Action? OnEndOfStreamAction, + params string[] nostrEventIds) { const string subscriptionName = "ProjectInfoLookups"; @@ -36,12 +37,6 @@ public void LookupProjectsInfoByPubKeys(Action responseDataAction, Action? if (nostrClient == null) throw new InvalidOperationException("The nostr client is null"); - var request = new NostrRequest(subscriptionName, new NostrFilter - { - Authors = nostrPubKeys, - Kinds = new[] { NostrKind.ApplicationSpecificData } - }); - if (!_subscriptionsHandling.RelaySubscriptionAdded(subscriptionName)) { var subscription = nostrClient.Streams.EventStream @@ -57,10 +52,16 @@ public void LookupProjectsInfoByPubKeys(Action responseDataAction, Action? _subscriptionsHandling.TryAddEoseAction(subscriptionName, OnEndOfStreamAction); } + var request = new NostrRequest(subscriptionName, new NostrFilter + { + Ids = nostrEventIds, + Kinds = [NostrKind.ApplicationSpecificData] + }); + nostrClient.Send(request); } - public void RequestProjectCreateEventsByPubKey(Action onResponseAction, Action? onEoseAction,params string[] nostrPubKeys) + public void RequestProjectCreateEventsByPubKey(Action onResponseAction, Action? onEoseAction,params string[] nPubs) { var subscriptionKey = Guid.NewGuid().ToString().Replace("-",""); @@ -84,8 +85,8 @@ public void RequestProjectCreateEventsByPubKey(Action onResponseActi nostrClient.Send(new NostrRequest(subscriptionKey, new NostrFilter { - Authors = nostrPubKeys, - Kinds = new[] { NostrKind.ApplicationSpecificData, NostrKind.Metadata}, + Authors = nPubs, + Kinds = [NostrKind.ApplicationSpecificData, NostrKind.Metadata], })); } @@ -180,6 +181,35 @@ public void CloseConnection() _subscriptionsHandling.Dispose(); } + public void LookupNostrProfileForNPub(Action onResponse, Action onEndOfStream, params string[] npubs) + { + var client = _communicationFactory.GetOrCreateClient(_networkService); + + var subscriptionKey = Guid.NewGuid().ToString().Replace("-",""); + + if (!_subscriptionsHandling.RelaySubscriptionAdded(subscriptionKey)) + { + var subscription = client.Streams.EventStream + .Where(_ => _.Subscription == subscriptionKey) + .Where(_ => _.Event is not null) + .Select(_ => _.Event as NostrMetadataEvent) + .Subscribe(@event => onResponse(@event.Pubkey, ProjectMetadata.Parse(_serializer.Deserialize(@event.Content)))); + + _subscriptionsHandling.TryAddRelaySubscription(subscriptionKey, subscription); + } + + if (onEndOfStream != null) + { + _subscriptionsHandling.TryAddEoseAction(subscriptionKey, onEndOfStream); + } + + client.Send(new NostrRequest(subscriptionKey, new NostrFilter + { + Authors = npubs, + Kinds = [NostrKind.Metadata], + })); + } + public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey, Action action) { var key = NostrPrivateKey.FromHex(hexPrivateKey); diff --git a/src/Angor/Shared/Services/SignService.cs b/src/Angor/Shared/Services/SignService.cs index 06656e00..68deba79 100644 --- a/src/Angor/Shared/Services/SignService.cs +++ b/src/Angor/Shared/Services/SignService.cs @@ -72,7 +72,8 @@ public void LookupSignatureForInvestmentRequest(string investorNostrPubKey, stri })); } - public Task LookupInvestmentRequestsAsync(string nostrPubKey, DateTime? since, Action action, Action onAllMessagesReceived) + public Task LookupInvestmentRequestsAsync(string nostrPubKey, string? senderNpub, DateTime? since, + Action action, Action onAllMessagesReceived) { var nostrClient = _communicationFactory.GetOrCreateClient(_networkService); var subscriptionKey = nostrPubKey + "sig_req"; @@ -91,13 +92,17 @@ public Task LookupInvestmentRequestsAsync(string nostrPubKey, DateTime? since, A } _subscriptionsHanding.TryAddEoseAction(subscriptionKey, onAllMessagesReceived); - - nostrClient.Send(new NostrRequest(subscriptionKey, new NostrFilter + + var nostrFilter = new NostrFilter { - P = new[] { nostrPubKey }, //To founder - Kinds = new[] { NostrKind.EncryptedDm }, + P = [nostrPubKey], //To founder, + Kinds = [NostrKind.EncryptedDm], Since = since - })); + }; + + if (senderNpub != null) nostrFilter.Authors = [senderNpub]; //From investor + + nostrClient.Send(new NostrRequest(subscriptionKey, nostrFilter)); return Task.CompletedTask; }