Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
abeb10cc8c | |||
4dca81403b | |||
6ba6a34df2 | |||
6d14fe9c30 |
@ -255,7 +255,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paidPartial", localInvoice.Status);
|
||||
Assert.Equal("new", localInvoice.Status);
|
||||
Assert.Equal(firstPayment, localInvoice.BtcPaid);
|
||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
|
||||
@ -311,7 +311,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paidOver", localInvoice.Status);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.20</Version>
|
||||
<Version>1.0.0.23</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
|
@ -75,8 +75,9 @@ 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)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60)
|
||||
{
|
||||
//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 entity = new InvoiceEntity
|
||||
{
|
||||
@ -88,6 +89,7 @@ namespace BTCPayServer.Controllers
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
|
||||
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications;
|
||||
@ -119,6 +121,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
{
|
||||
if(transactionSpeed == null)
|
||||
return defaultPolicy;
|
||||
var mappings = new Dictionary<string, SpeedPolicy>();
|
||||
mappings.Add("low", SpeedPolicy.LowSpeed);
|
||||
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
||||
|
@ -248,6 +248,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset? MonitoringExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
|
@ -80,7 +80,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
|
||||
}
|
||||
|
||||
if(invoice.Status == "complete" || invoice.Status == "invalid")
|
||||
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))
|
||||
{
|
||||
if(await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
|
||||
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
|
||||
@ -106,110 +108,110 @@ namespace BTCPayServer.Services.Invoices
|
||||
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
|
||||
{
|
||||
bool needSave = false;
|
||||
//Fetch unknown payments
|
||||
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
|
||||
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
if(invoice.Status != "invalid" && invoice.ExpirationTime < DateTimeOffset.UtcNow && (invoice.Status == "new" || invoice.Status == "paidPartial"))
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
|
||||
var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
|
||||
utxos =
|
||||
utxos
|
||||
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
|
||||
.ToArray();
|
||||
|
||||
List<Coin> receivedCoins = new List<Coin>();
|
||||
foreach(var received in utxos)
|
||||
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
|
||||
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
|
||||
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
}
|
||||
//////
|
||||
|
||||
if(invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
needSave = true;
|
||||
invoice.Status = "invalid";
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
if(invoice.Status == "invalid" || invoice.Status == "new" || invoice.Status == "paidPartial")
|
||||
if(invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
|
||||
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
|
||||
var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
|
||||
utxos =
|
||||
utxos
|
||||
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
|
||||
.ToArray();
|
||||
|
||||
List<Coin> receivedCoins = new List<Coin>();
|
||||
foreach(var received in utxos)
|
||||
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
|
||||
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
|
||||
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum();
|
||||
if(totalPaid >= invoice.GetTotalCryptoDue())
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
if(invoice.Status == "new")
|
||||
{
|
||||
invoice.Status = "paidPartial";
|
||||
invoice.Status = "paid";
|
||||
if(invoice.FullNotifications)
|
||||
{
|
||||
_NotificationManager.Notify(invoice);
|
||||
}
|
||||
invoice.ExceptionStatus = null;
|
||||
needSave = true;
|
||||
}
|
||||
else if(invoice.Status == "expired")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "paidPartial")
|
||||
{
|
||||
var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum();
|
||||
if(totalPaid == invoice.GetTotalCryptoDue())
|
||||
if(totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.Status = "paid";
|
||||
if(invoice.FullNotifications)
|
||||
{
|
||||
_NotificationManager.Notify(invoice);
|
||||
}
|
||||
invoice.ExceptionStatus = null;
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
if(totalPaid > invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "paidOver";
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
if(totalPaid < invoice.GetTotalCryptoDue() && invoice.ExceptionStatus == null)
|
||||
if(totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "paid" || invoice.Status == "paidOver")
|
||||
if(invoice.Status == "paid")
|
||||
{
|
||||
var getTransactions = invoice.Payments.Select(o => o.Outpoint.Hash).Select(o => _ExplorerClient.GetTransactionAsync(o, _Cts.Token)).ToArray();
|
||||
await Task.WhenAll(getTransactions).ConfigureAwait(false);
|
||||
var transactions = getTransactions.Select(c => c.GetAwaiter().GetResult()).ToArray();
|
||||
if(!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow)
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(invoice);
|
||||
if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => !t.Transaction.Transaction.RBF);
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Transaction.Confirmations >= 1);
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Transaction.Confirmations >= 6);
|
||||
}
|
||||
|
||||
bool confirmed = false;
|
||||
var minConf = transactions.Select(t => t.Confirmations).Min();
|
||||
if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
if(minConf > 0)
|
||||
confirmed = true;
|
||||
else
|
||||
confirmed = !transactions.Any(t => t.Transaction.RBF);
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
|
||||
if(totalConfirmed >= invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "confirmed";
|
||||
_NotificationManager.Notify(invoice);
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
else
|
||||
{
|
||||
confirmed = minConf >= 1;
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
confirmed = minConf >= 6;
|
||||
}
|
||||
|
||||
if(confirmed)
|
||||
{
|
||||
invoice.Status = "confirmed";
|
||||
_NotificationManager.Notify(invoice);
|
||||
invoice.Status = "invalid";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "confirmed")
|
||||
{
|
||||
var getTransactions = invoice.Payments.Select(o => o.Outpoint.Hash).Select(o => _ExplorerClient.GetTransactionAsync(o, _Cts.Token)).ToArray();
|
||||
await Task.WhenAll(getTransactions).ConfigureAwait(false);
|
||||
var transactions = getTransactions.Select(c => c.GetAwaiter().GetResult()).ToArray();
|
||||
var minConf = transactions.Select(t => t.Confirmations).Min();
|
||||
if(minConf >= 6)
|
||||
var transactions = await GetPaymentsWithTransaction(invoice);
|
||||
transactions = transactions.Where(t => t.Transaction.Confirmations >= 6);
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
|
||||
if(totalConfirmed >= invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "complete";
|
||||
if(invoice.FullNotifications)
|
||||
@ -221,6 +223,15 @@ namespace BTCPayServer.Services.Invoices
|
||||
return (needSave, changes);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<(PaymentEntity Payment, TransactionResult Transaction)>> GetPaymentsWithTransaction(InvoiceEntity invoice)
|
||||
{
|
||||
var getPayments = invoice.Payments
|
||||
.Select(async o => (Payment: o, Transaction: await _ExplorerClient.GetTransactionAsync(o.Outpoint.Hash, _Cts.Token)))
|
||||
.ToArray();
|
||||
await Task.WhenAll(getPayments).ConfigureAwait(false);
|
||||
var transactions = getPayments.Select(c => (Payment: c.Result.Payment, Transaction: c.Result.Transaction));
|
||||
return transactions;
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
|
@ -166,7 +166,6 @@ function onDataCallback(jsonData) {
|
||||
window.parent.postMessage({ "invoiceId": srvModel.invoiceId, "status": newStatus }, "*");
|
||||
}
|
||||
if (newStatus == "complete" ||
|
||||
newStatus == "paidOver" ||
|
||||
newStatus == "confirmed" ||
|
||||
newStatus == "paid") {
|
||||
if ($(".modal-dialog").hasClass("expired")) {
|
||||
@ -192,7 +191,7 @@ function onDataCallback(jsonData) {
|
||||
$("#paid").addClass("active");
|
||||
}
|
||||
|
||||
if (newStatus == "invalid") {
|
||||
if (newStatus == "expired" || newStatus == "invalid") { //TODO: different state if the invoice is invalid (failed to confirm after timeout)
|
||||
$(".timer-row").removeClass("expiring-soon");
|
||||
$(".timer-row__message span").html("Invoice expired.");
|
||||
$(".timer-row__spinner").html("");
|
||||
|
Reference in New Issue
Block a user