Can set store policy to define how much time to wait before passing a transaction from paid to invalid.

This commit is contained in:
nicolas.dorier
2017-12-03 14:43:52 +09:00
parent 7efe83eba8
commit 22f06ecd4e
8 changed files with 80 additions and 34 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.33</Version> <Version>1.0.0.34</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="Build\dockerfiles\**" /> <Compile Remove="Build\dockerfiles\**" />

View File

@@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider)); _FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
} }
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60) internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
{ {
//TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level //TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level
var derivationStrategy = store.DerivationStrategy; var derivationStrategy = store.DerivationStrategy;
@@ -87,12 +87,13 @@ namespace BTCPayServer.Controllers
InvoiceTime = DateTimeOffset.UtcNow, InvoiceTime = DateTimeOffset.UtcNow,
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy") DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
}; };
var storeBlob = store.GetStoreBlob(_Network);
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null; 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 ? if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null; notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute(); EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes); entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes); entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
entity.OrderId = invoice.OrderId; entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl; entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications; entity.FullNotifications = invoice.FullNotifications;
@@ -114,7 +115,7 @@ namespace BTCPayServer.Controllers
var getFeeRate = _FeeProvider.GetFeeRateAsync(); var getFeeRate = _FeeProvider.GetFeeRateAsync();
var getRate = _RateProvider.GetRateAsync(invoice.Currency); var getRate = _RateProvider.GetRateAsync(invoice.Currency);
var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy)); var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity.TxFee = store.GetStoreBlob(_Network).NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes entity.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await getRate; entity.Rate = (double)await getRate;
entity.PosData = invoice.PosData; entity.PosData = invoice.PosData;
entity.DepositAddress = await getAddress; entity.DepositAddress = await getAddress;

View File

@@ -144,13 +144,15 @@ namespace BTCPayServer.Controllers
if (store == null) if (store == null)
return NotFound(); return NotFound();
var storeBlob = store.GetStoreBlob(_Network);
var vm = new StoreViewModel(); var vm = new StoreViewModel();
vm.StoreName = store.StoreName; vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite; vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !store.GetStoreBlob(_Network).NetworkFeeDisabled; vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy; vm.SpeedPolicy = store.SpeedPolicy;
vm.DerivationScheme = store.DerivationStrategy; vm.DerivationScheme = store.DerivationStrategy;
vm.StatusMessage = StatusMessage; vm.StatusMessage = StatusMessage;
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
return View(vm); return View(vm);
} }
@@ -205,11 +207,12 @@ namespace BTCPayServer.Controllers
} }
} }
if (store.GetStoreBlob(_Network).NetworkFeeDisabled != !model.NetworkFee)
{
var blob = store.GetStoreBlob(_Network); var blob = store.GetStoreBlob(_Network);
blob.NetworkFeeDisabled = !model.NetworkFee; blob.NetworkFeeDisabled = !model.NetworkFee;
store.SetStoreBlob(blob, _Network); blob.MonitoringExpiration = model.MonitoringExpiration;
if (store.SetStoreBlob(blob, _Network))
{
needUpdate = true; needUpdate = true;
} }

View File

@@ -8,6 +8,8 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.ComponentModel;
using Newtonsoft.Json;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
@@ -65,17 +67,33 @@ namespace BTCPayServer.Data
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob)); return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
} }
public void SetStoreBlob(StoreBlob storeBlob, Network network) public bool SetStoreBlob(StoreBlob storeBlob, Network network)
{ {
StoreBlob = Encoding.UTF8.GetBytes(new Serializer(network).ToString(storeBlob)); var original = new Serializer(network).ToString(GetStoreBlob(network));
var newBlob = new Serializer(network).ToString(storeBlob);
if (original == newBlob)
return false;
StoreBlob = Encoding.UTF8.GetBytes(newBlob);
return true;
} }
} }
public class StoreBlob public class StoreBlob
{ {
public StoreBlob()
{
MonitoringExpiration = 60;
}
public bool NetworkFeeDisabled public bool NetworkFeeDisabled
{ {
get; set; get; set;
} }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int MonitoringExpiration
{
get;
set;
}
} }
} }

View File

@@ -34,6 +34,14 @@ namespace BTCPayServer.Models.StoreViewModels
get; set; get; set;
} }
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
[Range(10, 60 * 24 * 31)]
public int MonitoringExpiration
{
get;
set;
}
[Display(Name = "Consider the invoice confirmed when the payment transaction...")] [Display(Name = "Consider the invoice confirmed when the payment transaction...")]
public SpeedPolicy SpeedPolicy public SpeedPolicy SpeedPolicy
{ {

View File

@@ -251,7 +251,8 @@ namespace BTCPayServer.Services.Invoices
get; get;
set; set;
} }
public DateTimeOffset? MonitoringExpiration [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public DateTimeOffset MonitoringExpiration
{ {
get; get;
set; set;

View File

@@ -80,9 +80,8 @@ namespace BTCPayServer.Services.Invoices
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}"); Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
} }
var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60);
if (invoice.Status == "complete" || if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow)) ((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{ {
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false)) if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId); Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
@@ -184,10 +183,9 @@ namespace BTCPayServer.Services.Invoices
} }
if (invoice.Status == "paid") if (invoice.Status == "paid")
{
if (!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow)
{ {
var transactions = await GetPaymentsWithTransaction(invoice); var transactions = await GetPaymentsWithTransaction(invoice);
var chainConfirmedTransactions = transactions.Where(t => t.Confirmations >= 1);
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed) if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{ {
transactions = transactions.Where(t => !t.Transaction.RBF); transactions = transactions.Where(t => !t.Transaction.RBF);
@@ -201,6 +199,24 @@ namespace BTCPayServer.Services.Invoices
transactions = transactions.Where(t => t.Confirmations >= 6); transactions = transactions.Where(t => t.Confirmations >= 6);
} }
var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.Output.Value).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(chainTotalConfirmed < invoice.GetTotalCryptoDue()))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "invalid";
needSave = true;
if (invoice.FullNotifications)
{
_NotificationManager.Notify(invoice);
}
}
else
{
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if (totalConfirmed >= invoice.GetTotalCryptoDue()) if (totalConfirmed >= invoice.GetTotalCryptoDue())
{ {
@@ -210,12 +226,6 @@ namespace BTCPayServer.Services.Invoices
needSave = true; needSave = true;
} }
} }
else
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "invalid";
needSave = true;
}
} }
if (invoice.Status == "confirmed") if (invoice.Status == "confirmed")

View File

@@ -30,6 +30,11 @@
<label asp-for="NetworkFee"></label> <label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" /> <input asp-for="NetworkFee" type="checkbox" class="form-check" />
</div> </div>
<div class="form-group">
<label asp-for="MonitoringExpiration"></label>
<input asp-for="MonitoringExpiration" class="form-control" />
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group"> <div class="form-group">
<label asp-for="SpeedPolicy"></label> <label asp-for="SpeedPolicy"></label>
<select asp-for="SpeedPolicy" class="form-control"> <select asp-for="SpeedPolicy" class="form-control">