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>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.33</Version>
<Version>1.0.0.34</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />

View File

@@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
_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
var derivationStrategy = store.DerivationStrategy;
@@ -87,12 +87,13 @@ namespace BTCPayServer.Controllers
InvoiceTime = DateTimeOffset.UtcNow,
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;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
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.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications;
@@ -114,7 +115,7 @@ namespace BTCPayServer.Controllers
var getFeeRate = _FeeProvider.GetFeeRateAsync();
var getRate = _RateProvider.GetRateAsync(invoice.Currency);
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.PosData = invoice.PosData;
entity.DepositAddress = await getAddress;

View File

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

View File

@@ -8,6 +8,8 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
@@ -65,17 +67,33 @@ namespace BTCPayServer.Data
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 StoreBlob()
{
MonitoringExpiration = 60;
}
public bool NetworkFeeDisabled
{
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;
}
[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...")]
public SpeedPolicy SpeedPolicy
{

View File

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

View File

@@ -80,9 +80,8 @@ namespace BTCPayServer.Services.Invoices
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" ||
((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))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
@@ -184,10 +183,9 @@ namespace BTCPayServer.Services.Invoices
}
if (invoice.Status == "paid")
{
if (!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow)
{
var transactions = await GetPaymentsWithTransaction(invoice);
var chainConfirmedTransactions = transactions.Where(t => t.Confirmations >= 1);
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
transactions = transactions.Where(t => !t.Transaction.RBF);
@@ -201,6 +199,24 @@ namespace BTCPayServer.Services.Invoices
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();
if (totalConfirmed >= invoice.GetTotalCryptoDue())
{
@@ -210,12 +226,6 @@ namespace BTCPayServer.Services.Invoices
needSave = true;
}
}
else
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "invalid";
needSave = true;
}
}
if (invoice.Status == "confirmed")

View File

@@ -30,6 +30,11 @@
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
</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">
<label asp-for="SpeedPolicy"></label>
<select asp-for="SpeedPolicy" class="form-control">