diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 048c16dc5..f51d34d96 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -98,7 +98,7 @@ namespace BTCPayServer.Tests Rate = 10513.44m, }.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() { - TxFee = Money.Coins(0.00000100m), + NetworkFee = Money.Coins(0.00000100m), DepositAddress = dummy })); paymentMethods.Add(new PaymentMethod() @@ -107,7 +107,7 @@ namespace BTCPayServer.Tests Rate = 216.79m }.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() { - TxFee = Money.Coins(0.00010000m), + NetworkFee = Money.Coins(0.00010000m), DepositAddress = dummy })); invoiceEntity.SetPaymentMethods(paymentMethods); @@ -115,12 +115,12 @@ namespace BTCPayServer.Tests var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null); var accounting = btc.Calculate(); - invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData() + invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData() { Output = new TxOut() { Value = Money.Coins(0.00151263m) } })); accounting = btc.Calculate(); - invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData() + invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData() { Output = new TxOut() { Value = accounting.Due } })); @@ -147,7 +147,7 @@ namespace BTCPayServer.Tests var entity = new InvoiceEntity(); #pragma warning disable CS0618 entity.Payments = new System.Collections.Generic.List(); - entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) }); + entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NetworkFee = Money.Coins(0.1m) }); entity.ProductInformation = new ProductInformation() { Price = 5000 }; var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike); @@ -155,20 +155,20 @@ namespace BTCPayServer.Tests Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true }); + entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true, NetworkFee = 0.1m }); accounting = paymentMethod.Calculate(); //Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1 Assert.Equal(Money.Coins(0.7m), accounting.Due); Assert.Equal(Money.Coins(1.2m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); + entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true, NetworkFee = 0.1m }); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(0.6m), accounting.Due); Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true }); + entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true, NetworkFee = 0.1m }); accounting = paymentMethod.Calculate(); Assert.Equal(Money.Zero, accounting.Due); @@ -187,13 +187,13 @@ namespace BTCPayServer.Tests { CryptoCode = "BTC", Rate = 1000, - TxFee = Money.Coins(0.1m) + NetworkFee = Money.Coins(0.1m) }); paymentMethods.Add(new PaymentMethod() { CryptoCode = "LTC", Rate = 500, - TxFee = Money.Coins(0.01m) + NetworkFee = Money.Coins(0.01m) }); entity.SetPaymentMethods(paymentMethods); entity.Payments = new List(); @@ -205,7 +205,7 @@ namespace BTCPayServer.Tests accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(10.01m), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true }); + entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.1m }); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null); accounting = paymentMethod.Calculate(); @@ -222,7 +222,7 @@ namespace BTCPayServer.Tests Assert.Equal(Money.Coins(2.0m), accounting.Paid); Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue); - entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true }); + entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.01m }); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null); @@ -242,7 +242,7 @@ namespace BTCPayServer.Tests Assert.Equal(2, accounting.TxRequired); var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2); - entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true }); + entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true, NetworkFee = 0.1m }); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null); accounting = paymentMethod.Calculate(); @@ -272,7 +272,7 @@ namespace BTCPayServer.Tests var entity = new InvoiceEntity(); #pragma warning disable CS0618 entity.Payments = new List(); - entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) }); + entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NetworkFee = Money.Coins(0.1m) }); entity.ProductInformation = new ProductInformation() { Price = 5000 }; entity.PaymentTolerance = 0; diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index b9ebdaecb..74086aba6 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -304,7 +304,7 @@ namespace BTCPayServer.Controllers #pragma warning disable CS0618 // Type or member is obsolete Status = invoice.StatusString, #pragma warning restore CS0618 // Type or member is obsolete - NetworkFee = paymentMethodDetails.GetTxFee(), + NetworkFee = paymentMethodDetails.GetNetworkFee(), IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1, ChangellyEnabled = changelly != null, ChangellyMerchantId = changelly?.ChangellyMerchantId, diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index e7a0266eb..9ff5c468a 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -201,7 +201,7 @@ namespace BTCPayServer.Controllers paymentMethod.Rate = rate.BidAsk.Bid; var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment); if (storeBlob.NetworkFeeDisabled) - paymentDetails.SetNoTxFee(); + paymentDetails.SetNoNetworkFee(); paymentMethod.SetPaymentMethodDetails(paymentDetails); Func compare = null; @@ -241,7 +241,7 @@ namespace BTCPayServer.Controllers #pragma warning disable CS0618 if (paymentMethod.GetId().IsBTCOnChain) { - entity.TxFee = paymentMethod.TxFee; + entity.TxFee = paymentMethod.NetworkFee; entity.Rate = paymentMethod.Rate; entity.DepositAddress = paymentMethod.DepositAddress; } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs index d283a5458..5034df989 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs @@ -20,14 +20,14 @@ namespace BTCPayServer.Payments.Bitcoin return DepositAddress; } - public decimal GetTxFee() + public decimal GetNetworkFee() { - return TxFee.ToDecimal(MoneyUnit.BTC); + return NetworkFee.ToDecimal(MoneyUnit.BTC); } - public void SetNoTxFee() + public void SetNoNetworkFee() { - TxFee = Money.Zero; + NetworkFee = Money.Zero; } @@ -40,7 +40,7 @@ namespace BTCPayServer.Payments.Bitcoin [JsonIgnore] public FeeRate FeeRate { get; set; } [JsonIgnore] - public Money TxFee { get; set; } + public Money NetworkFee { get; set; } [JsonIgnore] public String DepositAddress { get; set; } public BitcoinAddress GetDepositAddress(Network network) diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs index d31c0a52e..2f1f236fb 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs @@ -32,6 +32,7 @@ namespace BTCPayServer.Payments.Bitcoin public TxOut Output { get; set; } public int ConfirmationCount { get; set; } public bool RBF { get; set; } + public decimal NetworkFee { get; set; } /// /// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index f38bd84f6..a7e075921 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -49,7 +49,7 @@ namespace BTCPayServer.Payments.Bitcoin var prepare = (Prepare)preparePaymentObject; Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod(); onchainMethod.FeeRate = await prepare.GetFeeRate; - onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes + onchainMethod.NetworkFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString(); return onchainMethod; } diff --git a/BTCPayServer/Payments/IPaymentMethodDetails.cs b/BTCPayServer/Payments/IPaymentMethodDetails.cs index 9fa165a0e..b8a260bbe 100644 --- a/BTCPayServer/Payments/IPaymentMethodDetails.cs +++ b/BTCPayServer/Payments/IPaymentMethodDetails.cs @@ -21,8 +21,8 @@ namespace BTCPayServer.Payments /// Returns what a merchant would need to pay to cashout this payment /// /// - decimal GetTxFee(); - void SetNoTxFee(); + decimal GetNetworkFee(); + void SetNoNetworkFee(); /// /// Change the payment destination (internal plumbing) /// diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs index 6d630f3a2..47ed0cc4d 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs @@ -22,6 +22,8 @@ namespace BTCPayServer.Payments.Lightning return GetPaymentId(); } + public decimal NetworkFee { get; set; } + public string GetPaymentId() { return BOLT11; diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs index 60ba19b13..0b3d08a6f 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs @@ -21,12 +21,12 @@ namespace BTCPayServer.Payments.Lightning return PaymentTypes.LightningLike; } - public decimal GetTxFee() + public decimal GetNetworkFee() { return 0.0m; } - public void SetNoTxFee() + public void SetNoNetworkFee() { } diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index 94938a457..f9ad840f5 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -65,8 +65,7 @@ namespace BTCPayServer.Services.Invoices.Export var pdata = payment.GetCryptoPaymentData(); var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks); - var paymentFee = pmethod.GetPaymentMethodDetails().GetTxFee(); - var paidAfterNetworkFees = pdata.GetValue() - paymentFee; + var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee; invoiceDue -= paidAfterNetworkFees * pmethod.Rate; var target = new ExportInvoiceHolder @@ -83,7 +82,7 @@ namespace BTCPayServer.Services.Invoices.Export // so if fee is 10000 satoshis, customer can essentially send infinite number of tx // and merchant effectivelly would receive 0 BTC, invoice won't be paid // while looking just at export you could sum Paid and assume merchant "received payments" - NetworkFee = paymentFee.ToString(CultureInfo.InvariantCulture), + NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture), InvoiceDue = invoiceDue, OrderId = invoice.OrderId, StoreId = invoice.StoreId, diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 9c958d9e0..746cdc4d4 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -485,7 +485,7 @@ namespace BTCPayServer.Services.Invoices public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider) { - PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider); + PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); var serializer = new Serializer(Dummy); #pragma warning disable CS0618 if (PaymentMethod != null) @@ -499,11 +499,11 @@ namespace BTCPayServer.Services.Invoices r.ParentEntity = this; r.Network = networkProvider?.GetNetwork(r.CryptoCode); if (r.Network != null || networkProvider == null) - rates.Add(r); + paymentMethods.Add(r); } } #pragma warning restore CS0618 - return rates; + return paymentMethods; } Network Dummy = Network.Main; @@ -517,8 +517,6 @@ namespace BTCPayServer.Services.Invoices public void SetPaymentMethods(PaymentMethodDictionary paymentMethods) { - if (paymentMethods.NetworkProvider != null) - throw new InvalidOperationException($"{nameof(paymentMethods)} should have NetworkProvider to null"); var obj = new JObject(); var serializer = new Serializer(Dummy); #pragma warning disable CS0618 @@ -731,7 +729,7 @@ namespace BTCPayServer.Services.Invoices { FeeRate = FeeRate, DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress, - TxFee = TxFee + NetworkFee = NetworkFee }; } else @@ -739,7 +737,7 @@ namespace BTCPayServer.Services.Invoices var details = PaymentMethodExtensions.DeserializePaymentMethodDetails(GetId(), PaymentMethodDetails); if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike) { - btcLike.TxFee = TxFee; + btcLike.NetworkFee = NetworkFee; btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress; btcLike.FeeRate = FeeRate; } @@ -761,7 +759,7 @@ namespace BTCPayServer.Services.Invoices if (paymentMethod is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod) { - TxFee = bitcoinPaymentMethod.TxFee; + NetworkFee = bitcoinPaymentMethod.NetworkFee; FeeRate = bitcoinPaymentMethod.FeeRate; DepositAddress = bitcoinPaymentMethod.DepositAddress; } @@ -777,7 +775,7 @@ namespace BTCPayServer.Services.Invoices public FeeRate FeeRate { get; set; } [JsonProperty(PropertyName = "txFee")] [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).TxFee")] - public Money TxFee { get; set; } + public Money NetworkFee { get; set; } [JsonProperty(PropertyName = "depositAddress")] [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")] public string DepositAddress { get; set; } @@ -801,7 +799,7 @@ namespace BTCPayServer.Services.Invoices .OrderBy(p => p.ReceivedTime) .Select(_ => { - var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetPaymentMethodId()].GetTxFee()); + var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee); paid += _.GetValue(paymentMethods, GetId()); if (!paidEnough) { @@ -842,17 +840,18 @@ namespace BTCPayServer.Services.Invoices var method = GetPaymentMethodDetails(); if (method == null) return 0.0m; - return method.GetTxFee(); + return method.GetNetworkFee(); } } public class PaymentEntity { + public int Version { get; set; } public DateTimeOffset ReceivedTime { get; set; } - + public decimal NetworkFee { get; set; } [Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")] public OutPoint Outpoint { @@ -889,7 +888,7 @@ namespace BTCPayServer.Services.Invoices #pragma warning disable CS0618 if (string.IsNullOrEmpty(CryptoPaymentDataType)) { - // In case this is a payment done before this update, consider it unconfirmed with RBF for safety + // For invoices created when CryptoPaymentDataType was not existing, we just consider that it is a RBFed payment for safety var paymentData = new Payments.Bitcoin.BitcoinLikePaymentData(); paymentData.Outpoint = Outpoint; paymentData.Output = Output; diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 47d4210ff..7e18bbcb2 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -379,11 +379,24 @@ retry: private InvoiceEntity ToEntity(Data.InvoiceData invoice) { var entity = ToObject(invoice.Blob, null); + PaymentMethodDictionary paymentMethods = null; #pragma warning disable CS0618 entity.Payments = invoice.Payments.Select(p => { var paymentEntity = ToObject(p.Blob, null); paymentEntity.Accounted = p.Accounted; + + // PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee. + // We want to hide this legacy detail in InvoiceRepository, so we fetch the fee from the PaymentMethod and assign it to the PaymentEntity. + if (paymentEntity.Version == 0) + { + if (paymentMethods == null) + paymentMethods = entity.GetPaymentMethods(null); + var paymentMethodDetails = paymentMethods.TryGet(paymentEntity.GetPaymentMethodId())?.GetPaymentMethodDetails(); + if (paymentMethodDetails != null) // == null should never happen, but we never know. + paymentEntity.NetworkFee = paymentMethodDetails.GetNetworkFee(); + } + return paymentEntity; }) .OrderBy(a => a.ReceivedTime).ToList(); @@ -551,13 +564,20 @@ retry: { using (var context = _ContextFactory.CreateContext()) { + var invoice = context.Invoices.Find(invoiceId); + if (invoice == null) + return null; PaymentEntity entity = new PaymentEntity { + Version = 1, #pragma warning disable CS0618 CryptoCode = cryptoCode, #pragma warning restore CS0618 ReceivedTime = date.UtcDateTime, - Accounted = accounted + Accounted = accounted, + NetworkFee = ToObject(invoice.Blob, null) + .GetPaymentMethod(new PaymentMethodId(cryptoCode, paymentData.GetPaymentType()), null) + .GetPaymentMethodDetails().GetNetworkFee() }; entity.SetCryptoPaymentData(paymentData); diff --git a/BTCPayServer/Services/Invoices/PaymentMethodDictionary.cs b/BTCPayServer/Services/Invoices/PaymentMethodDictionary.cs index e5c407f5a..05904588a 100644 --- a/BTCPayServer/Services/Invoices/PaymentMethodDictionary.cs +++ b/BTCPayServer/Services/Invoices/PaymentMethodDictionary.cs @@ -15,13 +15,6 @@ namespace BTCPayServer.Services.Invoices } - public PaymentMethodDictionary(BTCPayNetworkProvider networkProvider) - { - NetworkProvider = networkProvider; - } - - - public BTCPayNetworkProvider NetworkProvider { get; set; } public PaymentMethod this[PaymentMethodId index] { get