mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-03 17:36:59 +01:00
Fix Token permissions (merchant facade > pos facade) + Add IPN + Add Hangfire integration
This commit is contained in:
parent
c08d72b984
commit
3304d11da8
18 changed files with 425 additions and 75 deletions
|
@ -7,7 +7,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170628-02" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170720-02" />
|
||||
<PackageReference Include="NBitcoin.TestFramework" Version="1.4.4" />
|
||||
<PackageReference Include="xunit" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
|
|
75
BTCPayServer.Tests/CustomerHttpServer.cs
Normal file
75
BTCPayServer.Tests/CustomerHttpServer.cs
Normal file
|
@ -0,0 +1,75 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CustomServer : IDisposable
|
||||
{
|
||||
TaskCompletionSource<bool> _Evt = null;
|
||||
IWebHost _Host = null;
|
||||
CancellationTokenSource _Closed = new CancellationTokenSource();
|
||||
public CustomServer()
|
||||
{
|
||||
var port = Utils.FreeTcpPort();
|
||||
_Host = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(req =>
|
||||
{
|
||||
while(_Act == null)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
_Closed.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
_Act(req);
|
||||
_Act = null;
|
||||
_Evt.TrySetResult(true);
|
||||
req.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseUrls("http://127.0.0.1:" + port)
|
||||
.Build();
|
||||
_Host.Start();
|
||||
}
|
||||
|
||||
public Uri GetUri()
|
||||
{
|
||||
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
|
||||
}
|
||||
|
||||
Action<HttpContext> _Act;
|
||||
public void ProcessNextRequest(Action<HttpContext> act)
|
||||
{
|
||||
var source = new TaskCompletionSource<bool>();
|
||||
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
|
||||
cancellation.Token.Register(() => source.TrySetCanceled());
|
||||
source = new TaskCompletionSource<bool>();
|
||||
_Evt = source;
|
||||
_Act = act;
|
||||
try
|
||||
{
|
||||
_Evt.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
catch(TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Closed.Cancel();
|
||||
_Host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,9 @@ using Xunit;
|
|||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
|
@ -101,6 +104,39 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendIPN()
|
||||
{
|
||||
using(var callbackServer = new CustomServer())
|
||||
{
|
||||
using(var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.CreateAccount();
|
||||
acc.GrantAccess();
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
NotificationURL = callbackServer.GetUri().AbsoluteUri,
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
});
|
||||
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
|
||||
tester.ExplorerNode.CreateRPCClient().SendToAddress(url.Address, url.Amount);
|
||||
callbackServer.ProcessNextRequest((ctx) =>
|
||||
{
|
||||
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
|
||||
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
|
||||
});
|
||||
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.NotNull(invoice2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
|
@ -142,7 +178,7 @@ namespace BTCPayServer.Tests
|
|||
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
|
||||
Assert.Equal("new", invoice.Status);
|
||||
Assert.Equal("false", invoice.ExceptionStatus);
|
||||
Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value);
|
||||
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length);
|
||||
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length);
|
||||
|
@ -181,7 +217,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("false", localInvoice.ExceptionStatus);
|
||||
Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
||||
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
|
||||
|
@ -220,7 +256,7 @@ namespace BTCPayServer.Tests
|
|||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paidOver", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", localInvoice.ExceptionStatus);
|
||||
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
||||
cashCow.Generate(1);
|
||||
|
@ -230,7 +266,7 @@ namespace BTCPayServer.Tests
|
|||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", localInvoice.ExceptionStatus);
|
||||
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
|
@ -40,5 +41,20 @@ namespace BTCPayServer.Authentication
|
|||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public BitTokenEntity Clone(Facade facade)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Active = Active,
|
||||
DateCreated = DateCreated,
|
||||
Label = Label,
|
||||
Name = Name,
|
||||
PairedId = PairedId,
|
||||
PairingTime = PairingTime,
|
||||
SIN = SIN,
|
||||
Value = Value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,7 +163,7 @@ namespace BTCPayServer.Authentication
|
|||
return true;
|
||||
}
|
||||
|
||||
public Task<BitTokenEntity> GetToken(string sin, string tokenName)
|
||||
private Task<BitTokenEntity> GetToken(string sin, string tokenName)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire" Version="1.6.17" />
|
||||
<PackageReference Include="Hangfire.SQLite" Version="1.4.2" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.38" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.6" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.9" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.12" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace BTCPayServer.Controllers
|
|||
if(invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
|
||||
var resp = EntityToDTO(invoice);
|
||||
var resp = invoice.EntityToDTO(_ExternalUrl);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
|
||||
|
@ -73,25 +73,42 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select(EntityToDTO).ToArray();
|
||||
.Select((o) => o.EntityToDTO(_ExternalUrl)).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string exptectedToken)
|
||||
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
|
||||
{
|
||||
if(facade == null)
|
||||
throw new ArgumentNullException(nameof(facade));
|
||||
|
||||
var actualToken = await _TokenRepository.GetToken(this.GetBitIdentity().SIN, facade.ToString());
|
||||
if(exptectedToken == null || actualToken == null || !actualToken.Value.Equals(exptectedToken, StringComparison.Ordinal))
|
||||
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).Where(t => t.Active).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if(expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
|
||||
throw new BitpayHttpException(401, "This endpoint does not support the `user` facade");
|
||||
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Name).Concat(new[] { "user" }).FirstOrDefault()}` facade");
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if(token.Name == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if(token.Name == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
|
||||
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(bitToken.PairedId);
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace BTCPayServer.Controllers
|
|||
if(invoice == null || invoice.IsExpired())
|
||||
return NotFound();
|
||||
|
||||
var dto = EntityToDTO(invoice);
|
||||
var dto = invoice.EntityToDTO(_ExternalUrl);
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace BTCPayServer.Controllers
|
|||
if(invoice == null)
|
||||
return NotFound();
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var dto = EntityToDTO(invoice);
|
||||
var dto = invoice.EntityToDTO(_ExternalUrl);
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
|
@ -163,7 +163,7 @@ namespace BTCPayServer.Controllers
|
|||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
|
|
|
@ -90,8 +90,14 @@ namespace BTCPayServer.Controllers
|
|||
InvoiceTime = DateTimeOffset.UtcNow,
|
||||
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
|
||||
};
|
||||
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
|
||||
if(notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime + TimeSpan.FromMinutes(15.0);
|
||||
entity.ServerUrl = _ExternalUrl.GetAbsolute("");
|
||||
entity.FullNotifications = invoice.FullNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.RefundMail = IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
|
@ -106,60 +112,13 @@ namespace BTCPayServer.Controllers
|
|||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
|
||||
await _Wallet.MapAsync(entity.DepositAddress, entity.Id);
|
||||
await _Watcher.WatchAsync(entity.Id);
|
||||
var resp = EntityToDTO(entity);
|
||||
var resp = entity.EntityToDTO(_ExternalUrl);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private InvoiceResponse EntityToDTO(InvoiceEntity entity)
|
||||
{
|
||||
InvoiceResponse dto = new InvoiceResponse
|
||||
{
|
||||
Id = entity.Id,
|
||||
OrderId = entity.OrderId,
|
||||
PosData = entity.PosData,
|
||||
CurrentTime = DateTimeOffset.UtcNow,
|
||||
InvoiceTime = entity.InvoiceTime,
|
||||
ExpirationTime = entity.ExpirationTime,
|
||||
BTCPrice = Money.Coins((decimal)(1.0 / entity.Rate)).ToString(),
|
||||
Status = entity.Status,
|
||||
Url = _ExternalUrl.GetAbsolute("invoice?id=" + entity.Id),
|
||||
Currency = entity.ProductInformation.Currency,
|
||||
Flags = new Flags() { Refundable = entity.Refundable }
|
||||
};
|
||||
Populate(entity.ProductInformation, dto);
|
||||
Populate(entity.BuyerInformation, dto);
|
||||
dto.ExRates = new Dictionary<string, double>
|
||||
{
|
||||
{ entity.ProductInformation.Currency, entity.Rate }
|
||||
};
|
||||
dto.PaymentUrls = new InvoicePaymentUrls()
|
||||
{
|
||||
BIP72 = $"bitcoin:{entity.DepositAddress}?amount={entity.GetCryptoDue()}&r={_ExternalUrl.GetAbsolute($"i/{entity.Id}")}",
|
||||
BIP72b = $"bitcoin:?r={_ExternalUrl.GetAbsolute($"i/{entity.Id}")}",
|
||||
BIP73 = _ExternalUrl.GetAbsolute($"i/{entity.Id}"),
|
||||
BIP21 = $"bitcoin:{entity.DepositAddress}?amount={entity.GetCryptoDue()}",
|
||||
};
|
||||
dto.BitcoinAddress = entity.DepositAddress.ToString();
|
||||
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
|
||||
dto.Guid = Guid.NewGuid().ToString();
|
||||
|
||||
var paid = entity.Payments.Select(p => p.Output.Value).Sum();
|
||||
dto.BTCPaid = paid.ToString();
|
||||
dto.BTCDue = entity.GetCryptoDue().ToString();
|
||||
dto.ExceptionStatus = entity.ExceptionStatus == null ? new JValue(false) : new JValue(entity.ExceptionStatus);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
}
|
||||
private void Populate<TFrom, TDest>(TFrom from, TDest dest)
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(from);
|
||||
JsonConvert.PopulateObject(str, dest);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,8 +49,6 @@ namespace BTCPayServer
|
|||
{
|
||||
if(url == null)
|
||||
throw new ArgumentNullException(nameof(url));
|
||||
if(contextAccessor == null)
|
||||
throw new ArgumentNullException(nameof(contextAccessor));
|
||||
_ContextAccessor = contextAccessor;
|
||||
_Url = url.AbsoluteUri;
|
||||
}
|
||||
|
@ -71,7 +69,7 @@ namespace BTCPayServer
|
|||
public string GetEncodedUrl()
|
||||
{
|
||||
var req = _ContextAccessor.HttpContext.Request;
|
||||
return BuildAbsolute(req.Path, req.QueryString); ;
|
||||
return BuildAbsolute(req.Path, req.QueryString);
|
||||
}
|
||||
|
||||
private string BuildAbsolute(PathString path = new PathString(),
|
||||
|
|
|
@ -95,6 +95,7 @@ namespace BTCPayServer.Hosting
|
|||
var path = Path.Combine(provider.GetRequiredService<BTCPayServerOptions>().DataDir, "sqllite.db");
|
||||
o.UseSqlite("Data Source=" + path);
|
||||
});
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
services.TryAddSingleton<IConfigureOptions<MvcOptions>, BTCPayServerConfigureOptions>();
|
||||
services.TryAddSingleton(o =>
|
||||
|
@ -130,6 +131,7 @@ namespace BTCPayServer.Hosting
|
|||
});
|
||||
services.TryAddSingleton<IRateProvider, BitpayRateProvider>();
|
||||
services.TryAddSingleton<InvoiceWatcher>();
|
||||
services.TryAddSingleton<InvoiceNotificationManager>();
|
||||
services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.TryAddSingleton<IExternalUrlProvider>(o =>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
|
@ -18,7 +19,7 @@ using Microsoft.AspNetCore.Identity;
|
|||
using BTCPayServer.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Hangfire;
|
||||
using Hangfire.SQLite;
|
||||
using Hangfire.MemoryStorage;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -31,6 +32,8 @@ using BTCPayServer.Configuration;
|
|||
using System.IO;
|
||||
using Hangfire.Dashboard;
|
||||
using Hangfire.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
|
@ -59,22 +62,48 @@ namespace BTCPayServer.Hosting
|
|||
}
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// Big hack, tests fails because Hangfire fail at initializing at the second test run
|
||||
|
||||
|
||||
services.ConfigureBTCPayServer(Configuration);
|
||||
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services.AddHangfire(o =>
|
||||
AddHangfireFix(services);
|
||||
services.AddBTCPayServer();
|
||||
services.AddMvc();
|
||||
}
|
||||
|
||||
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
|
||||
private void AddHangfireFix(IServiceCollection services)
|
||||
{
|
||||
Action<IGlobalConfiguration> configuration = o =>
|
||||
{
|
||||
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
|
||||
var options = (ApplicationDbContext)scope.Resolve(typeof(ApplicationDbContext));
|
||||
var path = Path.Combine(((BTCPayServerOptions)scope.Resolve(typeof(BTCPayServerOptions))).DataDir, "hangfire.db");
|
||||
o.UseSQLiteStorage("Data Source=" + path + ";");
|
||||
});
|
||||
services.AddBTCPayServer();
|
||||
services.AddMvc();
|
||||
o.UseMemoryStorage(); //SQLite provider can work with only one background job :/
|
||||
};
|
||||
|
||||
ServiceCollectionDescriptorExtensions.TryAddSingleton<Action<IGlobalConfiguration>>(services, (IServiceProvider serviceProvider) => new Action<IGlobalConfiguration>((config) =>
|
||||
{
|
||||
ILoggerFactory service = ServiceProviderServiceExtensions.GetService<ILoggerFactory>(serviceProvider);
|
||||
if(service != null)
|
||||
{
|
||||
Hangfire.GlobalConfigurationExtensions.UseLogProvider<AspNetCoreLogProvider>(config, new AspNetCoreLogProvider(service));
|
||||
}
|
||||
IServiceScopeFactory service2 = ServiceProviderServiceExtensions.GetService<IServiceScopeFactory>(serviceProvider);
|
||||
if(service2 != null)
|
||||
{
|
||||
Hangfire.GlobalConfigurationExtensions.UseActivator<AspNetCoreJobActivator>(config, new AspNetCoreJobActivator(service2));
|
||||
}
|
||||
configuration(config);
|
||||
}));
|
||||
services.AddHangfire(configuration);
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IHostingEnvironment env,
|
||||
|
@ -91,6 +120,7 @@ namespace BTCPayServer.Hosting
|
|||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseHangfireServer();
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
|
||||
app.UseMvc(routes =>
|
||||
|
|
|
@ -42,6 +42,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[Url]
|
||||
public string NotificationUrl
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public SelectList Stores
|
||||
{
|
||||
get;
|
||||
|
|
|
@ -4,6 +4,10 @@ using Newtonsoft.Json;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Models;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Servcices.Invoices
|
||||
{
|
||||
|
@ -229,12 +233,75 @@ namespace BTCPayServer.Servcices.Invoices
|
|||
get;
|
||||
set;
|
||||
}
|
||||
public bool FullNotifications
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string NotificationURL
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string ServerUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
return DateTimeOffset.UtcNow > ExpirationTime;
|
||||
}
|
||||
|
||||
|
||||
public InvoiceResponse EntityToDTO(IExternalUrlProvider urlProvider = null)
|
||||
{
|
||||
urlProvider = urlProvider ?? new FixedExternalUrlProvider(new Uri(ServerUrl, UriKind.Absolute), null);
|
||||
InvoiceResponse dto = new InvoiceResponse
|
||||
{
|
||||
Id = Id,
|
||||
OrderId = OrderId,
|
||||
PosData = PosData,
|
||||
CurrentTime = DateTimeOffset.UtcNow,
|
||||
InvoiceTime = InvoiceTime,
|
||||
ExpirationTime = ExpirationTime,
|
||||
BTCPrice = Money.Coins((decimal)(1.0 / Rate)).ToString(),
|
||||
Status = Status,
|
||||
Url = urlProvider.GetAbsolute("invoice?id=" + Id),
|
||||
Currency = ProductInformation.Currency,
|
||||
Flags = new Flags() { Refundable = Refundable }
|
||||
};
|
||||
Populate(ProductInformation, dto);
|
||||
Populate(BuyerInformation, dto);
|
||||
dto.ExRates = new Dictionary<string, double>
|
||||
{
|
||||
{ ProductInformation.Currency, Rate }
|
||||
};
|
||||
dto.PaymentUrls = new InvoicePaymentUrls()
|
||||
{
|
||||
BIP72 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}&r={urlProvider.GetAbsolute($"i/{Id}")}",
|
||||
BIP72b = $"bitcoin:?r={urlProvider.GetAbsolute($"i/{Id}")}",
|
||||
BIP73 = urlProvider.GetAbsolute($"i/{Id}"),
|
||||
BIP21 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}",
|
||||
};
|
||||
dto.BitcoinAddress = DepositAddress.ToString();
|
||||
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
|
||||
dto.Guid = Guid.NewGuid().ToString();
|
||||
|
||||
var paid = Payments.Select(p => p.Output.Value).Sum();
|
||||
dto.BTCPaid = paid.ToString();
|
||||
dto.BTCDue = GetCryptoDue().ToString();
|
||||
|
||||
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void Populate<TFrom, TDest>(TFrom from, TDest dest)
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(from);
|
||||
JsonConvert.PopulateObject(str, dest);
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentEntity
|
||||
|
|
126
BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs
Normal file
126
BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs
Normal file
|
@ -0,0 +1,126 @@
|
|||
using Hangfire;
|
||||
using Hangfire.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hangfire.Annotations;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BTCPayServer.Servcices.Invoices
|
||||
{
|
||||
public class InvoiceNotificationManager
|
||||
{
|
||||
public static HttpClient _Client = new HttpClient();
|
||||
|
||||
public class ScheduledJob
|
||||
{
|
||||
public int TryCount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceEntity Invoice
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public ILogger Logger
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
IBackgroundJobClient _JobClient;
|
||||
public InvoiceNotificationManager(
|
||||
IBackgroundJobClient jobClient,
|
||||
ILogger<InvoiceNotificationManager> logger)
|
||||
{
|
||||
Logger = logger as ILogger ?? NullLogger.Instance;
|
||||
_JobClient = jobClient;
|
||||
}
|
||||
|
||||
public void Notify(InvoiceEntity invoice)
|
||||
{
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
|
||||
if(!string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
|
||||
}
|
||||
|
||||
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
|
||||
public async Task NotifyHttp(string invoiceData)
|
||||
{
|
||||
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
|
||||
var jobId = GetHttpJobId(job.Invoice);
|
||||
|
||||
if(!_Executing.TryAdd(jobId, jobId))
|
||||
return; //For some reason, Hangfire fire the job several time
|
||||
|
||||
Logger.LogInformation("Running " + jobId);
|
||||
bool reschedule = false;
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Method = HttpMethod.Post;
|
||||
|
||||
var dto = job.Invoice.EntityToDTO();
|
||||
InvoicePaymentNotification notification = new InvoicePaymentNotification()
|
||||
{
|
||||
Id = dto.Id,
|
||||
Url = dto.Url,
|
||||
BTCDue = dto.BTCDue,
|
||||
BTCPaid = dto.BTCPaid,
|
||||
BTCPrice = dto.BTCPrice,
|
||||
Currency = dto.Currency,
|
||||
CurrentTime = dto.CurrentTime,
|
||||
ExceptionStatus = dto.ExceptionStatus,
|
||||
ExpirationTime = dto.ExpirationTime,
|
||||
InvoiceTime = dto.InvoiceTime,
|
||||
PosData = dto.PosData,
|
||||
Price = dto.Price,
|
||||
Rate = dto.Rate,
|
||||
Status = dto.Status,
|
||||
BuyerFields = job.Invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", job.Invoice.RefundMail) }
|
||||
};
|
||||
request.RequestUri = new Uri(job.Invoice.NotificationURL, UriKind.Absolute);
|
||||
request.Content = new StringContent(JsonConvert.SerializeObject(notification), Encoding.UTF8, "application/json");
|
||||
var response = await _Client.SendAsync(request, cts.Token);
|
||||
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
|
||||
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
reschedule = true;
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
|
||||
}
|
||||
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
|
||||
|
||||
job.TryCount++;
|
||||
|
||||
if(job.TryCount < MaxTry && reschedule)
|
||||
{
|
||||
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
|
||||
|
||||
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
|
||||
}
|
||||
}
|
||||
|
||||
int MaxTry = 6;
|
||||
|
||||
private static string GetHttpJobId(InvoiceEntity invoice)
|
||||
{
|
||||
return $"{invoice.Id}-{invoice.Status}-HTTP";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ using BTCPayServer.Logging;
|
|||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Collections.Concurrent;
|
||||
using Hangfire;
|
||||
|
||||
namespace BTCPayServer.Servcices.Invoices
|
||||
{
|
||||
|
@ -19,12 +20,16 @@ namespace BTCPayServer.Servcices.Invoices
|
|||
InvoiceRepository _InvoiceRepository;
|
||||
ExplorerClient _ExplorerClient;
|
||||
DerivationStrategyFactory _DerivationFactory;
|
||||
InvoiceNotificationManager _NotificationManager;
|
||||
|
||||
public InvoiceWatcher(ExplorerClient explorerClient, InvoiceRepository invoiceRepository)
|
||||
public InvoiceWatcher(ExplorerClient explorerClient,
|
||||
InvoiceRepository invoiceRepository,
|
||||
InvoiceNotificationManager notificationManager)
|
||||
{
|
||||
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
|
||||
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager));
|
||||
}
|
||||
|
||||
private async Task StartWatchInvoice(string invoiceId)
|
||||
|
@ -114,6 +119,10 @@ namespace BTCPayServer.Servcices.Invoices
|
|||
if(totalPaid == invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "paid";
|
||||
if(invoice.FullNotifications)
|
||||
{
|
||||
_NotificationManager.Notify(invoice);
|
||||
}
|
||||
invoice.ExceptionStatus = null;
|
||||
needSave = true;
|
||||
}
|
||||
|
@ -159,6 +168,7 @@ namespace BTCPayServer.Servcices.Invoices
|
|||
if(confirmed)
|
||||
{
|
||||
invoice.Status = "confirmed";
|
||||
_NotificationManager.Notify(invoice);
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
@ -172,6 +182,8 @@ namespace BTCPayServer.Servcices.Invoices
|
|||
if(minConf >= 6)
|
||||
{
|
||||
invoice.Status = "complete";
|
||||
if(invoice.FullNotifications)
|
||||
_NotificationManager.Notify(invoice);
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,11 @@
|
|||
<input asp-for="BuyerEmail" class="form-control" />
|
||||
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationUrl" class="control-label"></label>
|
||||
<input asp-for="NotificationUrl" class="form-control" />
|
||||
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="StoreId" class="control-label"></label>
|
||||
<select asp-for="StoreId" asp-items="Model.Stores" class="form-control"></select>
|
||||
|
|
Loading…
Add table
Reference in a new issue