From e3a1eed8b32044bc924e8df2e442322a82f97c1e Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 8 Jan 2018 02:36:41 +0900 Subject: [PATCH] Use Websocket for blockchain notifications --- BTCPayServer.Tests/BTCPayServerTester.cs | 6 - BTCPayServer.Tests/UnitTest1.cs | 13 +- BTCPayServer.Tests/docker-compose.yml | 2 +- BTCPayServer/BTCPayNetwork.cs | 1 - BTCPayServer/BTCPayNetworkProvider.cs | 9 +- BTCPayServer/BTCPayServer.csproj | 2 +- .../Configuration/BTCPayServerOptions.cs | 42 +++-- .../Configuration/DefaultConfiguration.cs | 15 +- .../Configuration/NetworkInformation.cs | 24 +-- BTCPayServer/Controllers/AccountController.cs | 1 - .../InvoiceController.PaymentProtocol.cs | 8 +- .../Controllers/InvoiceController.UI.cs | 4 +- BTCPayServer/Controllers/InvoiceController.cs | 12 +- BTCPayServer/Events/InvoiceCreatedEvent.cs | 22 +++ .../Events/NBXplorerStateChangedEvent.cs | 7 +- BTCPayServer/Events/TxOutReceivedEvent.cs | 4 +- BTCPayServer/ExplorerClientProvider.cs | 56 +++++++ BTCPayServer/Extensions.cs | 13 +- .../InvoiceNotificationManager.cs | 3 +- .../InvoiceWatcher.cs | 29 ++-- .../HostedServices/NBXplorerListener.cs | 153 ++++++++++++++++++ .../{ => HostedServices}/NBXplorerWaiter.cs | 41 +++-- BTCPayServer/Hosting/BTCPayServerServices.cs | 23 +-- .../Services/Fees/NBxplorerFeeProvider.cs | 33 ++-- BTCPayServer/Services/Wallets/BTCPayWallet.cs | 37 ++--- 25 files changed, 410 insertions(+), 150 deletions(-) create mode 100644 BTCPayServer/Events/InvoiceCreatedEvent.cs create mode 100644 BTCPayServer/ExplorerClientProvider.cs rename BTCPayServer/{Services/Invoices => HostedServices}/InvoiceNotificationManager.cs (98%) rename BTCPayServer/{Services/Invoices => HostedServices}/InvoiceWatcher.cs (95%) create mode 100644 BTCPayServer/HostedServices/NBXplorerListener.cs rename BTCPayServer/{ => HostedServices}/NBXplorerWaiter.cs (81%) diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index c19269169..317e0682c 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -107,12 +107,6 @@ namespace BTCPayServer.Tests .Build(); _Host.Start(); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); - - var waiter = ((NBXplorerWaiterAccessor)_Host.Services.GetService(typeof(NBXplorerWaiterAccessor))).Instance; - while(waiter.State != NBXplorerState.Ready) - { - Thread.Sleep(10); - } } public string HostName diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index dcdf5e85b..e3e3ae217 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -289,7 +289,7 @@ namespace BTCPayServer.Tests } [Fact] - public void InvoiceFlowThroughDifferentStatesCorrectly() + public void TestAccessBitpayAPI() { using (var tester = ServerTester.Create()) { @@ -298,6 +298,17 @@ namespace BTCPayServer.Tests Assert.False(user.BitPay.TestAccess(Facade.Merchant)); user.GrantAccess(); Assert.True(user.BitPay.TestAccess(Facade.Merchant)); + } + } + + [Fact] + public void InvoiceFlowThroughDifferentStatesCorrectly() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); var invoice = user.BitPay.CreateInvoice(new Invoice() { Price = 5000.0, diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 7738abe1c..7c910552c 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -41,7 +41,7 @@ services: # - eclair2 nbxplorer: - image: nicolasdorier/nbxplorer:1.0.0.35 + image: nicolasdorier/nbxplorer:1.0.0.36 ports: - "32838:32838" expose: diff --git a/BTCPayServer/BTCPayNetwork.cs b/BTCPayServer/BTCPayNetwork.cs index a432fe45d..f27db9d61 100644 --- a/BTCPayServer/BTCPayNetwork.cs +++ b/BTCPayServer/BTCPayNetwork.cs @@ -22,6 +22,5 @@ namespace BTCPayServer return CryptoCode == "BTC"; } } - } } diff --git a/BTCPayServer/BTCPayNetworkProvider.cs b/BTCPayServer/BTCPayNetworkProvider.cs index 162b6bc56..5a1d154a2 100644 --- a/BTCPayServer/BTCPayNetworkProvider.cs +++ b/BTCPayServer/BTCPayNetworkProvider.cs @@ -18,7 +18,7 @@ namespace BTCPayServer CryptoCode = "BTC", BlockExplorerLink = "https://www.smartbit.com.au/tx/{0}", NBitcoinNetwork = Network.Main, - UriScheme = "bitcoin" + UriScheme = "bitcoin", }); } @@ -29,7 +29,7 @@ namespace BTCPayServer CryptoCode = "BTC", BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}", NBitcoinNetwork = Network.TestNet, - UriScheme = "bitcoin" + UriScheme = "bitcoin", }); } @@ -59,6 +59,11 @@ namespace BTCPayServer _Networks.Add(network.CryptoCode, network); } + public IEnumerable GetAll() + { + return _Networks.Values.ToArray(); + } + public BTCPayNetwork GetNetwork(string cryptoCode) { _Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 352a433d1..81b44be51 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -24,7 +24,7 @@ - + diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 28bb6abc0..2e96979b7 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -9,6 +9,7 @@ using System.Net; using System.Text; using StandardConfiguration; using Microsoft.Extensions.Configuration; +using NBXplorer; namespace BTCPayServer.Configuration { @@ -18,15 +19,6 @@ namespace BTCPayServer.Configuration { get; set; } - public Uri Explorer - { - get; set; - } - - public string CookieFile - { - get; set; - } public string ConfigurationFile { get; @@ -53,11 +45,39 @@ namespace BTCPayServer.Configuration DataDir = conf.GetOrDefault("datadir", networkInfo.DefaultDataDirectory); Logs.Configuration.LogInformation("Network: " + Network); - Explorer = conf.GetOrDefault("explorer.url", networkInfo.DefaultExplorerUrl); - CookieFile = conf.GetOrDefault("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile); + foreach (var net in new BTCPayNetworkProvider(Network).GetAll()) + { + var explorer = conf.GetOrDefault($"{net.CryptoCode}.explorer.url", null); + var cookieFile = conf.GetOrDefault($"{net.CryptoCode}.explorer.cookiefile", null); + if (explorer != null && cookieFile != null) + { + ExplorerFactories.Add(net.CryptoCode, (n) => CreateExplorerClient(n, explorer, cookieFile)); + } + } + + // Handle legacy explorer.url and explorer.cookiefile + if (ExplorerFactories.Count == 0) + { + var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(Network.Name); + var explorer = conf.GetOrDefault($"explorer.url", new Uri(nbxplorer.GetDefaultExplorerUrl(), UriKind.Absolute)); + var cookieFile = conf.GetOrDefault($"explorer.cookiefile", nbxplorer.GetDefaultCookieFile()); + ExplorerFactories.Add("BTC", (n) => CreateExplorerClient(n, explorer, cookieFile)); + } + ////// + PostgresConnectionString = conf.GetOrDefault("postgres", null); ExternalUrl = conf.GetOrDefault("externalurl", null); } + + private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile) + { + var explorer = new ExplorerClient(n.NBitcoinNetwork, uri); + if (!explorer.SetCookieAuth(cookieFile)) + explorer.SetNoAuth(); + return explorer; + } + + public Dictionary> ExplorerFactories = new Dictionary>(); public string PostgresConnectionString { get; diff --git a/BTCPayServer/Configuration/DefaultConfiguration.cs b/BTCPayServer/Configuration/DefaultConfiguration.cs index 8029311f5..1dfec91b3 100644 --- a/BTCPayServer/Configuration/DefaultConfiguration.cs +++ b/BTCPayServer/Configuration/DefaultConfiguration.cs @@ -27,8 +27,11 @@ namespace BTCPayServer.Configuration app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue); app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue); app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue); - app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue); - app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue); + foreach (var network in new BTCPayNetworkProvider(Network.Main).GetAll()) + { + app.Option($"--{network.CryptoCode}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: If no explorer is specified, the default for Bitcoin will be selected)", CommandOptionType.SingleValue); + app.Option($"--{network.CryptoCode}explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue); + } app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue); return app; } @@ -83,8 +86,12 @@ namespace BTCPayServer.Configuration builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;"); builder.AppendLine(); builder.AppendLine("### NBXplorer settings ###"); - builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri); - builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile); + foreach (var n in new BTCPayNetworkProvider(network.Network).GetAll()) + { + var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(n.NBitcoinNetwork.ToString()); + builder.AppendLine($"#{n.CryptoCode}.explorer.url={nbxplorer.GetDefaultExplorerUrl()}"); + builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ nbxplorer.GetDefaultCookieFile()}"); + } return builder.ToString(); } diff --git a/BTCPayServer/Configuration/NetworkInformation.cs b/BTCPayServer/Configuration/NetworkInformation.cs index 7e6d591fd..8f99a3912 100644 --- a/BTCPayServer/Configuration/NetworkInformation.cs +++ b/BTCPayServer/Configuration/NetworkInformation.cs @@ -13,25 +13,20 @@ namespace BTCPayServer.Configuration static NetworkInformation() { _Networks = new Dictionary(); - foreach (var network in Network.GetNetworks()) + foreach (var network in new[] { Network.Main, Network.TestNet, Network.RegTest }) { NetworkInformation info = new NetworkInformation(); info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name); info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config"); - info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie"); info.Network = network; - info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute); info.DefaultPort = 23002; _Networks.Add(network.Name, info); if (network == Network.Main) { - info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute); - Main = info; info.DefaultPort = 23000; } if (network == Network.TestNet) { - info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute); info.DefaultPort = 23001; } } @@ -54,12 +49,7 @@ namespace BTCPayServer.Configuration } return null; } - - public static NetworkInformation Main - { - get; - set; - } + public Network Network { get; set; @@ -74,21 +64,11 @@ namespace BTCPayServer.Configuration get; set; } - public Uri DefaultExplorerUrl - { - get; - internal set; - } public int DefaultPort { get; private set; } - public string DefaultExplorerCookieFile - { - get; - internal set; - } public override string ToString() { diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 3c2811245..952b6eb7a 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -273,7 +273,6 @@ namespace BTCPayServer.Controllers var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); RegisteredUserId = user.Id; await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl); - _logger.LogInformation("User created a new account with password."); if (!policies.RequiresConfirmedEmail) { await _signInManager.SignInAsync(user, isPersistent: false); diff --git a/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs b/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs index dfb382570..a4df5422b 100644 --- a/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs +++ b/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs @@ -61,14 +61,18 @@ namespace BTCPayServer.Controllers [HttpPost] [Route("i/{invoiceId}", Order = 99)] + [Route("i/{invoiceId}/{cryptoCode}", Order = 99)] [MediaTypeConstraint("application/bitcoin-payment")] - public async Task PostPayment(string invoiceId) + public async Task PostPayment(string invoiceId, string cryptoCode = null) { var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); if (invoice == null || invoice.IsExpired()) return NotFound(); + if (cryptoCode == null) + cryptoCode = "BTC"; + var network = _NetworkProvider.GetNetwork(cryptoCode); var payment = PaymentMessage.Load(Request.Body); - var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions); + var unused = _Wallet.BroadcastTransactionsAsync(network, payment.Transactions); await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray()); return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase...")); } diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 19b536fbb..ecbdbdbe7 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers { if (command == "refresh") { - _Watcher.Watch(invoiceId); + _EventAggregator.Publish(new Events.InvoiceCreatedEvent(invoiceId)); } StatusMessage = "Invoice is state is being refreshed, please refresh the page soon..."; return RedirectToAction(nameof(Invoice), new @@ -94,7 +94,7 @@ namespace BTCPayServer.Controllers { var m = new InvoiceDetailsModel.Payment(); m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(network.NBitcoinNetwork); - m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0; + m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0; m.TransactionId = payment.Outpoint.Hash.ToString(); m.ReceivedTime = payment.ReceivedTime; m.TransactionLink = string.Format(network.BlockExplorerLink, m.TransactionId); diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 920bc61e5..80aabdd43 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -38,6 +38,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc.Routing; using NBXplorer.DerivationStrategy; using NBXplorer; +using BTCPayServer.HostedServices; namespace BTCPayServer.Controllers { @@ -46,14 +47,13 @@ namespace BTCPayServer.Controllers InvoiceRepository _InvoiceRepository; BTCPayWallet _Wallet; IRateProvider _RateProvider; - private InvoiceWatcher _Watcher; StoreRepository _StoreRepository; UserManager _UserManager; IFeeProviderFactory _FeeProviderFactory; private CurrencyNameTable _CurrencyNameTable; - ExplorerClient _Explorer; EventAggregator _EventAggregator; BTCPayNetworkProvider _NetworkProvider; + ExplorerClientProvider _ExplorerClients; public InvoiceController(InvoiceRepository invoiceRepository, CurrencyNameTable currencyNameTable, UserManager userManager, @@ -61,18 +61,16 @@ namespace BTCPayServer.Controllers IRateProvider rateProvider, StoreRepository storeRepository, EventAggregator eventAggregator, - InvoiceWatcherAccessor watcher, - ExplorerClient explorerClient, BTCPayNetworkProvider networkProvider, + ExplorerClientProvider explorerClientProviders, IFeeProviderFactory feeProviderFactory) { + _ExplorerClients = explorerClientProviders; _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); - _Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); _RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider)); - _Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance; _UserManager = userManager; _FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory)); _EventAggregator = eventAggregator; @@ -151,7 +149,7 @@ namespace BTCPayServer.Controllers entity.SetCryptoData(cryptoDatas); entity.PosData = invoice.PosData; entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider); - _Watcher.Watch(entity.Id); + _EventAggregator.Publish(new Events.InvoiceCreatedEvent(entity.Id)); var resp = entity.EntityToDTO(_NetworkProvider); return new DataWrapper(resp) { Facade = "pos/invoice" }; } diff --git a/BTCPayServer/Events/InvoiceCreatedEvent.cs b/BTCPayServer/Events/InvoiceCreatedEvent.cs new file mode 100644 index 000000000..af0462f85 --- /dev/null +++ b/BTCPayServer/Events/InvoiceCreatedEvent.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Events +{ + public class InvoiceCreatedEvent + { + public InvoiceCreatedEvent(string id) + { + InvoiceId = id; + } + + public string InvoiceId { get; set; } + + public override string ToString() + { + return $"Invoice {InvoiceId} created"; + } + } +} diff --git a/BTCPayServer/Events/NBXplorerStateChangedEvent.cs b/BTCPayServer/Events/NBXplorerStateChangedEvent.cs index 32eb440fd..5774140ff 100644 --- a/BTCPayServer/Events/NBXplorerStateChangedEvent.cs +++ b/BTCPayServer/Events/NBXplorerStateChangedEvent.cs @@ -2,23 +2,26 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.HostedServices; namespace BTCPayServer.Events { public class NBXplorerStateChangedEvent { - public NBXplorerStateChangedEvent(NBXplorerState old, NBXplorerState newState) + public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState) { + Network = network; NewState = newState; OldState = old; } + public BTCPayNetwork Network { get; set; } public NBXplorerState NewState { get; set; } public NBXplorerState OldState { get; set; } public override string ToString() { - return $"NBXplorer: {OldState} => {NewState}"; + return $"NBXplorer {Network.CryptoCode}: {OldState} => {NewState}"; } } } diff --git a/BTCPayServer/Events/TxOutReceivedEvent.cs b/BTCPayServer/Events/TxOutReceivedEvent.cs index df368e7d6..2a614776d 100644 --- a/BTCPayServer/Events/TxOutReceivedEvent.cs +++ b/BTCPayServer/Events/TxOutReceivedEvent.cs @@ -8,12 +8,12 @@ namespace BTCPayServer.Events { public class TxOutReceivedEvent { + public BTCPayNetwork Network { get; set; } public Script ScriptPubKey { get; set; } - public BitcoinAddress Address { get; set; } public override string ToString() { - String address = Address?.ToString() ?? ScriptPubKey.ToHex(); + String address = ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork)?.ToString() ?? ScriptPubKey.ToString(); return $"{address} received a transaction"; } } diff --git a/BTCPayServer/ExplorerClientProvider.cs b/BTCPayServer/ExplorerClientProvider.cs new file mode 100644 index 000000000..8bd99bf45 --- /dev/null +++ b/BTCPayServer/ExplorerClientProvider.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using NBXplorer; + +namespace BTCPayServer +{ + public class ExplorerClientProvider + { + BTCPayNetworkProvider _NetworkProviders; + BTCPayServerOptions _Options; + + public BTCPayNetworkProvider NetworkProviders => _NetworkProviders; + + public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options) + { + _NetworkProviders = networkProviders; + _Options = options; + } + + public ExplorerClient GetExplorerClient(string cryptoCode) + { + var network = _NetworkProviders.GetNetwork(cryptoCode); + if (network == null) + return null; + if (_Options.ExplorerFactories.TryGetValue(network.CryptoCode, out Func factory)) + { + return factory(network); + } + return null; + } + + internal object GetExplorerClient(object network) + { + throw new NotImplementedException(); + } + + public ExplorerClient GetExplorerClient(BTCPayNetwork network) + { + return GetExplorerClient(network.CryptoCode); + } + + public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll() + { + foreach(var net in _NetworkProviders.GetAll()) + { + if(_Options.ExplorerFactories.TryGetValue(net.CryptoCode, out Func factory)) + { + yield return (net, factory(net)); + } + } + } + } +} diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 534e2ff33..a532b839a 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -18,21 +18,30 @@ using NBXplorer.Models; using System.Linq; using System.Threading; using BTCPayServer.Services.Wallets; +using System.IO; namespace BTCPayServer { public static class Extensions { + public static string GetDefaultExplorerUrl(this NBXplorer.Configuration.NetworkInformation networkInfo) + { + return $"http://127.0.0.1:{networkInfo.DefaultExplorerPort}/"; + } + public static string GetDefaultCookieFile(this NBXplorer.Configuration.NetworkInformation networkInfo) + { + return Path.Combine(networkInfo.DefaultDataDirectory, ".cookie"); + } public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider) { return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; } - public static async Task> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken)) + public static async Task> GetTransactions(this BTCPayWallet client, BTCPayNetwork network, uint256[] hashes, CancellationToken cts = default(CancellationToken)) { hashes = hashes.Distinct().ToArray(); var transactions = hashes - .Select(async o => await client.GetTransactionAsync(o, cts)) + .Select(async o => await client.GetTransactionAsync(network, o, cts)) .ToArray(); await Task.WhenAll(transactions).ConfigureAwait(false); return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash()); diff --git a/BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs similarity index 98% rename from BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs rename to BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 2fc725f57..1ad9112ec 100644 --- a/BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -18,8 +18,9 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Hosting; using BTCPayServer.Events; using NBXplorer; +using BTCPayServer.Services.Invoices; -namespace BTCPayServer.Services.Invoices +namespace BTCPayServer.HostedServices { public class InvoiceNotificationManager : IHostedService { diff --git a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs similarity index 95% rename from BTCPayServer/Services/Invoices/InvoiceWatcher.cs rename to BTCPayServer/HostedServices/InvoiceWatcher.cs index 088870c17..bbb574b78 100644 --- a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -16,13 +16,10 @@ using BTCPayServer.Services.Wallets; using BTCPayServer.Controllers; using BTCPayServer.Events; using Microsoft.AspNetCore.Hosting; +using BTCPayServer.Services.Invoices; -namespace BTCPayServer.Services.Invoices +namespace BTCPayServer.HostedServices { - public class InvoiceWatcherAccessor - { - public InvoiceWatcher Instance { get; set; } - } public class InvoiceWatcher : IHostedService { class UpdateInvoiceContext @@ -56,15 +53,13 @@ namespace BTCPayServer.Services.Invoices BTCPayNetworkProvider networkProvider, InvoiceRepository invoiceRepository, EventAggregator eventAggregator, - BTCPayWallet wallet, - InvoiceWatcherAccessor accessor) + BTCPayWallet wallet) { PollInterval = TimeSpan.FromMinutes(1.0); _Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _NetworkProvider = networkProvider; - accessor.Instance = this; } CompositeDisposable leases = new CompositeDisposable(); @@ -157,7 +152,6 @@ namespace BTCPayServer.Services.Invoices response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString())).ToArray(); } var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault(); - bool dirtyAddress = false; if (coins != null) { @@ -172,7 +166,7 @@ namespace BTCPayServer.Services.Invoices } } ////// - var network = _NetworkProvider.GetNetwork("BTC"); + var network = coins?.Strategy?.Network ?? _NetworkProvider.GetNetwork(invoice.GetCryptoData().First().Key); var cryptoData = invoice.GetCryptoData(network); var cryptoDataAll = invoice.GetCryptoData(); var accounting = cryptoData.Calculate(); @@ -187,7 +181,7 @@ namespace BTCPayServer.Services.Invoices if (invoice.Status == "new" || invoice.Status == "expired") { - var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); + var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalPaid >= accounting.TotalDue) { if (invoice.Status == "new") @@ -228,7 +222,7 @@ namespace BTCPayServer.Services.Invoices if (invoice.Status == "paid") { - var transactions = await GetPaymentsWithTransaction(invoice); + var transactions = await GetPaymentsWithTransaction(network, invoice); var chainConfirmedTransactions = transactions.Where(t => t.Confirmations >= 1); if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed) { @@ -271,7 +265,7 @@ namespace BTCPayServer.Services.Invoices if (invoice.Status == "confirmed") { - var transactions = await GetPaymentsWithTransaction(invoice); + var transactions = await GetPaymentsWithTransaction(network, invoice); transactions = transactions.Where(t => t.Confirmations >= 6); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalConfirmed >= accounting.TotalDue) @@ -283,9 +277,9 @@ namespace BTCPayServer.Services.Invoices } } - private async Task> GetPaymentsWithTransaction(InvoiceEntity invoice) + private async Task> GetPaymentsWithTransaction(BTCPayNetwork network, InvoiceEntity invoice) { - var transactions = await _Wallet.GetTransactions(invoice.Payments.Select(t => t.Outpoint.Hash).ToArray()); + var transactions = await _Wallet.GetTransactions(network, invoice.Payments.Select(t => t.Outpoint.Hash).ToArray()); var spentTxIn = new Dictionary(); var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet(); @@ -355,7 +349,7 @@ namespace BTCPayServer.Services.Invoices } } - public void Watch(string invoiceId) + private void Watch(string invoiceId) { if (invoiceId == null) throw new ArgumentNullException(nameof(invoiceId)); @@ -390,7 +384,8 @@ namespace BTCPayServer.Services.Invoices }, null, 0, (int)PollInterval.TotalMilliseconds); leases.Add(_EventAggregator.Subscribe(async b => { await NotifyBlock(); })); - leases.Add(_EventAggregator.Subscribe(async b => { await NotifyReceived(b.ScriptPubKey); })); + leases.Add(_EventAggregator.Subscribe(async b => { await NotifyReceived(b.ScriptPubKey); })); + leases.Add(_EventAggregator.Subscribe(b => { Watch(b.InvoiceId); })); return Task.CompletedTask; } diff --git a/BTCPayServer/HostedServices/NBXplorerListener.cs b/BTCPayServer/HostedServices/NBXplorerListener.cs new file mode 100644 index 000000000..dec54aa72 --- /dev/null +++ b/BTCPayServer/HostedServices/NBXplorerListener.cs @@ -0,0 +1,153 @@ +using System; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Logging; +using BTCPayServer.Services.Invoices; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using NBXplorer; +using System.Collections.Concurrent; +using NBXplorer.DerivationStrategy; +using BTCPayServer.Events; + +namespace BTCPayServer.HostedServices +{ + public class NBXplorerListener : IHostedService + { + EventAggregator _Aggregator; + ExplorerClientProvider _ExplorerClients; + IApplicationLifetime _Lifetime; + InvoiceRepository _InvoiceRepository; + private TaskCompletionSource _RunningTask; + private CancellationTokenSource _Cts; + + public NBXplorerListener(ExplorerClientProvider explorerClients, + InvoiceRepository invoiceRepository, + EventAggregator aggregator, IApplicationLifetime lifetime) + { + _InvoiceRepository = invoiceRepository; + _ExplorerClients = explorerClients; + _Aggregator = aggregator; + _Lifetime = lifetime; + } + + CompositeDisposable leases = new CompositeDisposable(); + ConcurrentDictionary _Sessions = new ConcurrentDictionary(); + + public Task StartAsync(CancellationToken cancellationToken) + { + _RunningTask = new TaskCompletionSource(); + _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + leases.Add(_Aggregator.Subscribe(async nbxplorerEvent => + { + if (nbxplorerEvent.NewState == NBXplorerState.Ready) + { + if (_Sessions.ContainsKey(nbxplorerEvent.Network.CryptoCode)) + return; + var client = _ExplorerClients.GetExplorerClient(nbxplorerEvent.Network); + var session = await client.CreateNotificationSessionAsync(_Cts.Token); + if (!_Sessions.TryAdd(nbxplorerEvent.Network.CryptoCode, session)) + { + await session.DisposeAsync(); + return; + } + + try + { + using (session) + { + await session.ListenNewBlockAsync(_Cts.Token); + await session.ListenDerivationSchemesAsync((await GetStrategies(nbxplorerEvent)).ToArray(), _Cts.Token); + Logs.PayServer.LogInformation($"Start Listening {nbxplorerEvent.Network.CryptoCode} explorer events"); + while (true) + { + var newEvent = await session.NextEventAsync(_Cts.Token); + switch (newEvent) + { + case NBXplorer.Models.NewBlockEvent evt: + _Aggregator.Publish(new Events.NewBlockEvent()); + break; + case NBXplorer.Models.NewTransactionEvent evt: + foreach (var txout in evt.Match.Outputs) + { + _Aggregator.Publish(new Events.TxOutReceivedEvent() + { + Network = nbxplorerEvent.Network, + ScriptPubKey = txout.ScriptPubKey + }); + } + break; + default: + Logs.PayServer.LogWarning("Received unknown message from NBXplorer"); + break; + } + } + } + } + catch when (_Cts.IsCancellationRequested) { } + finally + { + Logs.PayServer.LogInformation($"Stop listening {nbxplorerEvent.Network.CryptoCode} explorer events"); + _Sessions.TryRemove(nbxplorerEvent.Network.CryptoCode, out NotificationSession unused); + if(_Sessions.Count == 0 && _Cts.IsCancellationRequested) + { + _RunningTask.TrySetResult(true); + } + } + } + })); + + leases.Add(_Aggregator.Subscribe(async inv => + { + var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId); + List listeningDerivations = new List(); + foreach (var notificationSessions in _Sessions) + { + var derivationStrategy = GetStrategy(notificationSessions.Key, invoice); + if (derivationStrategy != null) + { + listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token)); + } + } + await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false); + })); + return Task.CompletedTask; + } + + private async Task> GetStrategies(NBXplorerStateChangedEvent nbxplorerEvent) + { + List strategies = new List(); + foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices()) + { + var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); + var strategy = GetStrategy(nbxplorerEvent.Network.CryptoCode, invoice); + if (strategy != null) + strategies.Add(strategy); + } + + return strategies; + } + + private DerivationStrategyBase GetStrategy(string cryptoCode, InvoiceEntity invoice) + { + foreach (var derivationStrategy in invoice.GetDerivationStrategies(_ExplorerClients.NetworkProviders)) + { + if (derivationStrategy.Network.CryptoCode == cryptoCode) + { + return derivationStrategy.DerivationStrategyBase; + } + } + return null; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + leases.Dispose(); + _Cts.Cancel(); + return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken)); + } + } +} diff --git a/BTCPayServer/NBXplorerWaiter.cs b/BTCPayServer/HostedServices/NBXplorerWaiter.cs similarity index 81% rename from BTCPayServer/NBXplorerWaiter.cs rename to BTCPayServer/HostedServices/NBXplorerWaiter.cs index 0279209fc..952be0f64 100644 --- a/BTCPayServer/NBXplorerWaiter.cs +++ b/BTCPayServer/HostedServices/NBXplorerWaiter.cs @@ -11,12 +11,8 @@ using NBXplorer.Models; using System.Collections.Concurrent; using BTCPayServer.Events; -namespace BTCPayServer +namespace BTCPayServer.HostedServices { - public class NBXplorerWaiterAccessor - { - public NBXplorerWaiter Instance { get; set; } - } public enum NBXplorerState { NotConnected, @@ -24,15 +20,38 @@ namespace BTCPayServer Ready } - public class NBXplorerWaiter : IHostedService + public class NBXplorerWaiters : IHostedService { - public NBXplorerWaiter(ExplorerClient client, EventAggregator aggregator, NBXplorerWaiterAccessor accessor) + List _Waiters = new List(); + public NBXplorerWaiters(ExplorerClientProvider explorerClientProvider, EventAggregator eventAggregator) { - _Client = client; - _Aggregator = aggregator; - accessor.Instance = this; + foreach(var explorer in explorerClientProvider.GetAll()) + { + _Waiters.Add(new NBXplorerWaiter(explorer.Item1, explorer.Item2, eventAggregator)); + } + } + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.WhenAll(_Waiters.Select(w => w.StartAsync(cancellationToken)).ToArray()); } + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.WhenAll(_Waiters.Select(w => w.StopAsync(cancellationToken)).ToArray()); + } + } + + public class NBXplorerWaiter : IHostedService + { + + public NBXplorerWaiter(BTCPayNetwork network, ExplorerClient client, EventAggregator aggregator) + { + _Network = network; + _Client = client; + _Aggregator = aggregator; + } + + BTCPayNetwork _Network; EventAggregator _Aggregator; ExplorerClient _Client; Timer _Timer; @@ -126,7 +145,7 @@ namespace BTCPayServer { SetInterval(TimeSpan.FromMinutes(1)); } - _Aggregator.Publish(new NBXplorerStateChangedEvent(oldState, State)); + _Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State)); } return oldState != State; } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 3eb09d381..017836305 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -36,6 +36,7 @@ using BTCPayServer.Services.Wallets; using BTCPayServer.Authentication; using Microsoft.Extensions.Caching.Memory; using BTCPayServer.Logging; +using BTCPayServer.HostedServices; namespace BTCPayServer.Hosting { @@ -134,22 +135,18 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(o => new NBXplorerFeeProviderFactory(o.GetRequiredService()) + services.TryAddSingleton(o => new NBXplorerFeeProviderFactory(o.GetRequiredService()) { Fallback = new FeeRate(100, 1), BlockTarget = 20 }); - services.TryAddSingleton(); - services.AddSingleton(); - services.TryAddSingleton(o => - { - var opts = o.GetRequiredService(); - var explorer = new ExplorerClient(opts.Network, opts.Explorer); - if (!explorer.SetCookieAuth(opts.CookieFile)) - explorer.SetNoAuth(); - return explorer; - }); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.TryAddSingleton(); services.TryAddSingleton(o => { if (o.GetRequiredService().Network == Network.Main) @@ -163,11 +160,7 @@ namespace BTCPayServer.Hosting var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))); return new CachedRateProvider(new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay }), o.GetRequiredService()) { CacheSpan = TimeSpan.FromMinutes(1.0) }; }); - - services.AddSingleton(); - services.TryAddSingleton(); - services.AddSingleton(); services.TryAddScoped(); services.TryAddSingleton(); services.AddTransient(); diff --git a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs index d5386b570..f5f1f8d3b 100644 --- a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs +++ b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs @@ -10,43 +10,38 @@ namespace BTCPayServer.Services.Fees { public class NBXplorerFeeProviderFactory : IFeeProviderFactory { - public NBXplorerFeeProviderFactory(ExplorerClient explorerClient) + public NBXplorerFeeProviderFactory(ExplorerClientProvider explorerClients) { - if (explorerClient == null) - throw new ArgumentNullException(nameof(explorerClient)); - _ExplorerClient = explorerClient; + if (explorerClients == null) + throw new ArgumentNullException(nameof(explorerClients)); + _ExplorerClients = explorerClients; } - private readonly ExplorerClient _ExplorerClient; - public ExplorerClient ExplorerClient - { - get - { - return _ExplorerClient; - } - } + private readonly ExplorerClientProvider _ExplorerClients; public FeeRate Fallback { get; set; } public int BlockTarget { get; set; } public IFeeProvider CreateFeeProvider(BTCPayNetwork network) { - return new NBXplorerFeeProvider(this); + return new NBXplorerFeeProvider(this, _ExplorerClients.GetExplorerClient(network)); } } public class NBXplorerFeeProvider : IFeeProvider { - public NBXplorerFeeProvider(NBXplorerFeeProviderFactory factory) + public NBXplorerFeeProvider(NBXplorerFeeProviderFactory parent, ExplorerClient explorerClient) { - if (factory == null) - throw new ArgumentNullException(nameof(factory)); - _Factory = factory; + if (explorerClient == null) + throw new ArgumentNullException(nameof(explorerClient)); + _Factory = parent; + _ExplorerClient = explorerClient; } - private readonly NBXplorerFeeProviderFactory _Factory; + NBXplorerFeeProviderFactory _Factory; + ExplorerClient _ExplorerClient; public async Task GetFeeRateAsync() { try { - return (await _Factory.ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate; + return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate; } catch (NBXplorerException ex) when (ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable") { diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index ec6320c94..5ee462ea7 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -25,11 +25,10 @@ namespace BTCPayServer.Services.Wallets } public class BTCPayWallet { - private ExplorerClient _Client; - private Serializer _Serializer; + private ExplorerClientProvider _Client; ApplicationDbContextFactory _DBFactory; - public BTCPayWallet(ExplorerClient client, ApplicationDbContextFactory factory) + public BTCPayWallet(ExplorerClientProvider client, ApplicationDbContextFactory factory) { if (client == null) throw new ArgumentNullException(nameof(client)); @@ -37,31 +36,32 @@ namespace BTCPayServer.Services.Wallets throw new ArgumentNullException(nameof(factory)); _Client = client; _DBFactory = factory; - _Serializer = new NBXplorer.Serializer(_Client.Network); - LongPollingMode = client.Network == Network.RegTest; } public async Task ReserveAddressAsync(DerivationStrategy derivationStrategy) { - var pathInfo = await _Client.GetUnusedAsync(derivationStrategy.DerivationStrategyBase, DerivationFeature.Deposit, 0, true).ConfigureAwait(false); - return pathInfo.ScriptPubKey.GetDestinationAddress(_Client.Network); + var client = _Client.GetExplorerClient(derivationStrategy.Network); + var pathInfo = await client.GetUnusedAsync(derivationStrategy.DerivationStrategyBase, DerivationFeature.Deposit, 0, true).ConfigureAwait(false); + return pathInfo.ScriptPubKey.GetDestinationAddress(client.Network); } public async Task TrackAsync(DerivationStrategy derivationStrategy) { - await _Client.TrackAsync(derivationStrategy.DerivationStrategyBase); + var client = _Client.GetExplorerClient(derivationStrategy.Network); + await client.TrackAsync(derivationStrategy.DerivationStrategyBase); } - public Task GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken)) + public Task GetTransactionAsync(BTCPayNetwork network, uint256 txId, CancellationToken cancellation = default(CancellationToken)) { - return _Client.GetTransactionAsync(txId, cancellation); + var client = _Client.GetExplorerClient(network); + return client.GetTransactionAsync(txId, cancellation); } - public bool LongPollingMode { get; set; } public async Task GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken)) { - var changes = await _Client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, !LongPollingMode, cancellation).ConfigureAwait(false); + var client = _Client.GetExplorerClient(strategy.Network); + var changes = await client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false); var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => c.AsCoin()).ToArray(); return new GetCoinsResult() { @@ -71,20 +71,17 @@ namespace BTCPayServer.Services.Wallets }; } - private byte[] ToBytes(T obj) + public Task BroadcastTransactionsAsync(BTCPayNetwork network, List transactions) { - return ZipUtils.Zip(_Serializer.ToString(obj)); - } - - public Task BroadcastTransactionsAsync(List transactions) - { - var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray(); + var client = _Client.GetExplorerClient(network); + var tasks = transactions.Select(t => client.BroadcastAsync(t)).ToArray(); return Task.WhenAll(tasks); } public async Task GetBalance(DerivationStrategy derivationStrategy) { - var result = await _Client.SyncAsync(derivationStrategy.DerivationStrategyBase, null, true); + var client = _Client.GetExplorerClient(derivationStrategy.Network); + var result = await client.SyncAsync(derivationStrategy.DerivationStrategyBase, null, true); return result.Confirmed.UTXOs.Select(u => u.Value) .Concat(result.Unconfirmed.UTXOs.Select(u => u.Value)) .Sum();