Use callback to update invoice state instead of long polling

This commit is contained in:
nicolas.dorier
2017-10-12 16:33:53 +09:00
parent 212a816598
commit bae08b6966
21 changed files with 936 additions and 63 deletions

View File

@@ -64,16 +64,20 @@ namespace BTCPayServer.Tests
} }
IWebHost _Host; IWebHost _Host;
public int Port
{
get; set;
}
public void Start() public void Start()
{ {
if(!Directory.Exists(_Directory)) if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory); Directory.CreateDirectory(_Directory);
HDPrivateKey = new ExtKey(); HDPrivateKey = new ExtKey();
var port = Utils.FreeTcpPort();
StringBuilder config = new StringBuilder(); StringBuilder config = new StringBuilder();
config.AppendLine($"regtest=1"); config.AppendLine($"regtest=1");
config.AppendLine($"port={port}"); config.AppendLine($"port={Port}");
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}"); config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"explorer.cookiefile={CookieFile}"); config.AppendLine($"explorer.cookiefile={CookieFile}");
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}"); config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
@@ -81,7 +85,7 @@ namespace BTCPayServer.Tests
config.AppendLine($"postgres=" + Postgres); config.AppendLine($"postgres=" + Postgres);
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString()); 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 }); var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
@@ -94,6 +98,7 @@ namespace BTCPayServer.Tests
{ {
l.SetMinimumLevel(LogLevel.Information) l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error) .AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddProvider(Logs.LogProvider); .AddProvider(Logs.LogProvider);
}); });
}) })
@@ -110,6 +115,11 @@ namespace BTCPayServer.Tests
{ {
get; set; get; set;
} }
public string HostName
{
get;
internal set;
}
public T GetController<T>(string userId = null) where T : Controller public T GetController<T>(string userId = null) where T : Controller
{ {
@@ -129,7 +139,8 @@ namespace BTCPayServer.Tests
httpAccessor.HttpContext = context; httpAccessor.HttpContext = context;
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T)); 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() controller.ControllerContext = new ControllerContext()
{ {
HttpContext = context HttpContext = context

View File

@@ -8,16 +8,21 @@ namespace BTCPayServer.Tests.Mocks
{ {
public class UrlHelperMock : IUrlHelper public class UrlHelperMock : IUrlHelper
{ {
Uri _BaseUrl;
public UrlHelperMock(Uri baseUrl)
{
_BaseUrl = baseUrl;
}
public ActionContext ActionContext => throw new NotImplementedException(); public ActionContext ActionContext => throw new NotImplementedException();
public string Action(UrlActionContext actionContext) public string Action(UrlActionContext actionContext)
{ {
return "http://127.0.0.1/mock"; return $"{_BaseUrl}mock";
} }
public string Content(string contentPath) public string Content(string contentPath)
{ {
return "http://127.0.0.1/mock"; return $"{_BaseUrl}{contentPath}";
} }
public bool IsLocalUrl(string url) public bool IsLocalUrl(string url)
@@ -27,12 +32,12 @@ namespace BTCPayServer.Tests.Mocks
public string Link(string routeName, object values) public string Link(string routeName, object values)
{ {
return "http://127.0.0.1/mock"; return _BaseUrl.AbsoluteUri;
} }
public string RouteUrl(UrlRouteContext routeContext) public string RouteUrl(UrlRouteContext routeContext)
{ {
return "http://127.0.0.1/mock"; return _BaseUrl.AbsoluteUri;
} }
} }
} }

View File

@@ -0,0 +1,7 @@
{
"profiles": {
"BTCPayServer.Tests": {
"commandName": "Project"
}
}
}

View File

@@ -1,17 +1,21 @@
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Models.AccountViewModels; using BTCPayServer.Models.AccountViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NBitcoin; using NBitcoin;
using NBitcoin.RPC; using NBitcoin.RPC;
using NBitpayClient; using NBitpayClient;
using NBXplorer; using NBXplorer;
using NBXplorer.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@@ -28,6 +32,11 @@ namespace BTCPayServer.Tests
_Directory = scope; _Directory = scope;
} }
public bool Dockerized
{
get; set;
}
public void Start() public void Start()
{ {
if(Directory.Exists(_Directory)) if(Directory.Exists(_Directory))
@@ -35,6 +44,8 @@ namespace BTCPayServer.Tests
if(!Directory.Exists(_Directory)) if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_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); 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/"))); ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/")));
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay")) PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
@@ -42,6 +53,8 @@ namespace BTCPayServer.Tests
NBXplorerUri = ExplorerClient.Address, NBXplorerUri = ExplorerClient.Address,
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver") 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(); PayTester.Start();
} }
@@ -56,6 +69,11 @@ namespace BTCPayServer.Tests
return new TestAccount(this); return new TestAccount(this);
} }
public bool FakeCallback
{
get;
set;
}
public RPCClient ExplorerNode public RPCClient ExplorerNode
{ {
get; set; get; set;
@@ -66,6 +84,136 @@ namespace BTCPayServer.Tests
get; set; 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<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}
/// <summary>
/// Simulating callback from NBXplorer. NBXplorer can't reach the host during tests as it is not running on localhost.
/// </summary>
/// <param name="address"></param>
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<CallbackController>();
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 public BTCPayServerTester PayTester

View File

@@ -99,6 +99,7 @@ namespace BTCPayServer.Tests
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(url.Address);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status); Assert.Equal("paid", localInvoice.Status);
Assert.True(localInvoice.Refundable); Assert.True(localInvoice.Refundable);
@@ -153,6 +154,8 @@ namespace BTCPayServer.Tests
}); });
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21); BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount); tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
Thread.Sleep(5000);
tester.SimulateCallback(url.Address);
callbackServer.ProcessNextRequest((ctx) => callbackServer.ProcessNextRequest((ctx) =>
{ {
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd(); var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
@@ -228,6 +231,7 @@ namespace BTCPayServer.Tests
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paidPartial", localInvoice.Status); Assert.Equal("paidPartial", localInvoice.Status);
Assert.Equal(firstPayment, localInvoice.BtcPaid); Assert.Equal(firstPayment, localInvoice.BtcPaid);
@@ -240,6 +244,7 @@ namespace BTCPayServer.Tests
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status); Assert.Equal("paid", localInvoice.Status);
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid); Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
@@ -251,6 +256,7 @@ namespace BTCPayServer.Tests
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status); Assert.Equal("confirmed", localInvoice.Status);
}); });
@@ -259,6 +265,7 @@ namespace BTCPayServer.Tests
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status); Assert.Equal("complete", localInvoice.Status);
}); });
@@ -280,6 +287,7 @@ namespace BTCPayServer.Tests
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paidOver", localInvoice.Status); Assert.Equal("paidOver", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue); Assert.Equal(Money.Zero, localInvoice.BtcDue);
@@ -290,6 +298,7 @@ namespace BTCPayServer.Tests
Eventually(() => Eventually(() =>
{ {
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status); Assert.Equal("confirmed", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue); Assert.Equal(Money.Zero, localInvoice.BtcDue);

View File

@@ -10,11 +10,18 @@ services:
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3 TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_NBXPLORERURL: http://nbxplorer:32838/ TESTS_NBXPLORERURL: http://nbxplorer:32838/
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_FAKECALLBACK: 'true'
TESTS_PORT: 80
TESTS_HOSTNAME: tests
expose:
- "80"
links: links:
- nbxplorer - nbxplorer
extra_hosts:
- "tests:127.0.0.1"
nbxplorer: nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.16 image: nicolasdorier/nbxplorer:1.0.0.18
ports: ports:
- "32838:32838" - "32838:32838"
expose: expose:

View File

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

View File

@@ -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<TransactionMatch>(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<CallbackSettings>();
if(await GetToken() != token)
throw new BTCPayServer.BitpayHttpException(400, "invalid-callback-token");
}
public async Task<Uri> 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<string> GetToken()
{
var callback = await _Settings.GetSettingAsync<CallbackSettings>();
if(callback == null)
{
callback = new CallbackSettings() { Token = Guid.NewGuid().ToString() };
await _Settings.UpdateSetting(callback);
}
var token = callback.Token;
return token;
}
public async Task<Uri> GetCallbackBlockUriAsync(HttpRequest request)
{
string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/blocks?token=" + token);
}
public async Task<Uri> RegisterCallbackBlockUriAsync(HttpRequest request)
{
var uri = await GetCallbackBlockUriAsync(request);
await _Explorer.SubscribeToBlocksAsync(uri);
return uri;
}
}
}

View File

@@ -36,6 +36,7 @@ using BTCPayServer.Validations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -98,7 +99,7 @@ namespace BTCPayServer.Controllers
entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency); entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
entity.PosData = invoice.PosData; entity.PosData = invoice.PosData;
entity.DepositAddress = await _Wallet.ReserveAddressAsync(derivationStrategy); entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id); await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id);
@@ -107,6 +108,11 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" }; return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
} }
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
{
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
}
private TDest Map<TFrom, TDest>(TFrom data) private TDest Map<TFrom, TDest>(TFrom data)
{ {
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data)); return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));

