Use Websocket for blockchain notifications

This commit is contained in:
nicolas.dorier 2018-01-08 02:36:41 +09:00
parent eb44203475
commit e3a1eed8b3
25 changed files with 410 additions and 150 deletions

View file

@ -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

View file

@ -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,

View file

@ -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:

View file

@ -22,6 +22,5 @@ namespace BTCPayServer
return CryptoCode == "BTC";
}
}
}
}

View file

@ -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<BTCPayNetwork> GetAll()
{
return _Networks.Values.ToArray();
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);

View file

@ -24,7 +24,7 @@
<PackageReference Include="NBitcoin" Version="4.0.0.51" />
<PackageReference Include="NBitpayClient" Version="1.0.0.13" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.23" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.24" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />

View file

@ -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<string>("datadir", networkInfo.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + Network);
Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl);
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
foreach (var net in new BTCPayNetworkProvider(Network).GetAll())
{
var explorer = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", null);
var cookieFile = conf.GetOrDefault<string>($"{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<Uri>($"explorer.url", new Uri(nbxplorer.GetDefaultExplorerUrl(), UriKind.Absolute));
var cookieFile = conf.GetOrDefault<string>($"explorer.cookiefile", nbxplorer.GetDefaultCookieFile());
ExplorerFactories.Add("BTC", (n) => CreateExplorerClient(n, explorer, cookieFile));
}
//////
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
ExternalUrl = conf.GetOrDefault<Uri>("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<string, Func<BTCPayNetwork, ExplorerClient>> ExplorerFactories = new Dictionary<string, Func<BTCPayNetwork, ExplorerClient>>();
public string PostgresConnectionString
{
get;

View file

@ -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();
}

View file

@ -13,25 +13,20 @@ namespace BTCPayServer.Configuration
static NetworkInformation()
{
_Networks = new Dictionary<string, NetworkInformation>();
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()
{

View file

@ -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);

View file

@ -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<IActionResult> PostPayment(string invoiceId)
public async Task<IActionResult> 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..."));
}

View file

@ -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);

View file

@ -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<ApplicationUser> _UserManager;
IFeeProviderFactory _FeeProviderFactory;
private CurrencyNameTable _CurrencyNameTable;
ExplorerClient _Explorer;
EventAggregator _EventAggregator;
BTCPayNetworkProvider _NetworkProvider;
ExplorerClientProvider _ExplorerClients;
public InvoiceController(InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> 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<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}

View file

@ -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";
}
}
}

View file

@ -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}";
}
}
}

View file

@ -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";
}
}

View file

@ -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<BTCPayNetwork, ExplorerClient> 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<BTCPayNetwork, ExplorerClient> factory))
{
yield return (net, factory(net));
}
}
}
}
}

View file

@ -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<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
public static async Task<Dictionary<uint256, TransactionResult>> 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());

View file

@ -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
{

View file

@ -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<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(InvoiceEntity invoice)
private async Task<IEnumerable<AccountedPaymentEntity>> 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<OutPoint, AccountedPaymentEntity>();
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<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
leases.Add(_EventAggregator.Subscribe<TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey); }));
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceCreatedEvent>(b => { Watch(b.InvoiceId); }));
return Task.CompletedTask;
}

View file

@ -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<bool> _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<string, NotificationSession> _Sessions = new ConcurrentDictionary<string, NotificationSession>();
public Task StartAsync(CancellationToken cancellationToken)
{
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(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<Events.InvoiceCreatedEvent>(async inv =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
List<Task> listeningDerivations = new List<Task>();
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<List<DerivationStrategyBase>> GetStrategies(NBXplorerStateChangedEvent nbxplorerEvent)
{
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
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));
}
}
}

View file

@ -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<NBXplorerWaiter> _Waiters = new List<NBXplorerWaiter>();
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;
}

View file

@ -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<StoreRepository>();
services.TryAddSingleton<BTCPayWallet>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClient>())
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
Fallback = new FeeRate(100, 1),
BlockTarget = 20
});
services.TryAddSingleton<NBXplorerWaiterAccessor>();
services.AddSingleton<IHostedService, NBXplorerWaiter>();
services.TryAddSingleton<ExplorerClient>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
var explorer = new ExplorerClient(opts.Network, opts.Explorer);
if (!explorer.SetCookieAuth(opts.CookieFile))
explorer.SetNoAuth();
return explorer;
});
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, NBXplorerListener>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o =>
{
if (o.GetRequiredService<BTCPayServerOptions>().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<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
});
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.TryAddSingleton<InvoiceWatcherAccessor>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>();

View file

@ -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<FeeRate> 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")
{

View file

@ -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<BitcoinAddress> 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<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken))
public Task<TransactionResult> 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<GetCoinsResult> 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>(T obj)
public Task BroadcastTransactionsAsync(BTCPayNetwork network, List<Transaction> transactions)
{
return ZipUtils.Zip(_Serializer.ToString(obj));
}
public Task BroadcastTransactionsAsync(List<Transaction> 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<Money> 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();