diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 6246fe0a2..0011dac21 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -64,16 +64,20 @@ namespace BTCPayServer.Tests } IWebHost _Host; + public int Port + { + get; set; + } + public void Start() { if(!Directory.Exists(_Directory)) Directory.CreateDirectory(_Directory); HDPrivateKey = new ExtKey(); - var port = Utils.FreeTcpPort(); StringBuilder config = new StringBuilder(); config.AppendLine($"regtest=1"); - config.AppendLine($"port={port}"); + config.AppendLine($"port={Port}"); config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}"); config.AppendLine($"explorer.cookiefile={CookieFile}"); config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}"); @@ -81,7 +85,7 @@ namespace BTCPayServer.Tests config.AppendLine($"postgres=" + Postgres); File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString()); - ServerUri = new Uri("http://127.0.0.1:" + port + "/"); + ServerUri = new Uri("http://" + HostName + ":" + Port + "/"); var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory }); @@ -94,6 +98,7 @@ namespace BTCPayServer.Tests { l.SetMinimumLevel(LogLevel.Information) .AddFilter("Microsoft", LogLevel.Error) + .AddFilter("Hangfire", LogLevel.Error) .AddProvider(Logs.LogProvider); }); }) @@ -110,6 +115,11 @@ namespace BTCPayServer.Tests { get; set; } + public string HostName + { + get; + internal set; + } public T GetController(string userId = null) where T : Controller { @@ -129,7 +139,8 @@ namespace BTCPayServer.Tests httpAccessor.HttpContext = context; var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T)); - controller.Url = new UrlHelperMock(); + + controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/")); controller.ControllerContext = new ControllerContext() { HttpContext = context diff --git a/BTCPayServer.Tests/Mocks/UrlHelperMock.cs b/BTCPayServer.Tests/Mocks/UrlHelperMock.cs index b7a9ca23c..1fa3c4ddd 100644 --- a/BTCPayServer.Tests/Mocks/UrlHelperMock.cs +++ b/BTCPayServer.Tests/Mocks/UrlHelperMock.cs @@ -8,16 +8,21 @@ namespace BTCPayServer.Tests.Mocks { public class UrlHelperMock : IUrlHelper { + Uri _BaseUrl; + public UrlHelperMock(Uri baseUrl) + { + _BaseUrl = baseUrl; + } public ActionContext ActionContext => throw new NotImplementedException(); public string Action(UrlActionContext actionContext) { - return "http://127.0.0.1/mock"; + return $"{_BaseUrl}mock"; } public string Content(string contentPath) { - return "http://127.0.0.1/mock"; + return $"{_BaseUrl}{contentPath}"; } public bool IsLocalUrl(string url) @@ -27,12 +32,12 @@ namespace BTCPayServer.Tests.Mocks public string Link(string routeName, object values) { - return "http://127.0.0.1/mock"; + return _BaseUrl.AbsoluteUri; } public string RouteUrl(UrlRouteContext routeContext) { - return "http://127.0.0.1/mock"; + return _BaseUrl.AbsoluteUri; } } } diff --git a/BTCPayServer.Tests/Properties/launchSettings.json b/BTCPayServer.Tests/Properties/launchSettings.json new file mode 100644 index 000000000..1c0aa7816 --- /dev/null +++ b/BTCPayServer.Tests/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "BTCPayServer.Tests": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 1dbd3c55c..2f78a5383 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -1,17 +1,21 @@ using BTCPayServer.Controllers; using BTCPayServer.Models.AccountViewModels; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitcoin.RPC; using NBitpayClient; using NBXplorer; +using NBXplorer.Models; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Net.Http; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using System.Threading; namespace BTCPayServer.Tests { @@ -28,6 +32,11 @@ namespace BTCPayServer.Tests _Directory = scope; } + public bool Dockerized + { + get; set; + } + public void Start() { if(Directory.Exists(_Directory)) @@ -35,6 +44,8 @@ namespace BTCPayServer.Tests if(!Directory.Exists(_Directory)) Directory.CreateDirectory(_Directory); + + FakeCallback = bool.Parse(GetEnvironment("TESTS_FAKECALLBACK", "true")); ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network); ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/"))); PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay")) @@ -42,6 +53,8 @@ namespace BTCPayServer.Tests NBXplorerUri = ExplorerClient.Address, Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver") }; + PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString())); + PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1"); PayTester.Start(); } @@ -56,6 +69,11 @@ namespace BTCPayServer.Tests return new TestAccount(this); } + public bool FakeCallback + { + get; + set; + } public RPCClient ExplorerNode { get; set; @@ -66,13 +84,143 @@ namespace BTCPayServer.Tests get; set; } - + HttpClient _Http = new HttpClient(); + + class MockHttpRequest : HttpRequest + { + Uri serverUri; + public MockHttpRequest(Uri serverUri) + { + this.serverUri = serverUri; + } + public override HttpContext HttpContext => throw new NotImplementedException(); + + public override string Method + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + public override string Scheme + { + get => serverUri.Scheme; + set => throw new NotImplementedException(); + } + public override bool IsHttps + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + public override HostString Host + { + get => new HostString(serverUri.Host, serverUri.Port); + set => throw new NotImplementedException(); + } + public override PathString PathBase + { + get => ""; + set => throw new NotImplementedException(); + } + public override PathString Path + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + public override QueryString QueryString + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + public override IQueryCollection Query + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + public override string Protocol + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override IHeaderDictionary Headers => throw new NotImplementedException(); + + public override IRequestCookieCollection Cookies + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + public override long? ContentLength + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + public override string ContentType + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + public override Stream Body + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override bool HasFormContentType => throw new NotImplementedException(); + + public override IFormCollection Form + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override Task ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + } + + /// + /// Simulating callback from NBXplorer. NBXplorer can't reach the host during tests as it is not running on localhost. + /// + /// + public void SimulateCallback(BitcoinAddress address = null) + { + if(!FakeCallback) //The callback of NBXplorer should work + return; + + var req = new MockHttpRequest(PayTester.ServerUri); + var controller = PayTester.GetController(); + if(address != null) + { + + var match = new TransactionMatch(); + match.Outputs.Add(new KeyPathInformation() { ScriptPubKey = address.ScriptPubKey }); + var content = new StringContent(new NBXplorer.Serializer(Network).ToString(match), new UTF8Encoding(false), "application/json"); + var uri = controller.GetCallbackUriAsync(req).GetAwaiter().GetResult(); + + HttpRequestMessage message = new HttpRequestMessage(); + message.Method = HttpMethod.Post; + message.RequestUri = uri; + message.Content = content; + + _Http.SendAsync(message).GetAwaiter().GetResult(); + } + else + { + + var uri = controller.GetCallbackBlockUriAsync(req).GetAwaiter().GetResult(); + HttpRequestMessage message = new HttpRequestMessage(); + message.Method = HttpMethod.Post; + message.RequestUri = uri; + _Http.SendAsync(message).GetAwaiter().GetResult(); + } + } + public BTCPayServerTester PayTester { get; set; } - + public Network Network { get; diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 818618ed1..b0c838033 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -99,6 +99,7 @@ namespace BTCPayServer.Tests Eventually(() => { + tester.SimulateCallback(url.Address); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("paid", localInvoice.Status); Assert.True(localInvoice.Refundable); @@ -153,6 +154,8 @@ namespace BTCPayServer.Tests }); BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21); tester.ExplorerNode.SendToAddress(url.Address, url.Amount); + Thread.Sleep(5000); + tester.SimulateCallback(url.Address); callbackServer.ProcessNextRequest((ctx) => { var ipn = new StreamReader(ctx.Request.Body).ReadToEnd(); @@ -228,6 +231,7 @@ namespace BTCPayServer.Tests Eventually(() => { + tester.SimulateCallback(invoiceAddress); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("paidPartial", localInvoice.Status); Assert.Equal(firstPayment, localInvoice.BtcPaid); @@ -240,6 +244,7 @@ namespace BTCPayServer.Tests Eventually(() => { + tester.SimulateCallback(invoiceAddress); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("paid", localInvoice.Status); Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid); @@ -251,6 +256,7 @@ namespace BTCPayServer.Tests Eventually(() => { + tester.SimulateCallback(); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("confirmed", localInvoice.Status); }); @@ -259,6 +265,7 @@ namespace BTCPayServer.Tests Eventually(() => { + tester.SimulateCallback(); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("complete", localInvoice.Status); }); @@ -280,6 +287,7 @@ namespace BTCPayServer.Tests Eventually(() => { + tester.SimulateCallback(invoiceAddress); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("paidOver", localInvoice.Status); Assert.Equal(Money.Zero, localInvoice.BtcDue); @@ -290,6 +298,7 @@ namespace BTCPayServer.Tests Eventually(() => { + tester.SimulateCallback(); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("confirmed", localInvoice.Status); Assert.Equal(Money.Zero, localInvoice.BtcDue); diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index abd75a49b..b821e5398 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -10,11 +10,18 @@ services: TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3 TESTS_NBXPLORERURL: http://nbxplorer:32838/ TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver + TESTS_FAKECALLBACK: 'true' + TESTS_PORT: 80 + TESTS_HOSTNAME: tests + expose: + - "80" links: - nbxplorer + extra_hosts: + - "tests:127.0.0.1" nbxplorer: - image: nicolasdorier/nbxplorer:1.0.0.16 + image: nicolasdorier/nbxplorer:1.0.0.18 ports: - "32838:32838" expose: diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 191d975d3..307091710 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -24,7 +24,7 @@ - + diff --git a/BTCPayServer/Controllers/CallbackController.cs b/BTCPayServer/Controllers/CallbackController.cs new file mode 100644 index 000000000..f14697010 --- /dev/null +++ b/BTCPayServer/Controllers/CallbackController.cs @@ -0,0 +1,116 @@ +using BTCPayServer.Logging; +using BTCPayServer.Servcices.Invoices; +using BTCPayServer.Services; +using BTCPayServer.Services.Wallets; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBXplorer; +using Microsoft.Extensions.Logging; +using NBXplorer.DerivationStrategy; +using NBXplorer.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BTCPayServer.Controllers +{ + public class CallbackController : Controller + { + public class CallbackSettings + { + public string Token + { + get; set; + } + } + SettingsRepository _Settings; + Network _Network; + InvoiceWatcher _Watcher; + ExplorerClient _Explorer; + + public CallbackController(SettingsRepository repo, + ExplorerClient explorer, + InvoiceWatcher watcher, + Network network) + { + _Settings = repo; + _Network = network; + _Watcher = watcher; + _Explorer = explorer; + } + + [Route("callbacks/transactions")] + [HttpPost] + public async Task NewTransaction(string token) + { + await AssertToken(token); + Logs.PayServer.LogInformation("New transaction callback"); + //We don't want to register all the json converter at MVC level, so we parse here + var serializer = new NBXplorer.Serializer(_Network); + var content = await new StreamReader(Request.Body, new UTF8Encoding(false), false, 1024, true).ReadToEndAsync(); + var match = serializer.ToObject(content); + + foreach(var output in match.Outputs) + { + await _Watcher.NotifyReceived(output.ScriptPubKey); + } + } + + [Route("callbacks/blocks")] + [HttpPost] + public async Task NewBlock(string token) + { + await AssertToken(token); + Logs.PayServer.LogInformation("New block callback"); + await _Watcher.NotifyBlock(); + } + + private async Task AssertToken(string token) + { + var callback = await _Settings.GetSettingAsync(); + if(await GetToken() != token) + throw new BTCPayServer.BitpayHttpException(400, "invalid-callback-token"); + } + + public async Task GetCallbackUriAsync(HttpRequest request) + { + string token = await GetToken(); + return new Uri(request.GetAbsoluteRoot() + "/callbacks/transactions?token=" + token); + } + + public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme, HttpRequest request) + { + var uri = await GetCallbackUriAsync(request); + await _Explorer.SubscribeToWalletAsync(uri, derivationScheme); + } + + private async Task GetToken() + { + var callback = await _Settings.GetSettingAsync(); + if(callback == null) + { + callback = new CallbackSettings() { Token = Guid.NewGuid().ToString() }; + await _Settings.UpdateSetting(callback); + } + var token = callback.Token; + return token; + } + + public async Task GetCallbackBlockUriAsync(HttpRequest request) + { + string token = await GetToken(); + return new Uri(request.GetAbsoluteRoot() + "/callbacks/blocks?token=" + token); + } + + public async Task RegisterCallbackBlockUriAsync(HttpRequest request) + { + var uri = await GetCallbackBlockUriAsync(request); + await _Explorer.SubscribeToBlocksAsync(uri); + return uri; + } + } +} diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 693174264..64e9fab47 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -36,6 +36,7 @@ using BTCPayServer.Validations; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc.Routing; +using NBXplorer.DerivationStrategy; namespace BTCPayServer.Controllers { @@ -98,7 +99,7 @@ namespace BTCPayServer.Controllers entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency); entity.PosData = invoice.PosData; - entity.DepositAddress = await _Wallet.ReserveAddressAsync(derivationStrategy); + entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy)); entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id); @@ -107,6 +108,11 @@ namespace BTCPayServer.Controllers return new DataWrapper(resp) { Facade = "pos/invoice" }; } + private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy) + { + return new DerivationStrategyFactory(_Network).Parse(derivationStrategy); + } + private TDest Map(TFrom data) { return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(data)); diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index fc8f71571..a9cd7bcf4 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -26,6 +26,7 @@ namespace BTCPayServer.Controllers public StoresController( StoreRepository repo, TokenRepository tokenRepo, + CallbackController callbackController, UserManager userManager, AccessTokenController tokenController, BTCPayWallet wallet, @@ -39,8 +40,10 @@ namespace BTCPayServer.Controllers _Wallet = wallet; _Env = env; _Network = network; + _CallbackController = callbackController; } Network _Network; + CallbackController _CallbackController; BTCPayWallet _Wallet; AccessTokenController _TokenController; StoreRepository _Repo; @@ -86,7 +89,7 @@ namespace BTCPayServer.Controllers StoresViewModel result = new StoresViewModel(); result.StatusMessage = StatusMessage; var stores = await _Repo.GetStoresByUserId(GetUserId()); - var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(s.DerivationStrategy)).ToArray(); + var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy))).ToArray(); for(int i = 0; i < stores.Length; i++) { @@ -156,7 +159,9 @@ namespace BTCPayServer.Controllers needUpdate = true; try { - await _Wallet.TrackAsync(model.DerivationScheme); + var strategy = ParseDerivationStrategy(model.DerivationScheme); + await _Wallet.TrackAsync(strategy); + await _CallbackController.RegisterCallbackUriAsync(strategy, Request); store.DerivationStrategy = model.DerivationScheme; } catch @@ -192,6 +197,11 @@ namespace BTCPayServer.Controllers } } + private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme) + { + return new DerivationStrategyFactory(_Network).Parse(derivationScheme); + } + [HttpGet] [Route("{storeId}/Tokens")] public async Task ListTokens(string storeId) diff --git a/BTCPayServer/Data/ApplicationDbContext.cs b/BTCPayServer/Data/ApplicationDbContext.cs index f3a494552..9a83baaa8 100644 --- a/BTCPayServer/Data/ApplicationDbContext.cs +++ b/BTCPayServer/Data/ApplicationDbContext.cs @@ -26,6 +26,10 @@ namespace BTCPayServer.Data get; set; } + public DbSet PendingInvoices + { + get; set; + } public DbSet RefundAddresses { get; set; diff --git a/BTCPayServer/Data/PendingInvoiceData.cs b/BTCPayServer/Data/PendingInvoiceData.cs new file mode 100644 index 000000000..7dff807bc --- /dev/null +++ b/BTCPayServer/Data/PendingInvoiceData.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Data +{ + public class PendingInvoiceData + { + public string Id + { + get; set; + } + } +} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 801157206..11a10070d 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -140,6 +140,7 @@ namespace BTCPayServer.Hosting services.TryAddScoped(); services.TryAddSingleton(); services.AddTransient(); + services.AddTransient(); // Add application services. services.AddTransient(); diff --git a/BTCPayServer/Hosting/BTCpayMiddleware.cs b/BTCPayServer/Hosting/BTCpayMiddleware.cs index 5e3e566bd..e2f2731bd 100644 --- a/BTCPayServer/Hosting/BTCpayMiddleware.cs +++ b/BTCPayServer/Hosting/BTCpayMiddleware.cs @@ -21,6 +21,7 @@ using BTCPayServer.Configuration; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Http.Extensions; +using BTCPayServer.Controllers; namespace BTCPayServer.Hosting { @@ -28,14 +29,27 @@ namespace BTCPayServer.Hosting { TokenRepository _TokenRepository; RequestDelegate _Next; - public BTCPayMiddleware(RequestDelegate next, TokenRepository tokenRepo) + CallbackController _CallbackController; + public BTCPayMiddleware(RequestDelegate next, + TokenRepository tokenRepo, + CallbackController callbackController) { _TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo)); _Next = next ?? throw new ArgumentNullException(nameof(next)); + _CallbackController = callbackController; } + + bool _Registered; public async Task Invoke(HttpContext httpContext) { + if(!_Registered) + { + var callback = await _CallbackController.RegisterCallbackBlockUriAsync(httpContext.Request); + Logs.PayServer.LogInformation($"Registering block callback to " + callback); + _Registered = true; + } + httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values); var sig = values.FirstOrDefault(); httpContext.Request.Headers.TryGetValue("x-identity", out values); diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 3cb77e549..dcb995d2e 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -33,6 +33,7 @@ using Hangfire.Dashboard; using Hangfire.Annotations; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Threading; +using Microsoft.Extensions.Options; namespace BTCPayServer.Hosting { @@ -59,6 +60,7 @@ namespace BTCPayServer.Hosting { get; set; } + public void ConfigureServices(IServiceCollection services) { services.ConfigureBTCPayServer(Configuration); diff --git a/BTCPayServer/Migrations/20171012020112_PendingInvoices.Designer.cs b/BTCPayServer/Migrations/20171012020112_PendingInvoices.Designer.cs new file mode 100644 index 000000000..fe3429d46 --- /dev/null +++ b/BTCPayServer/Migrations/20171012020112_PendingInvoices.Designer.cs @@ -0,0 +1,450 @@ +// +using BTCPayServer.Data; +using BTCPayServer.Servcices.Invoices; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20171012020112_PendingInvoices")] + partial class PendingInvoices + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .ValueGeneratedOnAdd(); + + b.Property("InvoiceDataId"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Created"); + + b.Property("CustomerEmail"); + + b.Property("ExceptionStatus"); + + b.Property("ItemCode"); + + b.Property("OrderId"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("PairingTime"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateCreated"); + + b.Property("Expiration"); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.Property("TokenValue"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DerivationStrategy"); + + b.Property("SpeedPolicy"); + + b.Property("StoreCertificate"); + + b.Property("StoreName"); + + b.Property("StoreWebsite"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId"); + + b.Property("StoreDataId"); + + b.Property("Role"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RequiresEmailConfirmation"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany() + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany() + .HasForeignKey("StoreDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Payments") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs b/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs new file mode 100644 index 000000000..99453ad00 --- /dev/null +++ b/BTCPayServer/Migrations/20171012020112_PendingInvoices.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace BTCPayServer.Migrations +{ + public partial class PendingInvoices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Name", + table: "PairingCodes"); + + migrationBuilder.DropColumn( + name: "Name", + table: "PairedSINData"); + + migrationBuilder.CreateTable( + name: "PendingInvoices", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PendingInvoices", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PendingInvoices"); + + migrationBuilder.AddColumn( + name: "Name", + table: "PairingCodes", + nullable: true); + + migrationBuilder.AddColumn( + name: "Name", + table: "PairedSINData", + nullable: true); + } + } +} diff --git a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs index c0283b9a5..b0ec6b352 100644 --- a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -71,8 +71,6 @@ namespace BTCPayServer.Migrations b.Property("Label"); - b.Property("Name"); - b.Property("PairingTime"); b.Property("SIN"); @@ -101,8 +99,6 @@ namespace BTCPayServer.Migrations b.Property("Label"); - b.Property("Name"); - b.Property("SIN"); b.Property("StoreDataId"); @@ -130,6 +126,16 @@ namespace BTCPayServer.Migrations b.ToTable("Payments"); }); + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => { b.Property("Id") diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index f0251ecf2..49f280458 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -56,38 +56,31 @@ namespace BTCPayServer.Servcices.Invoices - public Task AddPendingInvoice(string invoiceId) + public async Task AddPendingInvoice(string invoiceId) { - using(var tx = _Engine.GetTransaction()) + using(var ctx = _ContextFactory.CreateContext()) { - tx.Insert("T-Pending", invoiceId, new byte[0]); - tx.Commit(); + ctx.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId }); + await ctx.SaveChangesAsync(); } - return Task.FromResult(true); } - public Task RemovePendingInvoice(string invoiceId) + public async Task RemovePendingInvoice(string invoiceId) { - using(var tx = _Engine.GetTransaction()) + using(var ctx = _ContextFactory.CreateContext()) { - tx.RemoveKey("T-Pending", invoiceId); - tx.Commit(); + ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId }); + await ctx.SaveChangesAsync(); } - return Task.FromResult(true); } - public string[] GetPendingInvoices() + public async Task GetPendingInvoices() { - List pending = new List(); - using(var tx = _Engine.GetTransaction()) + using(var ctx = _ContextFactory.CreateContext()) { - foreach(var row in tx.SelectForward("T-Pending")) - { - pending.Add(row.Key); - } + return await ctx.PendingInvoices.Select(p => p.Id).ToArrayAsync(); } - return pending.ToArray(); } public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data) diff --git a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs b/BTCPayServer/Services/Invoices/InvoiceWatcher.cs index 67e963fa1..8492476eb 100644 --- a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs +++ b/BTCPayServer/Services/Invoices/InvoiceWatcher.cs @@ -36,9 +36,23 @@ namespace BTCPayServer.Servcices.Invoices _NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager)); } - private async Task StartWatchInvoice(string invoiceId) + public async Task NotifyReceived(Script scriptPubKey) { - Logs.PayServer.LogInformation("Watching invoice " + invoiceId); + var invoice = await _Wallet.GetInvoiceId(scriptPubKey); + _WatchRequests.Add(invoice); + } + + public async Task NotifyBlock() + { + foreach(var invoice in await _InvoiceRepository.GetPendingInvoices()) + { + _WatchRequests.Add(invoice); + } + } + + private async Task UpdateInvoice(string invoiceId) + { + Logs.PayServer.LogInformation("Updating invoice " + invoiceId); UTXOChanges changes = null; while(true) { @@ -53,7 +67,8 @@ namespace BTCPayServer.Servcices.Invoices if(result.NeedSave) await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false); - if(stateBefore != invoice.Status) + var changed = stateBefore != invoice.Status; + if(changed) { Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}"); } @@ -64,6 +79,9 @@ namespace BTCPayServer.Servcices.Invoices Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); break; } + + if(!changed || _Cts.Token.IsCancellationRequested) + break; } catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested) { @@ -96,7 +114,7 @@ namespace BTCPayServer.Servcices.Invoices if(invoice.Status == "new" || invoice.Status == "paidPartial") { var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); - changes = await _ExplorerClient.SyncAsync(strategy, changes, false, _Cts.Token).ConfigureAwait(false); + changes = await _ExplorerClient.SyncAsync(strategy, changes, true, _Cts.Token).ConfigureAwait(false); var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray(); @@ -232,34 +250,39 @@ namespace BTCPayServer.Servcices.Invoices Thread _Thread; TaskCompletionSource _RunningTask; CancellationTokenSource _Cts; + Timer _UpdatePendingInvoices; + public Task StartAsync(CancellationToken cancellationToken) { - foreach(var pending in _InvoiceRepository.GetPendingInvoices()) - { - _WatchRequests.Add(pending); - } _RunningTask = new TaskCompletionSource(); _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _Thread = new Thread(Run) { Name = "InvoiceWatcher" }; _Thread.Start(); + _UpdatePendingInvoices = new Timer(async s => + { + foreach(var pending in await _InvoiceRepository.GetPendingInvoices()) + { + _WatchRequests.Add(pending); + } + }, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds); return Task.CompletedTask; } void Run() { Logs.PayServer.LogInformation("Start watching invoices"); - List watching = new List(); + ConcurrentDictionary> updating = new ConcurrentDictionary>(); try { foreach(var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token)) { - watching.Add(StartWatchInvoice(item)); - foreach(var task in watching.ToList()) + var localItem = item; + + // If the invoice is already updating, ignore + Lazy updateInvoice =new Lazy(() => UpdateInvoice(localItem), false); + if(updating.TryAdd(item, updateInvoice)) { - if(task.Status != TaskStatus.Running) - { - watching.Remove(task); - } + updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice)); } } } @@ -267,7 +290,7 @@ namespace BTCPayServer.Servcices.Invoices { try { - Task.WaitAll(watching.ToArray()); + Task.WaitAll(updating.Select(c => c.Value.Value).ToArray()); } catch(AggregateException) { } _RunningTask.TrySetResult(true); @@ -287,6 +310,7 @@ namespace BTCPayServer.Servcices.Invoices public Task StopAsync(CancellationToken cancellationToken) { + _UpdatePendingInvoices.Dispose(); _Cts.Cancel(); return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken)); } diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index 3c4b1a84e..c52d4ab7b 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -14,7 +14,6 @@ namespace BTCPayServer.Services.Wallets { private ExplorerClient _Client; private Serializer _Serializer; - private DerivationStrategyFactory _DerivationStrategyFactory; ApplicationDbContextFactory _DBFactory; public BTCPayWallet(ExplorerClient client, ApplicationDbContextFactory factory) @@ -26,19 +25,18 @@ namespace BTCPayServer.Services.Wallets _Client = client; _DBFactory = factory; _Serializer = new NBXplorer.Serializer(_Client.Network); - _DerivationStrategyFactory = new DerivationStrategyFactory(_Client.Network); } - public async Task ReserveAddressAsync(string walletIdentifier) + public async Task ReserveAddressAsync(DerivationStrategyBase derivationStrategy) { - var pathInfo = await _Client.GetUnusedAsync(_DerivationStrategyFactory.Parse(walletIdentifier), DerivationFeature.Deposit, 0, true).ConfigureAwait(false); - return pathInfo.ScriptPubKey.GetDestinationAddress(_DerivationStrategyFactory.Network); + var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false); + return pathInfo.ScriptPubKey.GetDestinationAddress(_Client.Network); } - public Task TrackAsync(string walletIdentifier) + public async Task TrackAsync(DerivationStrategyBase derivationStrategy) { - return _Client.TrackAsync(_DerivationStrategyFactory.Parse(walletIdentifier)); + await _Client.TrackAsync(derivationStrategy); } public async Task GetInvoiceId(Script scriptPubKey) @@ -74,9 +72,9 @@ namespace BTCPayServer.Services.Wallets return Task.WhenAll(tasks); } - public async Task GetBalance(string derivationStrategy) + public async Task GetBalance(DerivationStrategyBase derivationStrategy) { - var result = await _Client.SyncAsync(_DerivationStrategyFactory.Parse(derivationStrategy), null, true); + var result = await _Client.SyncAsync(derivationStrategy, null, true); return result.Confirmed.UTXOs.Select(u => u.Output.Value) .Concat(result.Unconfirmed.UTXOs.Select(u => u.Output.Value)) .Sum();