View File

@@ -26,6 +26,7 @@ namespace BTCPayServer.Controllers
public StoresController( public StoresController(
StoreRepository repo, StoreRepository repo,
TokenRepository tokenRepo, TokenRepository tokenRepo,
CallbackController callbackController,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
AccessTokenController tokenController, AccessTokenController tokenController,
BTCPayWallet wallet, BTCPayWallet wallet,
@@ -39,8 +40,10 @@ namespace BTCPayServer.Controllers
_Wallet = wallet; _Wallet = wallet;
_Env = env; _Env = env;
_Network = network; _Network = network;
_CallbackController = callbackController;
} }
Network _Network; Network _Network;
CallbackController _CallbackController;
BTCPayWallet _Wallet; BTCPayWallet _Wallet;
AccessTokenController _TokenController; AccessTokenController _TokenController;
StoreRepository _Repo; StoreRepository _Repo;
@@ -86,7 +89,7 @@ namespace BTCPayServer.Controllers
StoresViewModel result = new StoresViewModel(); StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage; result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId()); 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++) for(int i = 0; i < stores.Length; i++)
{ {
@@ -156,7 +159,9 @@ namespace BTCPayServer.Controllers
needUpdate = true; needUpdate = true;
try try
{ {
await _Wallet.TrackAsync(model.DerivationScheme); var strategy = ParseDerivationStrategy(model.DerivationScheme);
await _Wallet.TrackAsync(strategy);
await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
store.DerivationStrategy = model.DerivationScheme; store.DerivationStrategy = model.DerivationScheme;
} }
catch catch
@@ -192,6 +197,11 @@ namespace BTCPayServer.Controllers
} }
} }
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme)
{
return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
}
[HttpGet] [HttpGet]
[Route("{storeId}/Tokens")] [Route("{storeId}/Tokens")]
public async Task<IActionResult> ListTokens(string storeId) public async Task<IActionResult> ListTokens(string storeId)

