diff --git a/src/Angor.Test/Angor.Test.csproj b/src/Angor.Test/Angor.Test.csproj index 43378709..f26916ac 100644 --- a/src/Angor.Test/Angor.Test.csproj +++ b/src/Angor.Test/Angor.Test.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Angor/Client/Pages/Browse.razor b/src/Angor/Client/Pages/Browse.razor index 290b6be1..a7b56904 100644 --- a/src/Angor/Client/Pages/Browse.razor +++ b/src/Angor/Client/Pages/Browse.razor @@ -77,6 +77,8 @@ { searchInProgress = true; var blockchainProjects = await _IndexerService.GetProjectsAsync(); + + blockchainProjects = blockchainProjects.Where(_ => _.NostrPubKey != null).ToList(); var projectCreators = SessionStorage.GetProjectSubscribedList(); @@ -85,7 +87,7 @@ .Select(_ => _.NostrPubKey) .Where(nostrPubKey => !projectCreators.Contains(nostrPubKey))); - await _RelayService.RequestProjectDataAsync(_ => + await _RelayService.LookupProjectsInfoByPubKeysAsync(_ => { if (!SessionStorage.IsProjectInStorageById(_.ProjectIdentifier)) SessionStorage.StoreProjectInfo(_); diff --git a/src/Angor/Client/Pages/Create.razor b/src/Angor/Client/Pages/Create.razor index 38d7e34f..545a2c0b 100644 --- a/src/Angor/Client/Pages/Create.razor +++ b/src/Angor/Client/Pages/Create.razor @@ -8,14 +8,17 @@ @using Angor.Shared.Services @using Blockcore.NBitcoin @using Blockcore.NBitcoin.DataEncoders +@using Nostr.Client.Json +@using Nostr.Client.Messages +@using Nostr.Client.Messages.Metadata +@implements IDisposable @inherits BaseComponent @inject IDerivationOperations _derivationOperations @inject IWalletStorage _walletStorage; @inject IClientStorage storage; @inject NavigationManager NavigationManager @inject IWalletOperations _WalletOperations -@inject INetworkConfiguration _NetworkConfiguration @inject IRelayService _RelayService @inject ISignService _SignService @@ -33,6 +36,53 @@ + @if (!nostrProfileCreated) + { + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +
+ } + else + { @@ -91,7 +141,7 @@ - + } @if (showCreateModal) { @@ -166,15 +216,31 @@ PenaltyDate = DateTime.UtcNow.AddDays(100), ExpiryDate = DateTime.UtcNow.AddDays(50), TargetAmount = 100, - TransactionId = "unknowen", + CreationTransactionId = "unknowen", Stages = new List { - new Stage { AmountToRelease = 10, ReleaseDate = DateTime.UtcNow }, //.AddDays(10) }, during testing we often need to spend a stage immediately - new Stage { AmountToRelease = 30, ReleaseDate = DateTime.UtcNow.AddDays(20) }, - new Stage { AmountToRelease = 60, ReleaseDate = DateTime.UtcNow.AddDays(30) }, + new () { AmountToRelease = 10, ReleaseDate = DateTime.UtcNow }, //.AddDays(10) }, during testing we often need to spend a stage immediately + new () { AmountToRelease = 30, ReleaseDate = DateTime.UtcNow.AddDays(20) }, + new () { AmountToRelease = 60, ReleaseDate = DateTime.UtcNow.AddDays(30) }, } }; + private class NostrMetadataCollection + { + public string Name { get; set; } + public string Website { get; set; } + public string About { get; set; } + public string Picture { get; set; } + public string Nip05 { get; set; } + public string Lud16 { get; set; } + public string Banner { get; set; } + public string Nip57 { get; set; } + } + + NostrMetadataCollection NostrMetadata = new (); + bool nostrProfileCreated; + bool applicationDataOnNostr; + protected override async Task OnInitializedAsync() { hasWallet = _walletStorage.HasWallet(); @@ -194,6 +260,53 @@ } await _RelayService.ConnectToRelaysAsync(); + + await _RelayService.RequestProjectEventsoByPubKeyAsync(project.NostrPubKey, _ => + { + nostrProfileCreated = _.Kind == NostrKind.Metadata; + + if (_.Kind == NostrKind.ApplicationSpecificData) //In case of a crashed application in the middle of the call + { + var nostrProject = Newtonsoft.Json.JsonConvert.DeserializeObject(_.Content!, NostrSerializer.Settings); + var findProject = storage.GetFounderProjects().FirstOrDefault(p => p.ProjectIdentifier == nostrProject!.ProjectIdentifier); + if (findProject == null) + storage.AddFounderProject(nostrProject!); + NavigationManager.NavigateTo($"/view/{nostrProject.ProjectIdentifier}"); + } + + StateHasChanged(); + }); + } + + private async Task CreatNostrProfile() + { + var nostrKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); + + var resultId = await _RelayService.CreateNostrProfileAsync(new NostrMetadata + { + About = NostrMetadata.About, + Banner = NostrMetadata.Banner, + Lud16 = NostrMetadata.Lud16, + Name = NostrMetadata.Name, + Nip05 = NostrMetadata.Nip05, + Nip57 = NostrMetadata.Nip57, + Picture = NostrMetadata.Picture, + AdditionalData = new Dictionary + { + {"website",NostrMetadata.Website}, + {"display_name",NostrMetadata.Name} + } + }, NBitcoin.DataEncoders.Encoders.Hex.EncodeData((byte[])nostrKey.ToBytes())); + + _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 + nostrProfileCreated = true; + StateHasChanged(); + }); } private async Task CreatProject() @@ -238,7 +351,7 @@ signedTransaction = _WalletOperations.AddInputsAndSignTransaction(accountInfo.GetNextChangeReceiveAddress(), unsignedTransaction, _walletStorage.GetWallet(), accountInfo, feeData.SelectedFeeEstimation); - project.TransactionId = signedTransaction.GetHash().ToString(); + project.CreationTransactionId = signedTransaction.GetHash().ToString(); return new OperationResult { Success = true }; @@ -281,18 +394,24 @@ var operationResult = await notificationComponent.LongOperation(async () => { showCreateModal = false; + + var nostrKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); + var nostrKeyHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(nostrKey.ToBytes()); + + var resultId = await _RelayService.AddProjectAsync(project, nostrKeyHex); + var response = await _WalletOperations.PublishTransactionAsync(network, signedTransaction); if (!response.Success) + { + await _RelayService.DeleteProjectAsync(resultId, nostrKeyHex); + //storage.AddFounderProject(project); TODO remove from storage return response; - - storage.AddFounderProject(project); + } - var nostrKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); + storage.AddFounderProject(project); - var resultId = await _RelayService.AddProjectAsync(project, NBitcoin.DataEncoders.Encoders.Hex.EncodeData(nostrKey.ToBytes())); - _RelayService.RegisterOKMessageHandler(resultId, _ => { if (_.EventId != resultId) @@ -301,11 +420,10 @@ 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); - await _SignService.AddSignKeyAsync(project, Encoders.Hex.EncodeData(key.ToBytes())); + await _SignService.AddSignKeyAsync(project, Encoders.Hex.EncodeData(key.ToBytes()), nostrKeyHex); return new OperationResult { Success = response.Success, Message = response.Message }; }); @@ -333,4 +451,10 @@ { project.Stages.Remove(stage); } + + public void Dispose() + { + _RelayService.CloseConnection(); + } + } diff --git a/src/Angor/Client/Pages/Invest.razor b/src/Angor/Client/Pages/Invest.razor index e1fe7128..a770c32f 100644 --- a/src/Angor/Client/Pages/Invest.razor +++ b/src/Angor/Client/Pages/Invest.razor @@ -2,25 +2,25 @@ @using Angor.Shared @using Angor.Client.Storage @using Angor.Shared.Models -@using Angor.Shared.Protocol @using Blockcore.Consensus.TransactionInfo @using Blockcore.NBitcoin @using Angor.Client.Services @using Angor.Shared.ProtocolNew @using Blockcore.NBitcoin.DataEncoders +@using JSException = Microsoft.JSInterop.JSException + +@inject IJSRuntime JS @inherits BaseComponent @inject ILogger _Logger; -@inject HttpClient Http @inject IDerivationOperations _derivationOperations @inject IWalletStorage _walletStorage; @inject IClientStorage storage; +@inject ISessionStorage SessionStorage; @inject NavigationManager NavigationManager @inject IWalletOperations _WalletOperations -@inject INetworkConfiguration _NetworkConfiguration -@inject IIndexerService _IndexerService @inject ISignService _SignService @inject IInvestorTransactionActions _InvestorTransactionActions @@ -43,7 +43,8 @@

You are founder.

return; } - +@if(recoverySigs == null) +{

Investment Page

Here is a small explanation of the project. You can view more details about the project here.

@@ -143,27 +144,42 @@ } - @if (StagesBreakdown != null) + @if (StagesBreakdown != null) + { +
+
+

Stages Breakdown

+
+
+ @foreach (var stage in StagesBreakdown) + { +

Stage @stage.StageNumber: invest @stage.Amount BTC that will released on @stage.StageDateTime.ToString("dd/MM/yyyy") (@stage.DaysFromStartDate days)

+ } +
+
+ } +
+} + @if (recoverySigs != null) { -
-
-

Stages Breakdown

-
-
- @foreach (var stage in StagesBreakdown) - { -

Stage @stage.StageNumber: invest @stage.Amount BTC that will released on @stage.StageDateTime.ToString("dd/MM/yyyy") (@stage.DaysFromStartDate days)

- } + } -
@code { + [Parameter] public string ProjectId { get; set; } - public InvestmentModel Investment { get; set; } = new InvestmentModel {InvestmentAmount = 10}; + public InvestmentModel Investment { get; set; } = new InvestmentModel { InvestmentAmount = 10 }; private bool IsSeederTimePassed { get; set; } private List StagesBreakdown { get; set; } = new(); bool founder = false; @@ -172,12 +188,12 @@ private bool showCreateModal; Transaction signedTransaction; Transaction unSignedTransaction; - //InvestorContext context; SignatureInfo recoverySigs; - + private FeeData feeData = new(); - + + private IJSInProcessObjectReference? javascriptNostrToolsModule; protected override async Task OnInitializedAsync() { @@ -202,7 +218,7 @@ } else { - findProject = storage.GetBrowseProjects().FirstOrDefault(p => p.ProjectIdentifier == ProjectId); + findProject = SessionStorage.GetProjectById(ProjectId); if (findProject != null) { @@ -217,6 +233,24 @@ UpdateStagesBreakdown(new ChangeEventArgs { Value = Investment.InvestmentAmount }); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + //TODO import the nostr tool module directly to c# class + javascriptNostrToolsModule = await JS.InvokeAsync("import", "./Pages/invest.razor.js?version=" + DateTime.UtcNow.Ticks); + } + catch (JSException e) + { + Console.WriteLine(e); + notificationComponent.ShowErrorMessage(e.Message); + } + + } + } + private Task CheckIfSeederTimeHasPassed() { // Implement your logic to check whether the seeder time has passed. @@ -352,14 +386,73 @@ var strippedInvestmentTransaction = network.CreateTransaction(signedTransaction.ToHex()); strippedInvestmentTransaction.Inputs.ForEach(f => f.WitScript = Blockcore.Consensus.TransactionInfo.WitScript.Empty); - recoverySigs = await _SignService.GetInvestmentSigsAsync(new SignRecoveryRequest + var accountInfo = storage.GetAccountInfo(_networkConfiguration.GetNetwork().Name); + + var nostrPrivateKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), accountInfo.InvestmentsCount + 1); + + var nostrPrivateKeyHex = Encoders.Hex.EncodeData(nostrPrivateKey.ToBytes()); + + var encryptedContent = await javascriptNostrToolsModule.InvokeAsync( + "encryptNostr", + nostrPrivateKeyHex, + project.NostrPubKey, + strippedInvestmentTransaction.ToHex(network.Consensus.ConsensusFactory)); + + var eventId = await _SignService.RequestInvestmentSigsAsync(new SignRecoveryRequest { - ProjectIdentifier = project.ProjectIdentifier, - InvestmentTransaction = strippedInvestmentTransaction.ToHex(network.Consensus.ConsensusFactory) - }); + ProjectIdentifier = project.ProjectIdentifier, + content = encryptedContent, + NostrPubKey = project.NostrPubKey, + InvestorNostrPrivateKey = nostrPrivateKeyHex + }, async _ => + { + var test = await javascriptNostrToolsModule.InvokeAsync( + "decryptNostr", + nostrPrivateKeyHex, + project.NostrPubKey, + _); + + _Logger.LogInformation("signature : " + test); + + recoverySigs ??= new SignatureInfo(); + + + recoverySigs.Signatures.Add(new SignatureInfoItem + { + Signature = test, StageIndex = recoverySigs.Signatures.Count() + }); + + + if (recoverySigs.Signatures.Count == project.Stages.Count()) + { + StateHasChanged(); + } + + }); + + return new OperationResult { Success = true, }; + }); + + if (operationResult.Success) + { + notificationComponent.ShowNotificationMessage("Project created", 1); + } + else + { + notificationComponent.ShowErrorMessage(operationResult.Message); + } + } - // validate the signatures - _InvestorTransactionActions.CheckInvestorRecoverySignatures(project, signedTransaction, recoverySigs); + public async Task PublishSignedTransactionAsync() + { + var operationResult = await notificationComponent.LongOperation(async () => + { + var validSignatures = _InvestorTransactionActions.CheckInvestorRecoverySignatures(project, signedTransaction, recoverySigs); + + if (!validSignatures) + { + return new OperationResult { Success = false, Message = "The signatures returned from the founder failed validation" }; + } // link the trx to the signatures recoverySigs.TransactionId = signedTransaction.GetHash().ToString(); @@ -368,18 +461,15 @@ var response = await _WalletOperations.PublishTransactionAsync(network, signedTransaction); - if (!response.Success) - return response; - - return new OperationResult { Success = response.Success, Message = response.Message }; + return !response.Success ? response : new OperationResult { Success = true }; }); if (operationResult.Success) { - notificationComponent.ShowNotificationMessage("Project created", 1); + notificationComponent.ShowNotificationMessage("Invested in project", 1); storage.AddProject(project); - + NavigationManager.NavigateTo($"/view/{project.ProjectIdentifier}"); } else diff --git a/src/Angor/Client/Pages/Invest.razor.js b/src/Angor/Client/Pages/Invest.razor.js new file mode 100644 index 00000000..767e62c4 --- /dev/null +++ b/src/Angor/Client/Pages/Invest.razor.js @@ -0,0 +1,13 @@ +import {nip04} from 'https://cdn.jsdelivr.net/npm/nostr-tools@1.17.0/+esm' + +export function encryptNostr(sk1,pk2, message){ + debugger; + console.log("encrypting the nostr message to pub key " + pk2) + return nip04.encrypt(sk1,pk2, message) +} + +export function decryptNostr(sk2,pk1, message){ + debugger; + console.log("decrypting the nostr message from pub key " + pk1) + return nip04.decrypt(sk2,pk1,message); +} \ No newline at end of file diff --git a/src/Angor/Client/Pages/View.razor b/src/Angor/Client/Pages/View.razor index 17270879..ee519637 100644 --- a/src/Angor/Client/Pages/View.razor +++ b/src/Angor/Client/Pages/View.razor @@ -12,6 +12,7 @@ @inject ISessionStorage SessionStorage; @inject NavigationManager NavigationManager @inject INetworkConfiguration _NetworkConfiguration +@inject IClipboardService _clipboardService

View Project

@@ -111,6 +112,15 @@

You are the founder.

Spend Funds + @if (project.NostrPubKey != null) + { //TODO this is just to be able to log in to the nostr client, need to get a proper way to get the nostr keys +
+ + + +
+ } + } else if (invested) { @@ -231,7 +241,7 @@ if (project != null) { - projectExplorerLink = _NetworkConfiguration.GetExplorerUrl().Url + $"/transaction/{project.TransactionId}"; + projectExplorerLink = _NetworkConfiguration.GetExplorerUrl().Url + $"/transaction/{project.CreationTransactionId}"; } } @@ -256,4 +266,13 @@ { NavigationManager.NavigateTo($"/invest/{ProjectId}"); } + + + private async Task CopyWordsToClibboardAsync() + { + var nostrKey = _derivationOperations.DeriveProjectNostrPrivateKey(_walletStorage.GetWallet(), project.ProjectIndex); + var nsec = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(nostrKey.ToBytes()); + await _clipboardService.WriteTextAsync(nsec); + StateHasChanged(); + } } \ No newline at end of file diff --git a/src/Angor/Server/Program.cs b/src/Angor/Server/Program.cs index 6fa22840..b38a0520 100644 --- a/src/Angor/Server/Program.cs +++ b/src/Angor/Server/Program.cs @@ -32,7 +32,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - +builder.Services.AddSingleton(); //TODO change this from a test class when the flow is complete var app = builder.Build(); diff --git a/src/Angor/Server/ProjectContext.cs b/src/Angor/Server/ProjectContext.cs new file mode 100644 index 00000000..f01f864f --- /dev/null +++ b/src/Angor/Server/ProjectContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace Angor.Server; + +public class ProjectContext : DbContext +{ + public DbSet Projects { get; set; } + + public DbSet ProjectKeys { get; set; } + public string DbPath { get; } + + public ProjectContext(string path) + { + DbPath = path; + } + + // The following configures EF to create a Sqlite database file in the + // special "local" folder for your platform. + protected override void OnConfiguring(DbContextOptionsBuilder options) + => options.UseSqlite($"Data Source={DbPath}"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + } +} \ No newline at end of file diff --git a/src/Angor/Server/TestController.cs b/src/Angor/Server/TestController.cs index 0c6eb6e6..8da2fde4 100644 --- a/src/Angor/Server/TestController.cs +++ b/src/Angor/Server/TestController.cs @@ -88,33 +88,42 @@ public class TestSignController : ControllerBase private readonly IFounderTransactionActions _founderTransactionActions; private readonly IInvestorTransactionActions _investorTransactionActions; private readonly INetworkConfiguration _networkConfiguration; + private readonly ITestNostrSigningFromRelay _signingFromRelay; - public TestSignController(TestStorageService storage, IFounderTransactionActions founderTransactionActions, IInvestorTransactionActions investorTransactionActions, INetworkConfiguration networkConfiguration) + public TestSignController(TestStorageService storage, IFounderTransactionActions founderTransactionActions, IInvestorTransactionActions investorTransactionActions, INetworkConfiguration networkConfiguration, ITestNostrSigningFromRelay signingFromRelay) { _storage = storage; _founderTransactionActions = founderTransactionActions; _investorTransactionActions = investorTransactionActions; _networkConfiguration = networkConfiguration; + _signingFromRelay = signingFromRelay; } [HttpPost] public async Task Post([FromBody] SignData project) { - await _storage.AddKey(project.ProjectIdentifier, project.FounderRecoveryPrivateKey); + await _storage.AddKey(project.ProjectIdentifier, project); + await _signingFromRelay.SignTransactionsFromNostrAsync(project.ProjectIdentifier); } + [HttpGet] + public async Task Get(string projectIdentifier) + { + await _signingFromRelay.SignTransactionsFromNostrAsync(projectIdentifier); + } + [HttpPost] [Route("sign")] public async Task Post([FromBody] SignRecoveryRequest signRecoveryRequest) { - var key = await _storage.GetKey(signRecoveryRequest.ProjectIdentifier); - + var key = await _storage.GetKeys(signRecoveryRequest.ProjectIdentifier); + var project = (await _storage.Get()).First(f => f.ProjectIdentifier == signRecoveryRequest.ProjectIdentifier); - + // build sigs var recoverytrx = _investorTransactionActions.BuildRecoverInvestorFundsTransaction(project, _networkConfiguration.GetNetwork().CreateTransaction(signRecoveryRequest.InvestmentTransaction)); - var sigs = _founderTransactionActions.SignInvestorRecoveryTransactions(project, signRecoveryRequest.InvestmentTransaction, recoverytrx, key); - + var sigs = _founderTransactionActions.SignInvestorRecoveryTransactions(project, signRecoveryRequest.InvestmentTransaction, recoverytrx, key.founderSigningPrivateKey); + return sigs; } } diff --git a/src/Angor/Server/TestNostrSigningFromRelay.cs b/src/Angor/Server/TestNostrSigningFromRelay.cs new file mode 100644 index 00000000..92796e48 --- /dev/null +++ b/src/Angor/Server/TestNostrSigningFromRelay.cs @@ -0,0 +1,185 @@ +using System.Reactive.Linq; +using System.Text; +using System.Text.Json; +using Angor.Shared; +using Angor.Shared.Models; +using Angor.Shared.ProtocolNew; +using Newtonsoft.Json; +using Nostr.Client.Client; +using Nostr.Client.Communicator; +using Nostr.Client.Json; +using Nostr.Client.Keys; +using Nostr.Client.Messages; +using Nostr.Client.Messages.Direct; +using Nostr.Client.Requests; + +namespace Angor.Server; + +public class TestNostrSigningFromRelay : ITestNostrSigningFromRelay +{ + private static NostrWebsocketClient? _nostrClient; + private static INostrCommunicator? _nostrCommunicator; + private ILogger _clientLogger; + private ILogger _communicatorLogger; + private readonly TestStorageService _storage; + private readonly IFounderTransactionActions _founderTransactionActions; + private readonly IInvestorTransactionActions _investorTransactionActions; + private readonly INetworkConfiguration _networkConfiguration; + private ILogger _logger; + + string angorRootKey = + "tpubD8JfN1evVWPoJmLgVg6Usq2HEW9tLqm6CyECAADnH5tyQosrL6NuhpL9X1cQCbSmndVrgLSGGdbRqLfUbE6cRqUbrHtDJgSyQEY2Uu7WwTL"; + + public TestNostrSigningFromRelay(ILogger clientLogger, ILogger communicatorLogger, TestStorageService storage, IFounderTransactionActions founderTransactionActions, IInvestorTransactionActions investorTransactionActions, INetworkConfiguration networkConfiguration, ILogger logger) + { + _clientLogger = clientLogger; + _communicatorLogger = communicatorLogger; + _storage = storage; + _founderTransactionActions = founderTransactionActions; + _investorTransactionActions = investorTransactionActions; + _networkConfiguration = networkConfiguration; + _logger = logger; + } + + 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}"); + }); + + _nostrClient.Streams.EoseStream.Subscribe(_ => + { + _clientLogger.LogInformation($"EoseStream {_.Subscription} message - {_.AdditionalData}"); + }); + } + + private void SetupNostrCommunicator() + { + _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("wss://relay.angor.io")) + { + Name = "angor-relay.test", + ReconnectTimeout = null //TODO need to check what is the actual best time to set here + }; + + _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); + }); + } + + public async Task SignTransactionsFromNostrAsync(string projectIdentifier) + { + var projectKeys = await _storage.GetKeys(projectIdentifier); + + SetupNostrCommunicator(); + SetupNostrClient(); + await _nostrCommunicator.StartOrFail(); + + var nostrPrivateKey = NostrPrivateKey.FromHex(projectKeys.nostrPrivateKey); + var nostrPubKey = nostrPrivateKey.DerivePublicKey().Hex; + + _nostrClient.Streams.EventStream.Where(_ => _.Subscription == nostrPubKey + "1") + .Where(_ => _.Event.Kind == NostrKind.ApplicationSpecificData) + //.Where(_ => _.Event.Pubkey == nostrPubKey) + .Subscribe(_ => + { + _clientLogger.LogInformation("application specific data" + _.Event.Content); + _storage.Add(JsonConvert.DeserializeObject(_.Event.Content,NostrSerializer.Settings)); + }); + + _nostrClient.Streams.EventStream.Where(_ => _.Subscription == nostrPubKey + "2") + .Where(_ => _.Event.Kind == NostrKind.EncryptedDm) + // .Where(_ => _.Event.Tags.ContainsTag("p",nostrPubKey)) + .Select(_ => _.Event as NostrEncryptedEvent) + .Subscribe(nostrEvent => + { + _clientLogger.LogInformation("encrypted direct message"); + var project = (_storage.Get().GetAwaiter().GetResult()).First(_ => _.ProjectIdentifier == projectIdentifier); + var transactionHex = nostrEvent.DecryptContent(nostrPrivateKey); + + _clientLogger.LogInformation(transactionHex); + + var sig = signProject(transactionHex,project,projectKeys.founderSigningPrivateKey); + + var stages = sig.Signatures.Select(_ => _.Signature ); + + foreach (var stage in sig.Signatures) + { + var sigJson = JsonConvert.SerializeObject(stage.Signature); + //JsonConvert.SerializeObject(sig.Signatures.OrderBy(_ => _.StageIndex).Select(_ => _.Signature), NostrSerializer.Settings); + + _logger.LogInformation($"Signature to send for stage {stage.StageIndex}: {sigJson}"); + + var ev = new NostrEvent + { + Kind = NostrKind.EncryptedDm, + CreatedAt = DateTime.UtcNow, + Content = sigJson, + Tags = new NostrEventTags(new[] { NostrEventTag.Profile(nostrEvent.Pubkey) }) + }; + + var signed = NostrEncryptedEvent.EncryptDirectMessage(ev, nostrPrivateKey) + .Sign(nostrPrivateKey); + + _nostrClient.Send(new NostrEventRequest(signed)); + } + + + }); + + _nostrClient.Send(new NostrRequest( nostrPubKey + "1", new NostrFilter + { + Authors = new []{nostrPubKey}, + Kinds = new[] { NostrKind.ApplicationSpecificData }, + Limit = 1 + })); + + _nostrClient.Send(new NostrRequest(nostrPubKey + "2", new NostrFilter + { + P = new []{nostrPubKey}, + Kinds = new[] {NostrKind.EncryptedDm }, + Since = DateTime.UtcNow + })); + } + + private SignatureInfo signProject(string transactionHex,ProjectInfo info, string founderSigningPrivateKey) + { + var investorTrx = _networkConfiguration.GetNetwork().CreateTransaction(transactionHex); + + // build sigs + var recoverytrx = _investorTransactionActions.BuildRecoverInvestorFundsTransaction(info, investorTrx); + var sig = _founderTransactionActions.SignInvestorRecoveryTransactions(info, transactionHex, recoverytrx, founderSigningPrivateKey); + + if (!_investorTransactionActions.CheckInvestorRecoverySignatures(info, investorTrx, sig)) + throw new InvalidOperationException(); + + return sig; + } +} + +public interface ITestNostrSigningFromRelay +{ + public Task SignTransactionsFromNostrAsync(string projectIdentifier); +} + diff --git a/src/Angor/Server/TestStorageService.cs b/src/Angor/Server/TestStorageService.cs index ffd97aef..746e1866 100644 --- a/src/Angor/Server/TestStorageService.cs +++ b/src/Angor/Server/TestStorageService.cs @@ -1,10 +1,7 @@ #nullable enable -using Blockcore.AtomicSwaps.Server.Controllers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; -using Polly; using System.ComponentModel.DataAnnotations; -using System.Text.Json; using Angor.Shared.Models; namespace Angor.Server @@ -17,6 +14,16 @@ public class SerializeData public string Data { get; set; } } + public class ProjectKeys + { + [Key] + public string Key { get; set; } + + public string nostrPrivateKey { get; set; } + + public string founderSigningPrivateKey { get; set; } + } + public class ProjectIndexerData { public string FounderKey { get; set; } @@ -34,26 +41,6 @@ public class ProjectInvestment public string TrxHex { get; set; } } - public class ProjectContext : DbContext - { - public DbSet Projects { get; set; } - public string DbPath { get; } - - public ProjectContext(string path) - { - DbPath = path; - } - - // The following configures EF to create a Sqlite database file in the - // special "local" folder for your platform. - protected override void OnConfiguring(DbContextOptionsBuilder options) - => options.UseSqlite($"Data Source={DbPath}"); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - } - } - public class TestStorageService { private readonly string dbPath; @@ -99,15 +86,31 @@ public async Task Add(ProjectInfo project) await context.SaveChangesAsync(); } - public async Task AddKey(string projectid, string founderRecoveryPrivateKey) + public async Task AddKey(string projectid, string founderKey) + { + await using var context = new ProjectContext(dbPath); + + context.Projects.Add(new SerializeData { Key = "key:" + projectid, Data = founderKey }); + + await context.SaveChangesAsync(); + } + + public async Task AddKey(string projectid, SignData signData) { await using var context = new ProjectContext(dbPath); - context.Projects.Add(new SerializeData { Key = "key:" + projectid, Data = founderRecoveryPrivateKey }); + context.ProjectKeys.Add(new ProjectKeys { Key = projectid, founderSigningPrivateKey = signData.FounderRecoveryPrivateKey, nostrPrivateKey = signData.NostrPrivateKey }); await context.SaveChangesAsync(); } + public async Task GetKeys(string projectid) + { + await using var context = new ProjectContext(dbPath); + + return context.ProjectKeys.First(_ => _.Key == projectid); + } + public async Task GetKey(string projectid) { await using var context = new ProjectContext(dbPath); diff --git a/src/Angor/Shared/DerivationOperations.cs b/src/Angor/Shared/DerivationOperations.cs index 8ea82267..c9b95888 100644 --- a/src/Angor/Shared/DerivationOperations.cs +++ b/src/Angor/Shared/DerivationOperations.cs @@ -6,7 +6,6 @@ using Blockcore.NBitcoin.DataEncoders; using Blockcore.Networks; using Microsoft.Extensions.Logging; -using System.IO; namespace Angor.Shared; @@ -22,12 +21,32 @@ public DerivationOperations(IHdOperations hdOperations, ILogger AddressesInfo { get; set; } = new(); public List ChangeAddressesInfo { get; set; } = new(); + public int InvestmentsCount { get; set; } + public string? GetNextReceiveAddress() { return AddressesInfo.Last()?.Address; diff --git a/src/Angor/Shared/Models/ProjectInfo.cs b/src/Angor/Shared/Models/ProjectInfo.cs index f9812c6e..0bec3f7d 100644 --- a/src/Angor/Shared/Models/ProjectInfo.cs +++ b/src/Angor/Shared/Models/ProjectInfo.cs @@ -17,6 +17,6 @@ public class ProjectInfo public DateTime ExpiryDate { get; set; } public decimal TargetAmount { get; set; } public List Stages { get; set; } = new(); - public string TransactionId { get; set; } + public string CreationTransactionId { get; set; } public ProjectSeeders ProjectSeeders { get; set; } = new(); } \ No newline at end of file diff --git a/src/Angor/Shared/Models/SignData.cs b/src/Angor/Shared/Models/SignData.cs index 5a6655f4..90e409e9 100644 --- a/src/Angor/Shared/Models/SignData.cs +++ b/src/Angor/Shared/Models/SignData.cs @@ -5,4 +5,6 @@ public class SignData public string ProjectIdentifier { get; set; } public string FounderRecoveryPrivateKey { get; set; } + + public string NostrPrivateKey { get; set; } } \ No newline at end of file diff --git a/src/Angor/Shared/Models/SignRecoveryRequest.cs b/src/Angor/Shared/Models/SignRecoveryRequest.cs index 0cabbfbf..2621db45 100644 --- a/src/Angor/Shared/Models/SignRecoveryRequest.cs +++ b/src/Angor/Shared/Models/SignRecoveryRequest.cs @@ -8,4 +8,6 @@ public class SignRecoveryRequest public string NostrPubKey { get; set; } public string InvestmentTransaction { get; set; } + + public string content { get; set; } } \ No newline at end of file diff --git a/src/Angor/Shared/ProtocolNew/InvestorTransactionActions.cs b/src/Angor/Shared/ProtocolNew/InvestorTransactionActions.cs index e36f8b5e..a18af8c0 100644 --- a/src/Angor/Shared/ProtocolNew/InvestorTransactionActions.cs +++ b/src/Angor/Shared/ProtocolNew/InvestorTransactionActions.cs @@ -281,7 +281,7 @@ public bool CheckInvestorRecoverySignatures(ProjectInfo projectInfo, Transaction .Select(_ => _.TxOut) .ToArray(); - // todo: david change to Enumerable.Range + // todo: David change to Enumerable.Range for (var stageIndex = 0; stageIndex < projectInfo.Stages.Count; stageIndex++) { var scriptStages = _investmentScriptBuilder.BuildProjectScriptsForStage(projectInfo, investorKey, stageIndex, secretHash); diff --git a/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs b/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs index 6710be11..090f4401 100644 --- a/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs +++ b/src/Angor/Shared/ProtocolNew/Scripts/ProjectScriptsBuilder.cs @@ -1,5 +1,6 @@ using Blockcore.Consensus.ScriptInfo; using Blockcore.NBitcoin; +using Blockcore.NBitcoin.DataEncoders; namespace Angor.Shared.ProtocolNew.Scripts; @@ -27,7 +28,7 @@ public Script BuildFounderInfoScript(string founderKey, string nostrPuKey) { return new Script(OpcodeType.OP_RETURN, Op.GetPushOp(new PubKey(founderKey).ToBytes()), - Op.GetPushOp(new NBitcoin.PubKey(nostrPuKey).GetTaprootFullPubKey().ToBytes())); + Op.GetPushOp(Encoders.Hex.DecodeData(nostrPuKey))); } public Script BuildSeederInfoScript(string investorKey, uint256 secretHash) diff --git a/src/Angor/Shared/Services/IRelayService.cs b/src/Angor/Shared/Services/IRelayService.cs index 331e4a12..43ea7d4a 100644 --- a/src/Angor/Shared/Services/IRelayService.cs +++ b/src/Angor/Shared/Services/IRelayService.cs @@ -1,4 +1,6 @@ using Angor.Shared.Models; +using Nostr.Client.Messages; +using Nostr.Client.Messages.Metadata; using Nostr.Client.Responses; namespace Angor.Shared.Services; @@ -8,6 +10,9 @@ public interface IRelayService Task ConnectToRelaysAsync(); void RegisterOKMessageHandler(string eventId, Action action); Task AddProjectAsync(ProjectInfo project, string nsec); - Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKey); + Task CreateNostrProfileAsync(NostrMetadata metadata, string nsec); + Task DeleteProjectAsync(string eventId, string hexPrivateKey); + Task LookupProjectsInfoByPubKeysAsync(Action responseDataAction,params string[] nostrPubKey); + Task RequestProjectEventsoByPubKeyAsync(string nostrPubKey, Action onResponseAction); 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 edecdcc4..69455950 100644 --- a/src/Angor/Shared/Services/RelayService.cs +++ b/src/Angor/Shared/Services/RelayService.cs @@ -1,12 +1,13 @@ -using System.Diagnostics; -using System.Reactive.Linq; +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.Json; using Nostr.Client.Keys; using Nostr.Client.Messages; +using Nostr.Client.Messages.Metadata; using Nostr.Client.Responses; namespace Angor.Shared.Services @@ -40,12 +41,13 @@ public async Task ConnectToRelaysAsync() { SetupNostrCommunicator(); } + + if (_nostrClient == null) + { + SetupNostrClient(); + } await _nostrCommunicator.StartOrFail(); - - if (_nostrClient != null) - return; - SetupNostrClient(); } public void RegisterOKMessageHandler(string eventId, Action action) @@ -53,24 +55,30 @@ public void RegisterOKMessageHandler(string eventId, Action act OkVerificationActions.Add(eventId,action); } - public Task RequestProjectDataAsync(Action responseDataAction,params string[] nostrPubKeys) + public Task LookupProjectsInfoByPubKeysAsync(Action responseDataAction,params string[] nostrPubKeys) { - string subscriptionName = "ProjectInfoLookups"; - _nostrClient.Send(new NostrRequest(subscriptionName, new NostrFilter + const string subscriptionName = "ProjectInfoLookups"; + + if (_nostrClient == null) + throw new InvalidOperationException("The nostr client is null"); + + var request = new NostrRequest(subscriptionName, new NostrFilter { Authors = nostrPubKeys, - Kinds = new[] { NostrKind.ApplicationSpecificData, NostrKind.Metadata, (NostrKind)30402 }, - })); + Kinds = new[] { NostrKind.ApplicationSpecificData }, //, NostrKind.Metadata, (NostrKind)30402 }, + }); + + _nostrClient.Send(request); if (!subscriptions.ContainsKey(subscriptionName)) { var subscription = _nostrClient.Streams.EventStream .Where(_ => _.Subscription == subscriptionName) - .Where(_ => nostrPubKeys.Contains(_.Event.Pubkey)) + //.Where(_ => nostrPubKeys.Contains(_.Event.Pubkey)) .Select(_ => _.Event) .Subscribe(ev => { - responseDataAction(Newtonsoft.Json.JsonConvert.DeserializeObject(ev.Content)); + responseDataAction(Newtonsoft.Json.JsonConvert.DeserializeObject(ev.Content, NostrSerializer.Settings)); }); subscriptions.Add(subscriptionName, subscription); @@ -79,6 +87,29 @@ public Task RequestProjectDataAsync(Action responseDataAction,params strin return Task.CompletedTask; } + public Task RequestProjectEventsoByPubKeyAsync(string nostrPubKey, Action onResponseAction) + { + if (_nostrClient == null) throw new InvalidOperationException("The nostr client is null"); + _nostrClient.Send(new NostrRequest(nostrPubKey, new NostrFilter + { + Authors = new []{nostrPubKey[2..]}, + Kinds = new[] { NostrKind.ApplicationSpecificData, NostrKind.Metadata}, + })); + + if (!subscriptions.ContainsKey(nostrPubKey)) + { + var subscription = _nostrClient.Streams.EventStream + .Where(_ => _.Subscription == nostrPubKey) + .Where(_ => _.Event is not null) + .Select(_ => _.Event) + .Subscribe(onResponseAction!); + + subscriptions.Add(nostrPubKey, subscription); + } + + return Task.CompletedTask; + } + public void CloseConnection() { foreach (var subscription in subscriptions.Values) @@ -87,15 +118,20 @@ public void CloseConnection() } _nostrClient?.Dispose(); _nostrCommunicator?.Dispose(); + _nostrClient = null; + _nostrCommunicator = null; } public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) { - var content = Newtonsoft.Json.JsonConvert.SerializeObject(project); - var key = NostrPrivateKey.FromHex(hexPrivateKey); + + if (!project.NostrPubKey.Contains(key.DerivePublicKey().Hex)) + throw new ArgumentException($"The nostr pub key on the project does not fit the npub calculated from the nsec {project.NostrPubKey} {key.DerivePublicKey().Hex}"); - var signed = GetNip78NostrEvent(project, content) + var content = Newtonsoft.Json.JsonConvert.SerializeObject(project, NostrSerializer.Settings); + + var signed = GetNip78NostrEvent(content) .Sign(key); if (_nostrClient == null) @@ -106,14 +142,55 @@ public Task AddProjectAsync(ProjectInfo project, string hexPrivateKey) return Task.FromResult(signed.Id); } - private static NostrEvent GetNip78NostrEvent(ProjectInfo project, string content) + public Task CreateNostrProfileAsync(NostrMetadata metadata, string hexPrivateKey) + { + var key = NostrPrivateKey.FromHex(hexPrivateKey); + + var content = Newtonsoft.Json.JsonConvert.SerializeObject(metadata, NostrSerializer.Settings); + + var signed = new NostrEvent + { + Kind = NostrKind.Metadata, + CreatedAt = DateTime.UtcNow, + Content = content, + 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")) + }.Sign(key); + + if (_nostrClient == null) + throw new InvalidOperationException(); + + _nostrClient.Send(new NostrEventRequest(signed)); + + return Task.FromResult(signed.Id); + } + + public Task DeleteProjectAsync(string eventId, string hexPrivateKey) + { + var key = NostrPrivateKey.FromHex(hexPrivateKey); + + var deleteEvent = new NostrEvent + { + Kind = NostrKind.EventDeletion, + CreatedAt = DateTime.UtcNow, + Content = "Failed to publish the transaction to the blockchain", + Tags = new NostrEventTags(NostrEventTag.Event(eventId)) + }.Sign(key); + + _nostrClient.Send(deleteEvent); + + return Task.FromResult(deleteEvent.Id); + } + + private static NostrEvent GetNip78NostrEvent( 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"), @@ -129,7 +206,6 @@ private static NostrEvent GetNip99NostrEvent(ProjectInfo project, string content 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 :)"), @@ -181,7 +257,7 @@ private void SetupNostrClient() private void SetupNostrCommunicator() { - _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")) + _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("wss://relay.angor.io")) { Name = "angor-relay.test", ReconnectTimeout = null //TODO need to check what is the actual best time to set here diff --git a/src/Angor/Shared/Services/SignService.cs b/src/Angor/Shared/Services/SignService.cs index 1e5f6a85..46ed249f 100644 --- a/src/Angor/Shared/Services/SignService.cs +++ b/src/Angor/Shared/Services/SignService.cs @@ -1,4 +1,6 @@ -using Angor.Shared.Models; +using System.Net.Http.Json; +using System.Reactive.Linq; +using Angor.Shared.Models; using Microsoft.Extensions.Logging; using Nostr.Client.Client; using Nostr.Client.Communicator; @@ -10,19 +12,22 @@ namespace Angor.Client.Services { public interface ISignService { - Task AddSignKeyAsync(ProjectInfo project, string founderRecoveryPrivateKey); - Task GetInvestmentSigsAsync(SignRecoveryRequest signRecoveryRequest); + Task AddSignKeyAsync(ProjectInfo project, string founderRecoveryPrivateKey, string nostrPrivateKey); + Task RequestInvestmentSigsAsync(SignRecoveryRequest signRecoveryRequest, Action action); } public class SignService : ISignService { + private HttpClient _httpClient; private static INostrClient _nostrClient; private static INostrCommunicator _nostrCommunicator; - public SignService(ILogger _logger) + private List subscriptions = new (); + public SignService(ILogger _logger, HttpClient httpClient) { - _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("ws://angor-relay.test")); + _httpClient = httpClient; + _nostrCommunicator = new NostrWebsocketCommunicator(new Uri("wss://relay.angor.io")); _nostrCommunicator.Name = "angor-relay.test"; _nostrCommunicator.ReconnectTimeout = null; @@ -39,29 +44,61 @@ public SignService(ILogger _logger) _nostrClient = new NostrWebsocketClient(_nostrCommunicator, _logger); } - public async Task AddSignKeyAsync(ProjectInfo project, string founderRecoveryPrivateKey) + public async Task AddSignKeyAsync(ProjectInfo project, string founderRecoveryPrivateKey, string nostrPrivateKey) { - // var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}", new SignData { ProjectIdentifier = project.ProjectIdentifier, FounderRecoveryPrivateKey = founderRecoveryPrivateKey }); - // response.EnsureSuccessStatusCode(); + var response = await _httpClient.PostAsJsonAsync($"/api/TestSign", + new SignData + { + ProjectIdentifier = project.ProjectIdentifier, + FounderRecoveryPrivateKey = founderRecoveryPrivateKey, + NostrPrivateKey = nostrPrivateKey + }); + response.EnsureSuccessStatusCode(); } - public Task GetInvestmentSigsAsync(SignRecoveryRequest signRecoveryRequest) + public Task RequestInvestmentSigsAsync(SignRecoveryRequest signRecoveryRequest, Action action) { var sender = NostrPrivateKey.FromHex(signRecoveryRequest.InvestorNostrPrivateKey); - var receiver = NostrPublicKey.FromHex(signRecoveryRequest.NostrPubKey); - + //var receiver = NostrPublicKey.FromHex(signRecoveryRequest.NostrPubKey); + var ev = new NostrEvent { + Kind = NostrKind.EncryptedDm, CreatedAt = DateTime.UtcNow, - Content = $"Test private message from C# client" + Content = signRecoveryRequest.content, + Tags = new NostrEventTags(new []{NostrEventTag.Profile(signRecoveryRequest.NostrPubKey)}) }; - var encrypted = ev.EncryptDirect(sender, receiver); - var signed = encrypted.Sign(sender); + // Blazor does not support AES so needs to be done manually in the UI + // var encrypted = ev.EncryptDirect(sender, receiver); + // var signed = encrypted.Sign(sender); + + var signed = ev.Sign(sender); + var timeOfMessage = DateTime.UtcNow; _nostrClient.Send(new NostrEventRequest(signed)); - return Task.FromResult(new SignatureInfo()); + var nostrPubKey = sender.DerivePublicKey().Hex; + + _nostrClient.Send(new NostrRequest(nostrPubKey, new NostrFilter + { + Authors = new []{signRecoveryRequest.NostrPubKey}, + P = new []{nostrPubKey}, + Kinds = new[] { NostrKind.EncryptedDm}, + Since = timeOfMessage + })); + + var subscription = _nostrClient.Streams.EventStream + .Where(_ => _.Subscription == nostrPubKey) + .Where(_ => _.Event.Kind == NostrKind.EncryptedDm) + .Subscribe(_ => + { + action.Invoke(_.Event.Content); + }); + + subscriptions.Add(subscription); //TODO dispose of if after the signatures have been received + + return Task.FromResult(signed.Id!); } } }