diff --git a/BTCPayServer/BTCPayNetwork.cs b/BTCPayServer/BTCPayNetwork.cs index c9e78ea71..7462f7a00 100644 --- a/BTCPayServer/BTCPayNetwork.cs +++ b/BTCPayServer/BTCPayNetwork.cs @@ -66,7 +66,7 @@ namespace BTCPayServer public BTCPayDefaultSettings DefaultSettings { get; set; } public KeyPath CoinType { get; internal set; } - public int MaxTrackedConfirmation { get; internal set; } = 7; + public int MaxTrackedConfirmation { get; internal set; } = 6; public override string ToString() { diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 194f1bb82..3cfbf9421 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -80,14 +80,34 @@ namespace BTCPayServer.Controllers var payments = invoice .GetPayments() + .Where(p => p.GetCryptoPaymentData() is BitcoinLikePaymentData) .Select(async payment => { + var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData(); var m = new InvoiceDetailsModel.Payment(); var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode()); m.CryptoCode = payment.GetCryptoCode(); m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork); - m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0; - m.TransactionId = payment.Outpoint.Hash.ToString(); + + int confirmationCount = 0; + if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date + { + confirmationCount = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0; + } + else + { + confirmationCount = paymentData.ConfirmationCount; + } + if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation) + { + m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation); + } + else + { + m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture); + } + + m.TransactionId = paymentData.Outpoint.Hash.ToString(); m.ReceivedTime = payment.ReceivedTime; m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId); m.Replaced = !payment.Accounted; diff --git a/BTCPayServer/HostedServices/NBXplorerListener.cs b/BTCPayServer/HostedServices/NBXplorerListener.cs index a35629ac1..6888333f2 100644 --- a/BTCPayServer/HostedServices/NBXplorerListener.cs +++ b/BTCPayServer/HostedServices/NBXplorerListener.cs @@ -165,10 +165,11 @@ namespace BTCPayServer.HostedServices var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode); if (invoice != null) { - var payment = invoice.GetPayments().FirstOrDefault(p => p.Outpoint == txCoin.Outpoint); - if (payment == null) + var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF); + var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); + if (!alreadyExist) { - payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, txCoin, network.CryptoCode); + var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode); await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy); } else @@ -205,17 +206,27 @@ namespace BTCPayServer.HostedServices } } + IEnumerable GetAllBitcoinPaymentData(InvoiceEntity invoice) + { + return invoice.GetPayments() + .Select(p => p.GetCryptoPaymentData() as BitcoinLikePaymentData) + .Where(p => p != null); + } + async Task UpdatePaymentStates(BTCPayWallet wallet, string invoiceId) { var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false); List updatedPaymentEntities = new List(); - var transactions = await wallet.GetTransactions(invoice.GetPayments(wallet.Network) - .Select(t => t.Outpoint.Hash) + var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice) + .Select(p => p.Outpoint.Hash) .ToArray()); var conflicts = GetConflicts(transactions.Select(t => t.Value)); foreach (var payment in invoice.GetPayments(wallet.Network)) { - if (!transactions.TryGetValue(payment.Outpoint.Hash, out TransactionResult tx)) + var paymentData = payment.GetCryptoPaymentData() as BitcoinLikePaymentData; + if (paymentData == null) + continue; + if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx)) continue; var txId = tx.Transaction.GetHash(); var txConflict = conflicts.GetConflict(txId); @@ -228,27 +239,12 @@ namespace BTCPayServer.HostedServices payment.Accounted = accounted; } - var bitcoinLike = payment.GetCryptoPaymentData() as BitcoinLikePaymentData; - - // Legacy - if (bitcoinLike == null) + if (paymentData.ConfirmationCount != tx.Confirmations) { -#pragma warning disable CS0618 // Type or member is obsolete - payment.CryptoPaymentDataType = "BTCLike"; -#pragma warning restore CS0618 // Type or member is obsolete - bitcoinLike = new BitcoinLikePaymentData(); - bitcoinLike.ConfirmationCount = tx.Confirmations; - bitcoinLike.RBF = tx.Transaction.RBF; - payment.SetCryptoPaymentData(bitcoinLike); - updated = true; - } - - if (bitcoinLike.ConfirmationCount != tx.Confirmations) - { - if(wallet.Network.MaxTrackedConfirmation >= bitcoinLike.ConfirmationCount) - { - bitcoinLike.ConfirmationCount = tx.Confirmations; - payment.SetCryptoPaymentData(bitcoinLike); + if(wallet.Network.MaxTrackedConfirmation >= paymentData.ConfirmationCount) + { + paymentData.ConfirmationCount = tx.Confirmations; + payment.SetCryptoPaymentData(paymentData); updated = true; } } @@ -328,7 +324,7 @@ namespace BTCPayServer.HostedServices foreach (var invoiceId in invoices) { var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true); - var alreadyAccounted = new HashSet(invoice.GetPayments(network).Select(p => p.Outpoint)); + var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet(); var strategy = invoice.GetDerivationStrategy(network); if (strategy == null) continue; @@ -337,7 +333,9 @@ namespace BTCPayServer.HostedServices .ToArray(); foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint))) { - var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, coin.Coin, network.CryptoCode).ConfigureAwait(false); + var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash); + var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF); + var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false); alreadyAccounted.Add(coin.Coin.Outpoint); invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy); totalPayment++; diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index c55754973..8533028ff 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -22,7 +22,7 @@ namespace BTCPayServer.Models.InvoicingModels public class Payment { public string CryptoCode { get; set; } - public int Confirmations + public string Confirmations { get; set; } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 963fcd6ce..f1670ee94 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -537,7 +537,7 @@ namespace BTCPayServer.Services.Invoices public BitcoinAddress GetDepositAddress() { - if(string.IsNullOrEmpty(DepositAddress)) + if (string.IsNullOrEmpty(DepositAddress)) { return null; } @@ -604,12 +604,14 @@ namespace BTCPayServer.Services.Invoices { get; set; } + + [Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")] public OutPoint Outpoint { get; set; } - [Obsolete("Use GetValue() or GetScriptPubKey() instead")] + [Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Output")] public TxOut Output { get; set; @@ -627,6 +629,7 @@ namespace BTCPayServer.Services.Invoices get; set; } + [Obsolete("Use GetCryptoCode() instead")] public string CryptoCode { @@ -644,23 +647,38 @@ namespace BTCPayServer.Services.Invoices #pragma warning disable CS0618 if (string.IsNullOrEmpty(CryptoPaymentDataType)) { - return NullPaymentData.Instance; + // In case this is a payment done before this update, consider it unconfirmed with RBF for safety + var paymentData = new BitcoinLikePaymentData(); + paymentData.Outpoint = Outpoint; + paymentData.Output = Output; + paymentData.RBF = true; + paymentData.ConfirmationCount = 0; + paymentData.Legacy = true; + return paymentData; } if (CryptoPaymentDataType == "BTCLike") { - return JsonConvert.DeserializeObject(CryptoPaymentData); + var paymentData = JsonConvert.DeserializeObject(CryptoPaymentData); + // legacy + paymentData.Output = Output; + paymentData.Outpoint = Outpoint; + return paymentData; } - else - return NullPaymentData.Instance; + + throw new NotSupportedException(nameof(CryptoPaymentDataType) + " does not support " + CryptoPaymentDataType); #pragma warning restore CS0618 } public void SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData) { #pragma warning disable CS0618 - if (cryptoPaymentData is BitcoinLikePaymentData) + if (cryptoPaymentData is BitcoinLikePaymentData paymentData) { CryptoPaymentDataType = "BTCLike"; + // Legacy + Outpoint = paymentData.Outpoint; + Output = paymentData.Output; + /// } else throw new NotSupportedException(cryptoPaymentData.ToString()); @@ -701,41 +719,60 @@ namespace BTCPayServer.Services.Invoices public interface CryptoPaymentData { + /// + /// Returns an identifier which uniquely identify the payment + /// + /// The payment id + string GetPaymentId(); + + /// + /// Returns terms which will be indexed and searchable in the search bar of invoice + /// + /// The search terms + string[] GetSearchTerms(); bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network); bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network); } - public class NullPaymentData : CryptoPaymentData - { - - private static readonly NullPaymentData _Instance = new NullPaymentData(); - public static NullPaymentData Instance - { - get - { - return _Instance; - } - } - public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network) - { - return false; - } - - public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network) - { - return false; - } - } - public class BitcoinLikePaymentData : CryptoPaymentData { + public BitcoinLikePaymentData() + { + + } + public BitcoinLikePaymentData(Coin coin, bool rbf) + { + Outpoint = coin.Outpoint; + Output = coin.TxOut; + ConfirmationCount = 0; + RBF = rbf; + } + [JsonIgnore] + public OutPoint Outpoint { get; set; } + [JsonIgnore] + public TxOut Output { get; set; } public int ConfirmationCount { get; set; } public bool RBF { get; set; } + /// + /// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer + /// + public bool Legacy { get; set; } + + public string GetPaymentId() + { + return Outpoint.ToString(); + } + + public string[] GetSearchTerms() + { + return new[] { Outpoint.Hash.ToString() }; + } + public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network) { - return ConfirmationCount >= 6; + return ConfirmationCount >= network.MaxTrackedConfirmation; } public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network) diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index dc78f21d3..a245721cd 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -443,24 +443,24 @@ namespace BTCPayServer.Services.Invoices AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray()); } - public async Task AddPayment(string invoiceId, DateTimeOffset date, Coin receivedCoin, string cryptoCode) + public async Task AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode) { using (var context = _ContextFactory.CreateContext()) { PaymentEntity entity = new PaymentEntity { - Outpoint = receivedCoin.Outpoint, #pragma warning disable CS0618 - Output = receivedCoin.TxOut, CryptoCode = cryptoCode, #pragma warning restore CS0618 ReceivedTime = date.UtcDateTime, Accounted = false }; - entity.SetCryptoPaymentData(new BitcoinLikePaymentData()); + entity.SetCryptoPaymentData(paymentData); + + PaymentData data = new PaymentData { - Id = receivedCoin.Outpoint.ToString(), + Id = paymentData.GetPaymentId(), Blob = ToBytes(entity, null), InvoiceDataId = invoiceId, Accounted = false @@ -469,7 +469,7 @@ namespace BTCPayServer.Services.Invoices context.Payments.Add(data); await context.SaveChangesAsync().ConfigureAwait(false); - AddToTextSearch(invoiceId, receivedCoin.Outpoint.Hash.ToString()); + AddToTextSearch(invoiceId, paymentData.GetSearchTerms()); return entity; } } @@ -482,8 +482,9 @@ namespace BTCPayServer.Services.Invoices { foreach (var payment in payments) { + var paymentData = payment.GetCryptoPaymentData(); var data = new PaymentData(); - data.Id = payment.Outpoint.ToString(); + data.Id = paymentData.GetPaymentId(); data.Accounted = payment.Accounted; data.Blob = ToBytes(payment, null); context.Attach(data);