View File

@@ -26,6 +26,10 @@ namespace BTCPayServer.Data
get; set; get; set;
} }
public DbSet<PendingInvoiceData> PendingInvoices
{
get; set;
}
public DbSet<RefundAddressesData> RefundAddresses public DbSet<RefundAddressesData> RefundAddresses
{ {
get; set; get; set;

View File

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

View File

@@ -140,6 +140,7 @@ namespace BTCPayServer.Hosting
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>(); services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>(); services.AddTransient<AccessTokenController>();
services.AddTransient<CallbackController>();
// Add application services. // Add application services.
services.AddTransient<IEmailSender, EmailSender>(); services.AddTransient<IEmailSender, EmailSender>();

View File

@@ -21,6 +21,7 @@ using BTCPayServer.Configuration;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using BTCPayServer.Controllers;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
@@ -28,14 +29,27 @@ namespace BTCPayServer.Hosting
{ {
TokenRepository _TokenRepository; TokenRepository _TokenRepository;
RequestDelegate _Next; 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)); _TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next)); _Next = next ?? throw new ArgumentNullException(nameof(next));
_CallbackController = callbackController;
} }
bool _Registered;
public async Task Invoke(HttpContext httpContext) 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); httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault(); var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values); httpContext.Request.Headers.TryGetValue("x-identity", out values);

