From 4052aaefc9ec9cb415e2c3ddde87dfad81fbb14c Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 13 Mar 2023 16:37:47 +0100 Subject: [PATCH] imrpovs --- .../BTCPayKeyChain.cs | 4 +- .../BTCPayServer.Plugins.Wabisabi.csproj | 2 +- .../BTCPayWallet.cs | 76 +++--- .../NostrWabiSabiApiClient.cs | 240 ++++++++++++++++++ .../NostrWabisabiApiServer.cs | 228 +++++++++++++++++ .../Smartifier.cs | 9 +- .../Shared/Wabisabi/WabisabiDashboard.cshtml | 20 +- .../UpdateWabisabiStoreSettings.cshtml | 18 +- .../WabisabiPlugin.cs | 4 +- .../WalletProvider.cs | 123 +++++---- submodules/walletwasabi | 2 +- 11 files changed, 592 insertions(+), 134 deletions(-) create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabiSabiApiClient.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabisabiApiServer.cs diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayKeyChain.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayKeyChain.cs index e99826d..ee22d3e 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayKeyChain.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayKeyChain.cs @@ -13,6 +13,7 @@ namespace BTCPayServer.Plugins.Wabisabi; public class BTCPayKeyChain : IKeyChain { + public Smartifier Smartifier { get; } private readonly ExplorerClient _explorerClient; private readonly DerivationStrategyBase _derivationStrategy; private readonly ExtKey _masterKey; @@ -21,8 +22,9 @@ public class BTCPayKeyChain : IKeyChain public bool KeysAvailable => _masterKey is not null && _accountKey is not null; public BTCPayKeyChain(ExplorerClient explorerClient, DerivationStrategyBase derivationStrategy, ExtKey masterKey, - ExtKey accountKey) + ExtKey accountKey, Smartifier smartifier) { + Smartifier = smartifier; _explorerClient = explorerClient; _derivationStrategy = derivationStrategy; _masterKey = masterKey; diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj index 44bc408..5407318 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj @@ -43,7 +43,7 @@ - + diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs index 782fd64..9cd36c0 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs @@ -44,10 +44,10 @@ public class BTCPayWallet : IWallet, IDestinationProvider private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly Services.Wallets.BTCPayWallet _btcPayWallet; private readonly PullPaymentHostedService _pullPaymentHostedService; - public OnChainPaymentMethodData OnChainPaymentMethodData; + // public OnChainPaymentMethodData OnChainPaymentMethodData; public readonly DerivationStrategyBase DerivationScheme; public readonly ExplorerClient ExplorerClient; - public readonly IBTCPayServerClientFactory BtcPayServerClientFactory; + // public readonly IBTCPayServerClientFactory BtcPayServerClientFactory; public WabisabiStoreSettings WabisabiStoreSettings; public readonly IUTXOLocker UtxoLocker; public readonly ILogger Logger; @@ -59,16 +59,13 @@ public class BTCPayWallet : IWallet, IDestinationProvider BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, Services.Wallets.BTCPayWallet btcPayWallet, PullPaymentHostedService pullPaymentHostedService, - OnChainPaymentMethodData onChainPaymentMethodData, DerivationStrategyBase derivationScheme, ExplorerClient explorerClient, BTCPayKeyChain keyChain, - IBTCPayServerClientFactory btcPayServerClientFactory, string storeId, WabisabiStoreSettings wabisabiStoreSettings, IUTXOLocker utxoLocker, ILoggerFactory loggerFactory, - Smartifier smartifier, StoreRepository storeRepository, ConcurrentDictionary> bannedCoins, EventAggregator eventAggregator) { @@ -79,14 +76,11 @@ public class BTCPayWallet : IWallet, IDestinationProvider _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _btcPayWallet = btcPayWallet; _pullPaymentHostedService = pullPaymentHostedService; - OnChainPaymentMethodData = onChainPaymentMethodData; DerivationScheme = derivationScheme; ExplorerClient = explorerClient; - BtcPayServerClientFactory = btcPayServerClientFactory; StoreId = storeId; WabisabiStoreSettings = wabisabiStoreSettings; UtxoLocker = utxoLocker; - _smartifier = smartifier; _storeRepository = storeRepository; _bannedCoins = bannedCoins; _eventAggregator = eventAggregator; @@ -101,8 +95,9 @@ public class BTCPayWallet : IWallet, IDestinationProvider bool IWallet.IsMixable(string coordinator) { - return OnChainPaymentMethodData?.Enabled is true && WabisabiStoreSettings.Settings.SingleOrDefault(settings => - settings.Coordinator.Equals(coordinator))?.Enabled is true && ((BTCPayKeyChain)KeyChain).KeysAvailable; + return KeyChain is BTCPayKeyChain {KeysAvailable: true} && WabisabiStoreSettings.Settings.SingleOrDefault( + settings => + settings.Coordinator.Equals(coordinator))?.Enabled is true; } public IKeyChain KeyChain { get; } @@ -152,7 +147,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider } private IRoundCoinSelector _coinSelector; - public readonly Smartifier _smartifier; + public Smartifier _smartifier => (KeyChain as BTCPayKeyChain)?.Smartifier; private readonly StoreRepository _storeRepository; private readonly ConcurrentDictionary> _bannedCoins; private readonly EventAggregator _eventAggregator; @@ -352,7 +347,13 @@ public class BTCPayWallet : IWallet, IDestinationProvider public async Task RegisterCoinjoinTransaction(SuccessfulCoinJoinResult result, string coordinatorName) { await _savingProgress; + + + + _savingProgress = RegisterCoinjoinTransactionInternal(result, coordinatorName); + + await _savingProgress; } private async Task RegisterCoinjoinTransactionInternal(SuccessfulCoinJoinResult result, string coordinatorName) @@ -378,10 +379,10 @@ public class BTCPayWallet : IWallet, IDestinationProvider Dictionary indexToPayment = new(); - foreach (var script in result.OutputScripts) + foreach (var script in result.Outputs) { var txout = result.UnsignedCoinJoin.Outputs.AsIndexedOutputs() - .Single(@out => @out.TxOut.ScriptPubKey == script); + .Single(@out => @out.TxOut.ScriptPubKey == script.ScriptPubKey && @out.TxOut.Value == script.Value); //this was not a mix to self, but rather a payment @@ -393,7 +394,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider continue; } - scriptInfos.Add((txout, ExplorerClient.GetKeyInformationAsync(BlockchainAnalyzer.StdDenoms.Contains(txout.TxOut.Value)?utxoDerivationScheme:DerivationScheme, script))); + scriptInfos.Add((txout, ExplorerClient.GetKeyInformationAsync(BlockchainAnalyzer.StdDenoms.Contains(txout.TxOut.Value)?utxoDerivationScheme:DerivationScheme, script.ScriptPubKey))); } await Task.WhenAll(scriptInfos.Select(t => t.Item2)); @@ -405,9 +406,9 @@ public class BTCPayWallet : IWallet, IDestinationProvider coin.SpenderTransaction = smartTx; smartTx.TryAddWalletInput(coin); }); - result.OutputScripts.ForEach(s => + result.Outputs.ForEach(s => { - if (scriptInfos2.TryGetValue(s, out var si)) + if (scriptInfos2.TryGetValue(s.ScriptPubKey, out var si)) { var derivation = DerivationScheme.GetChild(si.Item2.Result.KeyPath).GetExtPubKeys().First() .PubKey; @@ -582,22 +583,22 @@ public class BTCPayWallet : IWallet, IDestinationProvider } - public async Task UnlockUTXOs() - { - var client = await BtcPayServerClientFactory.Create(null, StoreId); - var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC"); - var unlocked = new List(); - foreach (OnChainWalletUTXOData utxo in utxos) - { - - if (await UtxoLocker.TryUnlock(utxo.Outpoint)) - { - unlocked.Add(utxo.Outpoint.ToString()); - } - } - - Logger.LogTrace($"unlocked utxos: {string.Join(',', unlocked)}"); - } + // public async Task UnlockUTXOs() + // { + // var client = await BtcPayServerClientFactory.Create(null, StoreId); + // var utxos = await client.GetOnChainWalletUTXOs(StoreId, "BTC"); + // var unlocked = new List(); + // foreach (OnChainWalletUTXOData utxo in utxos) + // { + // + // if (await UtxoLocker.TryUnlock(utxo.Outpoint)) + // { + // unlocked.Add(utxo.Outpoint.ToString()); + // } + // } + // + // Logger.LogTrace($"unlocked utxos: {string.Join(',', unlocked)}"); + // } public async Task> GetNextDestinationsAsync(int count, bool mixedOutputs) { @@ -605,15 +606,14 @@ public async Task> GetNextDestinationsAsync(int count, { try { - var mixClient = await BtcPayServerClientFactory.Create(null, WabisabiStoreSettings.MixToOtherWallet); - var pm = await mixClient.GetStoreOnChainPaymentMethod(WabisabiStoreSettings.MixToOtherWallet, - "BTC"); + var mixStore = await _storeRepository.FindStore(WabisabiStoreSettings.MixToOtherWallet); + var pm = mixStore.GetDerivationSchemeSettings(_btcPayNetworkProvider, "BTC"); - var deriv = ExplorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme); - if (deriv.ScriptPubKeyType() == DerivationScheme.ScriptPubKeyType()) + + if (pm?.AccountDerivation?.ScriptPubKeyType() == DerivationScheme.ScriptPubKeyType()) { return await Task.WhenAll(Enumerable.Repeat(0, count).Select(_ => - _btcPayWallet.ReserveAddressAsync(WabisabiStoreSettings.MixToOtherWallet, deriv, "coinjoin"))).ContinueWith(task => task.Result.Select(information => information.Address)); + _btcPayWallet.ReserveAddressAsync(WabisabiStoreSettings.MixToOtherWallet, pm.AccountDerivation, "coinjoin"))).ContinueWith(task => task.Result.Select(information => information.Address)); } } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabiSabiApiClient.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabiSabiApiClient.cs new file mode 100644 index 0000000..c148359 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabiSabiApiClient.cs @@ -0,0 +1,240 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using NBitcoin; +using NBitcoin.Secp256k1; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NNostr.Client; +using WalletWasabi.Logging; +using WalletWasabi.WabiSabi; +using WalletWasabi.WabiSabi.Backend.Models; +using WalletWasabi.WabiSabi.Backend.PostRequests; +using WalletWasabi.WabiSabi.Models; +using WalletWasabi.WabiSabi.Models.Serialization; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class NostrWabiSabiApiClient : IWabiSabiApiRequestHandler, IHostedService +{ + public static int RoundStateKind = 15750; + public static int CommunicationKind = 25750; + private readonly NostrClient _client; + private readonly ECXOnlyPubKey _coordinatorKey; + private string _coordinatorKeyHex => _coordinatorKey.ToHex(); + private readonly string _coordinatorFilterId; + + public NostrWabiSabiApiClient(NostrClient client, ECXOnlyPubKey coordinatorKey) + { + _client = client; + _coordinatorKey = coordinatorKey; + _coordinatorFilterId = new Guid().ToString(); + } + + + public async Task StartAsync(CancellationToken cancellationToken) + { + _ = _client.ListenForMessages(); + var filter = new NostrSubscriptionFilter() + { + Authors = new[] {_coordinatorKey.ToHex()}, + Kinds = new[] {RoundStateKind, CommunicationKind}, + Since = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(1)) + }; + await _client.CreateSubscription(_coordinatorFilterId, new[] {filter}, cancellationToken); + _client.EventsReceived += EventsReceived; + + await _client.ConnectAndWaitUntilConnected(cancellationToken); + } + + private RoundStateResponse _lastRoundState { get; set; } + private TaskCompletionSource _lastRoundStateTask = new(); + + + private void EventsReceived(object sender, (string subscriptionId, NostrEvent[] events) e) + { + if (e.subscriptionId == _coordinatorFilterId) + { + var roundState = e.events.Where(evt => evt.Kind == RoundStateKind).MaxBy(@event => @event.CreatedAt); + if (roundState != null) + { + _lastRoundState = Deserialize(roundState.Content); + _lastRoundStateTask.TrySetResult(); + } + } + } + + private async Task SendAndWaitForReply(RemoteAction action, TRequest request, + CancellationToken cancellationToken) + { + await SendAndWaitForReply(action, request, cancellationToken); + } + + + private async Task SendAndWaitForReply(RemoteAction action, TRequest request, + CancellationToken cancellationToken) + { + var newKey = ECPrivKey.Create(RandomUtils.GetBytes(32)); + var pubkey = newKey.CreateXOnlyPubKey(); + var evt = new NostrEvent() + { + Content = Serialize(new + { + Action = action, + Request = request + }), + PublicKey = pubkey.ToHex(), + Kind = 4, + CreatedAt = DateTimeOffset.Now + }; + evt.SetTag("p", _coordinatorKeyHex); + + await evt.EncryptNip04EventAsync(newKey); + evt.Kind = CommunicationKind; + await evt.ComputeIdAndSignAsync(newKey); + var tcs = new TaskCompletionSource(cancellationToken); + + void OnClientEventsReceived(object sender, (string subscriptionId, NostrEvent[] events) e) + { + foreach (var nostrEvent in e.events) + { + if (nostrEvent.PublicKey != _coordinatorKeyHex) continue; + var replyToEvent = evt.GetTaggedData("e"); + var replyToUser = evt.GetTaggedData("p"); + if (replyToEvent.All(s => s != evt.Id) || replyToUser.All(s => s != evt.PublicKey)) continue; + if (!nostrEvent.Verify()) continue; + _client.EventsReceived -= OnClientEventsReceived; + tcs.TrySetResult(nostrEvent); + break; + } + } + + _client.EventsReceived += OnClientEventsReceived; + try + { + var replyEvent = await tcs.Task; + replyEvent.Kind = 4; + var response = await replyEvent.DecryptNip04EventAsync(newKey); + var jobj = JObject.Parse(response); + if (jobj.TryGetValue("error", out var errorJson)) + { + var contentString = errorJson.Value(); + var error = JsonConvert.DeserializeObject(contentString, new JsonSerializerSettings() + { + Converters = JsonSerializationOptions.Default.Settings.Converters, + Error = (_, e) => e.ErrorContext.Handled = true // Try to deserialize an Error object + }); + var innerException = error switch + { + {Type: ProtocolConstants.ProtocolViolationType} => Enum.TryParse( + error.ErrorCode, out var code) + ? new WabiSabiProtocolException(code, error.Description, exceptionData: error.ExceptionData) + : new NotSupportedException( + $"Received WabiSabi protocol exception with unknown '{error.ErrorCode}' error code.\n\tDescription: '{error.Description}'."), + {Type: "unknown"} => new Exception(error.Description), + _ => null + }; + + if (innerException is not null) + { + throw new HttpRequestException("Remote coordinator responded with an error.", innerException); + } + + // Remove " from beginning and end to ensure backwards compatibility and it's kind of trash, too. + if (contentString.Count(f => f == '"') <= 2) + { + contentString = contentString.Trim('"'); + } + + var errorMessage = string.Empty; + if (!string.IsNullOrWhiteSpace(contentString)) + { + errorMessage = $"\n{contentString}"; + } + + + throw new HttpRequestException($"ERROR:{errorMessage}"); + } + + return jobj.ToObject(JsonSerializer.Create(JsonSerializationOptions.Default.Settings)); + } + catch (OperationCanceledException e) + { + _client.EventsReceived -= OnClientEventsReceived; + throw; + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _client.CloseSubscription(_coordinatorFilterId, cancellationToken); + _client.EventsReceived -= EventsReceived; + } + + public async Task GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken) + { + await _lastRoundStateTask.Task; + return _lastRoundState; + } + + public Task RegisterInputAsync(InputRegistrationRequest request, + CancellationToken cancellationToken) => + SendAndWaitForReply(RemoteAction.RegisterInput, request, + cancellationToken); + + public Task ConfirmConnectionAsync(ConnectionConfirmationRequest request, + CancellationToken cancellationToken) => + SendAndWaitForReply( + RemoteAction.ConfirmConnection, request, cancellationToken); + + public Task RegisterOutputAsync(OutputRegistrationRequest request, CancellationToken cancellationToken) => + SendAndWaitForReply(RemoteAction.RegisterOutput, request, cancellationToken); + + public Task ReissuanceAsync(ReissueCredentialRequest request, + CancellationToken cancellationToken) => + SendAndWaitForReply(RemoteAction.ReissueCredential, + request, cancellationToken); + + public Task RemoveInputAsync(InputsRemovalRequest request, CancellationToken cancellationToken) => + SendAndWaitForReply(RemoteAction.RemoveInput, request, cancellationToken); + + public virtual Task SignTransactionAsync(TransactionSignaturesRequest request, + CancellationToken cancellationToken) => + SendAndWaitForReply(RemoteAction.SignTransaction, request, cancellationToken); + + public Task ReadyToSignAsync(ReadyToSignRequestRequest request, CancellationToken cancellationToken) => + SendAndWaitForReply(RemoteAction.ReadyToSign, request, cancellationToken); + + + private static string Serialize(T obj) + => JsonConvert.SerializeObject(obj, JsonSerializationOptions.Default.Settings); + + private static TResponse Deserialize(string jsonString) + { + try + { + return JsonConvert.DeserializeObject(jsonString, JsonSerializationOptions.Default.Settings) + ?? throw new InvalidOperationException("Deserialization error"); + } + catch + { + Logger.LogDebug($"Failed to deserialize {typeof(TResponse)} from JSON '{jsonString}'"); + throw; + } + } + + private enum RemoteAction + { + RegisterInput, + RemoveInput, + ConfirmConnection, + RegisterOutput, + ReissueCredential, + SignTransaction, + GetStatus, + ReadyToSign + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabisabiApiServer.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabisabiApiServer.cs new file mode 100644 index 0000000..1ecdf22 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/NostrWabisabiApiServer.cs @@ -0,0 +1,228 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using NBitcoin; +using NBitcoin.Secp256k1; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NNostr.Client; +using WalletWasabi.Logging; +using WalletWasabi.WabiSabi; +using WalletWasabi.WabiSabi.Backend.Models; +using WalletWasabi.WabiSabi.Backend.Rounds; +using WalletWasabi.WabiSabi.Crypto; +using WalletWasabi.WabiSabi.Models; +using WalletWasabi.WabiSabi.Models.Serialization; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class NostrWabisabiApiServer: IHostedService +{ + public static int RoundStateKind = 15750; + public static int CommunicationKind = 25750; + private readonly Arena _arena; + private readonly NostrClient _client; + private readonly ECPrivKey _coordinatorKey; + private string _coordinatorKeyHex => _coordinatorKey.CreateXOnlyPubKey().ToHex(); + private readonly string _coordinatorFilterId; + + private Channel PendingEvents { get; } = Channel.CreateUnbounded(); + public NostrWabisabiApiServer(Arena arena,NostrClient client, ECPrivKey coordinatorKey) + { + _arena = arena; + _client = client; + _coordinatorKey = coordinatorKey; + _coordinatorFilterId = new Guid().ToString(); + _serializer = JsonSerializer.Create(JsonSerializationOptions.Default.Settings); + } + + + public async Task StartAsync(CancellationToken cancellationToken) + { + _ = _client.ListenForMessages(); + var filter = new NostrSubscriptionFilter() + { + PublicKey = new[] {_coordinatorKey.ToHex()}, + Kinds = new[] { CommunicationKind}, + Since = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(1)) + }; + await _client.CreateSubscription(_coordinatorFilterId, new[] {filter}, cancellationToken); + _client.EventsReceived += EventsReceived; + + await _client.ConnectAndWaitUntilConnected(cancellationToken); + _ = RoutinelyUpdateRoundEvent(cancellationToken); + _ = ProcessRequests(cancellationToken); + + } + + private void EventsReceived(object sender, (string subscriptionId, NostrEvent[] events) e) + { + if (e.subscriptionId != _coordinatorFilterId) return; + var requests = e.events.Where(evt => + evt.Kind == CommunicationKind && + evt.GetTaggedData("p").Any(s => s == _coordinatorKeyHex) && evt.Verify()); + foreach (var request in requests) + PendingEvents.Writer.TryWrite(request); + } + + + private async Task RoutinelyUpdateRoundEvent(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var response = await _arena.GetStatusAsync(RoundStateRequest.Empty, cancellationToken); + var nostrEvent = new NostrEvent() + { + Kind = RoundStateKind, + PublicKey = _coordinatorKeyHex, + CreatedAt = DateTimeOffset.Now, + Content = Serialize(response) + }; + await _client.PublishEvent(nostrEvent, cancellationToken); + await Task.Delay(1000, cancellationToken); + } + } + + private async Task ProcessRequests(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && + await PendingEvents.Reader.WaitToReadAsync(cancellationToken) && + PendingEvents.Reader.TryRead(out var evt)) + { + evt.Kind = 4; + var content = JObject.Parse(await evt.DecryptNip04EventAsync(_coordinatorKey)); + if (content.TryGetValue("action", out var actionJson) && + actionJson.Value(actionJson) is { } actionString && + Enum.TryParse(actionString, out var action) && + content.ContainsKey("request")) + { + try + { + switch (action) + { + case RemoteAction.GetStatus: + // ignored as we use a dedicated public event for this to not spam + break; + case RemoteAction.RegisterInput: + var registerInputRequest = + content["request"].ToObject(_serializer); + var registerInputResponse = + await _arena.RegisterInputAsync(registerInputRequest, CancellationToken.None); + await Reply(evt, registerInputResponse, CancellationToken.None); + break; + case RemoteAction.RegisterOutput: + var registerOutputRequest = + content["request"].ToObject(_serializer); + await _arena.RegisterOutputAsync(registerOutputRequest, CancellationToken.None); + break; + case RemoteAction.RemoveInput: + var removeInputRequest = content["request"].ToObject(_serializer); + await _arena.RemoveInputAsync(removeInputRequest, CancellationToken.None); + break; + case RemoteAction.ConfirmConnection: + var connectionConfirmationRequest = + content["request"].ToObject(_serializer); + var connectionConfirmationResponse = + await _arena.ConfirmConnectionAsync(connectionConfirmationRequest, + CancellationToken.None); + await Reply(evt, connectionConfirmationResponse, CancellationToken.None); + break; + case RemoteAction.ReissueCredential: + var reissueCredentialRequest = + content["request"].ToObject(_serializer); + var reissueCredentialResponse = + await _arena.ReissuanceAsync(reissueCredentialRequest, CancellationToken.None); + await Reply(evt, reissueCredentialResponse, CancellationToken.None); + break; + case RemoteAction.SignTransaction: + var transactionSignaturesRequest = + content["request"].ToObject(_serializer); + await _arena.SignTransactionAsync(transactionSignaturesRequest, CancellationToken.None); + break; + case RemoteAction.ReadyToSign: + var readyToSignRequest = + content["request"].ToObject(_serializer); + await _arena.ReadyToSignAsync(readyToSignRequest, CancellationToken.None); + break; + } + } + catch (Exception ex) + { + object response = ex switch + { + WabiSabiProtocolException wabiSabiProtocolException => new + { + Error = new Error(Type: ProtocolConstants.ProtocolViolationType, + ErrorCode: wabiSabiProtocolException.ErrorCode.ToString(), + Description: wabiSabiProtocolException.Message, + ExceptionData: wabiSabiProtocolException.ExceptionData ?? + EmptyExceptionData.Instance) + }, + WabiSabiCryptoException wabiSabiCryptoException => new + { + Error = new Error(Type: ProtocolConstants.ProtocolViolationType, + ErrorCode: WabiSabiProtocolErrorCode.CryptoException.ToString(), + Description: wabiSabiCryptoException.Message, + ExceptionData: EmptyExceptionData.Instance) + }, + _ => new + { + Error = new Error(Type: "unknown", ErrorCode: ex.GetType().Name, + Description: ex.Message, ExceptionData: EmptyExceptionData.Instance) + } + }; + + await Reply(evt, response, CancellationToken.None); + } + } + + } + } + + private readonly JsonSerializer _serializer; + + + + private async Task Reply(NostrEvent originaltEvent,TResponse response, + CancellationToken cancellationToken) + { + var evt = new NostrEvent() + { + Content = Serialize(response), + PublicKey = _coordinatorKeyHex, + Kind = 4, + CreatedAt = DateTimeOffset.Now + }; + evt.SetTag("p", originaltEvent.PublicKey); + evt.SetTag("e", originaltEvent.Id); + + await evt.EncryptNip04EventAsync(_coordinatorKey); + evt.Kind = CommunicationKind; + await evt.ComputeIdAndSignAsync(_coordinatorKey); + await _client.PublishEvent(evt, cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _client.CloseSubscription(_coordinatorFilterId, cancellationToken); + _client.EventsReceived -= EventsReceived; + } + + private static string Serialize(T obj) + => JsonConvert.SerializeObject(obj, JsonSerializationOptions.Default.Settings); + private enum RemoteAction + { + RegisterInput, + RemoveInput, + ConfirmConnection, + RegisterOutput, + ReissueCredential, + SignTransaction, + GetStatus, + ReadyToSign + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs index 48ca7cb..3cd37f7 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs @@ -31,21 +31,20 @@ public class Smartifier public Smartifier( WalletRepository walletRepository, ExplorerClient explorerClient, DerivationStrategyBase derivationStrategyBase, string storeId, - IUTXOLocker utxoLocker) + IUTXOLocker utxoLocker, RootedKeyPath accountKeyPath) { _walletRepository = walletRepository; _explorerClient = explorerClient; DerivationScheme = derivationStrategyBase; _storeId = storeId; _utxoLocker = utxoLocker; - _accountKeyPath = _explorerClient.GetMetadataAsync(DerivationScheme, - WellknownMetadataKeys.AccountKeyPath); + _accountKeyPath = accountKeyPath; } public readonly ConcurrentDictionary> CachedTransactions = new(); public readonly ConcurrentDictionary> Transactions = new(); public readonly ConcurrentDictionary> Coins = new(); - private readonly Task _accountKeyPath; + private readonly RootedKeyPath _accountKeyPath; public async Task LoadCoins(List coins, int current , Dictionary labels, double anonset, BTCPayWallet.CoinjoinData coinjoinData)> utxoLabels) @@ -150,7 +149,7 @@ public class Smartifier utxoLabels.TryGetValue(coin.OutPoint, out var labels); var unsmartTx = await CachedTransactions[coin.OutPoint.Hash]; var pubKey = DerivationScheme.GetChild(coin.KeyPath).GetExtPubKeys().First().PubKey; - var kp = (await _accountKeyPath).Derive(coin.KeyPath).KeyPath; + var kp = _accountKeyPath.Derive(coin.KeyPath).KeyPath; var hdPubKey = new HdPubKey(pubKey, kp, new SmartLabel(labels.labels ?? new HashSet()), current == 1 ? KeyState.Clean : KeyState.Used); diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml index 0e24be1..7ac8415 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml @@ -29,11 +29,6 @@ return; } var storeId = ScopeProvider.GetCurrentStoreId(); - // var methods = await Client.GetStoreOnChainPaymentMethods(storeId, true); - // var method = methods.FirstOrDefault(data => data.CryptoCode == "BTC"); - // var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32); - // contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'"); - // contentSecurityPolicies.AllowUnsafeHashes(); } @if (available) @@ -87,7 +82,7 @@ } - var wallet = (BTCPayWallet) await WalletProvider.GetWalletAsync(storeId); + var wallet = (BTCPayWallet?) await WalletProvider.GetWalletAsync(storeId); var coins = await wallet.GetAllCoins(); var privacy = wallet.GetPrivacyPercentage(coins, wallet.AnonScoreTarget); @@ -96,20 +91,13 @@ var colorCoins = coins.GroupBy(coin => coin.CoinColor(wallet.AnonScoreTarget)).ToDictionary(grouping => grouping.Key, grouping => grouping);
- @if (wallet is BTCPayWallet btcPayWallet) + @if (wallet is { }) { - @if (btcPayWallet.OnChainPaymentMethodData?.Enabled is not true) + @if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable) { - } - else if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable) - { - } } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml index 03edfcf..d62a858 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml @@ -53,24 +53,14 @@ } var wallet = await WalletProvider.GetWalletAsync(storeId); - if (wallet is BTCPayWallet btcPayWallet) + if (wallet is BTCPayWallet) { - @if (btcPayWallet.OnChainPaymentMethodData?.Enabled is not true) + + @if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable) { - } - else if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable) - { - } } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs index 0e04641..973ebb9 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs @@ -62,12 +62,12 @@ public class WabisabiPlugin : BaseBTCPayServerPlugin applicationBuilder.AddSingleton(provider => new( provider, provider.GetRequiredService(), - provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), utxoLocker, provider.GetRequiredService(), - provider.GetRequiredService>() + provider.GetRequiredService>(), + provider.GetRequiredService() )); applicationBuilder.AddWabisabiCoordinator(); applicationBuilder.AddSingleton(provider => provider.GetRequiredService()); diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs index c863447..59ad6fd 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs @@ -18,13 +18,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NBitcoin; using NBXplorer; -using NBXplorer.Models; using WalletWasabi.Bases; -using WalletWasabi.Blockchain.Analysis; -using WalletWasabi.Blockchain.TransactionOutputs; using WalletWasabi.WabiSabi.Client; using WalletWasabi.Wallets; -using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest; using PayoutData = BTCPayServer.Data.PayoutData; namespace BTCPayServer.Plugins.Wabisabi; @@ -34,31 +30,31 @@ public class WalletProvider : PeriodicRunner,IWalletProvider private Dictionary? _cachedSettings; private readonly IServiceProvider _serviceProvider; private readonly StoreRepository _storeRepository; - private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; private readonly IExplorerClientProvider _explorerClientProvider; public IUTXOLocker UtxoLocker { get; set; } private readonly ILoggerFactory _loggerFactory; private readonly EventAggregator _eventAggregator; private readonly ILogger _logger; + private readonly BTCPayNetworkProvider _networkProvider; public WalletProvider( IServiceProvider serviceProvider, StoreRepository storeRepository, - IBTCPayServerClientFactory btcPayServerClientFactory, IExplorerClientProvider explorerClientProvider, ILoggerFactory loggerFactory, IUTXOLocker utxoLocker, EventAggregator eventAggregator, - ILogger logger) : base(TimeSpan.FromMinutes(5)) + ILogger logger, + BTCPayNetworkProvider networkProvider) : base(TimeSpan.FromMinutes(5)) { UtxoLocker = utxoLocker; _serviceProvider = serviceProvider; _storeRepository = storeRepository; - _btcPayServerClientFactory = btcPayServerClientFactory; _explorerClientProvider = explorerClientProvider; _loggerFactory = loggerFactory; _eventAggregator = eventAggregator; _logger = logger; + _networkProvider = networkProvider; } public readonly ConcurrentDictionary> LoadedWallets = new(); @@ -76,43 +72,59 @@ public class WalletProvider : PeriodicRunner,IWalletProvider } public event EventHandler? WalletUnloaded; - public async Task GetWalletAsync(string name) + public async Task GetWalletAsync(string name) { await initialLoad.Task; return await LoadedWallets.GetOrAddAsync(name, async s => { - if (!_cachedSettings.TryGetValue(name, out var wabisabiStoreSettings)) { return null; } - - var client = await _btcPayServerClientFactory.Create(null, name); - var pm = await client.GetStoreOnChainPaymentMethod(name, "BTC"); + var store = await _storeRepository.FindStore(name); + var paymentMethod = store?.GetDerivationSchemeSettings(_networkProvider, "BTC"); + if (paymentMethod is null) + { + return null; + } + var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); - var derivationStrategy = - explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme); + var isHotWallet = paymentMethod.IsHotWallet; + var enabled = store.GetEnabledPaymentIds(_networkProvider).Contains(paymentMethod.PaymentId); + var derivationStrategy = paymentMethod.AccountDerivation; + BTCPayKeyChain keychain; + if (isHotWallet && enabled) + { + var masterKey = await explorerClient.GetMetadataAsync(derivationStrategy, + WellknownMetadataKeys.MasterHDKey); + var accountKey = await explorerClient.GetMetadataAsync(derivationStrategy, + WellknownMetadataKeys.AccountHDKey); + var accountKeyPath = await explorerClient.GetMetadataAsync(derivationStrategy, + WellknownMetadataKeys.AccountKeyPath); - var masterKey = await explorerClient.GetMetadataAsync(derivationStrategy, - WellknownMetadataKeys.MasterHDKey); - var accountKey = await explorerClient.GetMetadataAsync(derivationStrategy, - WellknownMetadataKeys.AccountHDKey); + if (masterKey is null || accountKey is null || accountKeyPath is null) + { + + keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, null); + }else + keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey, new Smartifier(_serviceProvider.GetRequiredService(),explorerClient, derivationStrategy, name, UtxoLocker, accountKeyPath)); - var keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey); + } + else + { + keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, null, null, null); + } - var smartifier = new Smartifier(_serviceProvider.GetRequiredService(),explorerClient, derivationStrategy, name, UtxoLocker); - return (IWallet)new BTCPayWallet( _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService().GetWallet("BTC"), - _serviceProvider.GetRequiredService(), - pm, derivationStrategy, explorerClient, keychain, - _btcPayServerClientFactory, name, wabisabiStoreSettings, UtxoLocker, - _loggerFactory, smartifier, + _serviceProvider.GetRequiredService(),derivationStrategy, explorerClient, keychain, + name, wabisabiStoreSettings, UtxoLocker, + _loggerFactory, _serviceProvider.GetRequiredService(), BannedCoins, _eventAggregator); @@ -121,8 +133,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider } private TaskCompletionSource initialLoad = new(); - private IEventAggregatorSubscription _subscription; - private IEventAggregatorSubscription _subscription2; + private CompositeDisposable _disposables = new(); public async Task> GetWalletsAsync() { @@ -186,38 +197,25 @@ public class WalletProvider : PeriodicRunner,IWalletProvider protected override async Task ActionAsync(CancellationToken cancel) { - var toCheck = LoadedWallets.Keys.ToList(); - while (toCheck.Any()) - { - var storeid = toCheck.First(); - await Check(storeid, cancel); - toCheck.Remove(storeid); - } + // var toCheck = LoadedWallets.Keys.ToList(); + // while (toCheck.Any()) + // { + // var storeid = toCheck.First(); + // await Check(storeid, cancel); + // toCheck.Remove(storeid); + // } } public async Task Check(string storeId, CancellationToken cancellationToken) { - var client = await _btcPayServerClientFactory.Create(null, storeId); try { if (LoadedWallets.TryGetValue(storeId, out var currentWallet)) { - var wallet = (BTCPayWallet)await currentWallet; - var kc = (BTCPayKeyChain)wallet.KeyChain; - var pm = await client.GetStoreOnChainPaymentMethod(storeId, "BTC", cancellationToken); - if (pm.DerivationScheme != wallet.OnChainPaymentMethodData.DerivationScheme) - { - await UnloadWallet(storeId); - } - else - { - wallet.OnChainPaymentMethodData = pm; - } - - if (!kc.KeysAvailable) - { - await UnloadWallet(storeId); - } + await UnloadWallet(storeId); + if(_cachedSettings.TryGetValue(storeId , out var settings) && settings.Settings.Any(coordinatorSettings => coordinatorSettings.Enabled)) + await GetWalletAsync(storeId); + await GetWalletAsync(storeId); } } catch (Exception e) @@ -291,15 +289,28 @@ public class WalletProvider : PeriodicRunner,IWalletProvider await _storeRepository.GetSettingsAsync(nameof(WabisabiStoreSettings)); initialLoad.SetResult(); }, cancellationToken); - _subscription = _eventAggregator.SubscribeAsync(@event => - Check(@event.WalletId.StoreId, cancellationToken)); + _disposables.Add(_eventAggregator.SubscribeAsync(async @event => + { + await initialLoad.Task; + await UnloadWallet(@event.StoreId); + + })); + _disposables.Add(_eventAggregator.SubscribeAsync(async @event => + { + if (@event.WalletId.CryptoCode == "BTC") + { + await initialLoad.Task; + await Check(@event.WalletId.StoreId, cancellationToken); + } + + })); + return base.StartAsync(cancellationToken); } public override Task StopAsync(CancellationToken cancellationToken) { - _subscription?.Dispose(); - _subscription2?.Dispose(); + _disposables?.Dispose(); return base.StopAsync(cancellationToken); } } diff --git a/submodules/walletwasabi b/submodules/walletwasabi index 3a51a87..fdfddd8 160000 --- a/submodules/walletwasabi +++ b/submodules/walletwasabi @@ -1 +1 @@ -Subproject commit 3a51a875421706a8780645b65564967d6d88cc1a +Subproject commit fdfddd858323954a25911fff53a5b159fd2538d5