View File

@@ -33,6 +33,7 @@ using Hangfire.Dashboard;
using Hangfire.Annotations; using Hangfire.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Threading; using System.Threading;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
@@ -59,6 +60,7 @@ namespace BTCPayServer.Hosting
{ {
get; set; get; set;
} }
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.ConfigureBTCPayServer(Configuration); services.ConfigureBTCPayServer(Configuration);

View File

@@ -0,0 +1,450 @@
// <auto-generated />
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<string>("Address")
.ValueGeneratedOnAdd();
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("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<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -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<string>(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<string>(
name: "Name",
table: "PairingCodes",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Name",
table: "PairedSINData",
nullable: true);
}
}
}

View File

@@ -71,8 +71,6 @@ namespace BTCPayServer.Migrations
b.Property<string>("Label"); b.Property<string>("Label");
b.Property<string>("Name");
b.Property<DateTimeOffset>("PairingTime"); b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN"); b.Property<string>("SIN");
@@ -101,8 +99,6 @@ namespace BTCPayServer.Migrations
b.Property<string>("Label"); b.Property<string>("Label");
b.Property<string>("Name");
b.Property<string>("SIN"); b.Property<string>("SIN");
b.Property<string>("StoreDataId"); b.Property<string>("StoreDataId");
@@ -130,6 +126,16 @@ namespace BTCPayServer.Migrations
b.ToTable("Payments"); b.ToTable("Payments");
}); });
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")

View File

@@ -56,39 +56,32 @@ 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<string, byte[]>("T-Pending", invoiceId, new byte[0]); ctx.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId });
tx.Commit(); 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); ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
tx.Commit(); await ctx.SaveChangesAsync();
} }
return Task.FromResult(true);
} }
public string[] GetPendingInvoices() public async Task<string[]> GetPendingInvoices()
{ {
List<string> pending = new List<string>(); using(var ctx = _ContextFactory.CreateContext())
using(var tx = _Engine.GetTransaction())
{ {
foreach(var row in tx.SelectForward<string, byte[]>("T-Pending")) return await ctx.PendingInvoices.Select(p => p.Id).ToArrayAsync();
{
pending.Add(row.Key);
} }
} }
return pending.ToArray();
}
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data) public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
{ {

View File

@@ -36,9 +36,23 @@ namespace BTCPayServer.Servcices.Invoices
_NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager)); _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; UTXOChanges changes = null;
while(true) while(true)
{ {
@@ -53,7 +67,8 @@ namespace BTCPayServer.Servcices.Invoices
if(result.NeedSave) if(result.NeedSave)
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false); 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}"); Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
} }
@@ -64,6 +79,9 @@ namespace BTCPayServer.Servcices.Invoices
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
break; break;
} }
if(!changed || _Cts.Token.IsCancellationRequested)
break;
} }
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested) catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested)
{ {
@@ -96,7 +114,7 @@ namespace BTCPayServer.Servcices.Invoices
if(invoice.Status == "new" || invoice.Status == "paidPartial") if(invoice.Status == "new" || invoice.Status == "paidPartial")
{ {
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); 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 utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray(); var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
@@ -232,34 +250,39 @@ namespace BTCPayServer.Servcices.Invoices
Thread _Thread; Thread _Thread;
TaskCompletionSource<bool> _RunningTask; TaskCompletionSource<bool> _RunningTask;
CancellationTokenSource _Cts; CancellationTokenSource _Cts;
Timer _UpdatePendingInvoices;
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
foreach(var pending in _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(pending);
}
_RunningTask = new TaskCompletionSource<bool>(); _RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Thread = new Thread(Run) { Name = "InvoiceWatcher" }; _Thread = new Thread(Run) { Name = "InvoiceWatcher" };
_Thread.Start(); _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; return Task.CompletedTask;
} }
void Run() void Run()
{ {
Logs.PayServer.LogInformation("Start watching invoices"); Logs.PayServer.LogInformation("Start watching invoices");
List<Task> watching = new List<Task>(); ConcurrentDictionary<string, Lazy<Task>> updating = new ConcurrentDictionary<string, Lazy<Task>>();
try try
{ {
foreach(var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token)) foreach(var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
{ {
watching.Add(StartWatchInvoice(item)); var localItem = item;
foreach(var task in watching.ToList())
// If the invoice is already updating, ignore
Lazy<Task> updateInvoice =new Lazy<Task>(() => UpdateInvoice(localItem), false);
if(updating.TryAdd(item, updateInvoice))
{ {
if(task.Status != TaskStatus.Running) updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice));
{
watching.Remove(task);
}
} }
} }
} }
@@ -267,7 +290,7 @@ namespace BTCPayServer.Servcices.Invoices
{ {
try try
{ {
Task.WaitAll(watching.ToArray()); Task.WaitAll(updating.Select(c => c.Value.Value).ToArray());
} }
catch(AggregateException) { } catch(AggregateException) { }
_RunningTask.TrySetResult(true); _RunningTask.TrySetResult(true);
@@ -287,6 +310,7 @@ namespace BTCPayServer.Servcices.Invoices
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
_UpdatePendingInvoices.Dispose();
_Cts.Cancel(); _Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken)); return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
} }

View File

@@ -14,7 +14,6 @@ namespace BTCPayServer.Services.Wallets
{ {
private ExplorerClient _Client; private ExplorerClient _Client;
private Serializer _Serializer; private Serializer _Serializer;
private DerivationStrategyFactory _DerivationStrategyFactory;
ApplicationDbContextFactory _DBFactory; ApplicationDbContextFactory _DBFactory;
public BTCPayWallet(ExplorerClient client, ApplicationDbContextFactory factory) public BTCPayWallet(ExplorerClient client, ApplicationDbContextFactory factory)
@@ -26,19 +25,18 @@ namespace BTCPayServer.Services.Wallets
_Client = client; _Client = client;
_DBFactory = factory; _DBFactory = factory;
_Serializer = new NBXplorer.Serializer(_Client.Network); _Serializer = new NBXplorer.Serializer(_Client.Network);
_DerivationStrategyFactory = new DerivationStrategyFactory(_Client.Network);
} }
public async Task<BitcoinAddress> ReserveAddressAsync(string walletIdentifier) public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
{ {
var pathInfo = await _Client.GetUnusedAsync(_DerivationStrategyFactory.Parse(walletIdentifier), DerivationFeature.Deposit, 0, true).ConfigureAwait(false); var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
return pathInfo.ScriptPubKey.GetDestinationAddress(_DerivationStrategyFactory.Network); 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<string> GetInvoiceId(Script scriptPubKey) public async Task<string> GetInvoiceId(Script scriptPubKey)
@@ -74,9 +72,9 @@ namespace BTCPayServer.Services.Wallets
return Task.WhenAll(tasks); return Task.WhenAll(tasks);
} }
public async Task<Money> GetBalance(string derivationStrategy) public async Task<Money> 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) return result.Confirmed.UTXOs.Select(u => u.Output.Value)
.Concat(result.Unconfirmed.UTXOs.Select(u => u.Output.Value)) .Concat(result.Unconfirmed.UTXOs.Select(u => u.Output.Value))
.Sum(); .Sum();