From 22435a2bf510773afce0a47e9bc7929f30b94c8d Mon Sep 17 00:00:00 2001 From: Nicolas Dorier Date: Wed, 19 Jul 2023 18:47:32 +0900 Subject: [PATCH] Refactor logic for calculating due amount of invoices (#5174) * Refactor logic for calculating due amount of invoices * Remove Money type from the accounting * Fix tests * Fix a corner case * fix bug * Rename PaymentCurrency to Currency * Fix bug * Rename PaymentCurrency -> Currency * Payment objects should have access to the InvoiceEntity * Set Currency USD in tests * Simplify some code * Remove useless code * Simplify code, kukks comment --- .../Liquid/ElementsLikeBtcPayNetwork.cs | 5 +- BTCPayServer.Common/BTCPayNetwork.cs | 7 +- BTCPayServer.Tests/FastTests.cs | 226 ++++++++----- BTCPayServer.Tests/UnitTest1.cs | 2 +- .../GreenField/GreenfieldInvoiceController.cs | 14 +- .../UIInvoiceController.Testing.cs | 6 +- .../Controllers/UIInvoiceController.UI.cs | 31 +- .../Controllers/UIInvoiceController.cs | 9 +- BTCPayServer/Controllers/UILNURLController.cs | 2 +- .../BitcoinLike/BitcoinLikePayoutHandler.cs | 2 +- BTCPayServer/Extensions.cs | 24 +- BTCPayServer/HostedServices/InvoiceWatcher.cs | 84 ++--- .../TransactionLabelMarkerHostedService.cs | 2 +- .../PaymentRequest/PaymentRequestHub.cs | 2 +- .../PaymentRequest/PaymentRequestService.cs | 12 +- .../Bitcoin/BitcoinLikePaymentHandler.cs | 2 +- .../Payments/LNURLPay/PaymentTypes.LNURL.cs | 5 +- .../Lightning/LightningLikePaymentHandler.cs | 2 +- .../Payments/Lightning/LightningListener.cs | 2 +- .../PayJoin/PayJoinEndpointController.cs | 2 +- BTCPayServer/Payments/PaymentTypes.Bitcoin.cs | 4 +- .../Payments/PaymentTypes.Lightning.cs | 5 +- BTCPayServer/Payments/PaymentTypes.cs | 2 +- .../Plugins/Crowdfund/CrowdfundPlugin.cs | 18 +- BTCPayServer/Plugins/NFC/NFCController.cs | 8 +- BTCPayServer/Program.cs | 1 + .../MoneroLikePaymentMethodHandler.cs | 2 +- .../Monero/Payments/MoneroPaymentType.cs | 4 +- .../Monero/Services/MoneroListener.cs | 6 +- .../Payments/ZcashLikePaymentMethodHandler.cs | 2 +- .../Zcash/Payments/ZcashPaymentType.cs | 4 +- .../Altcoins/Zcash/Services/ZcashListener.cs | 6 +- BTCPayServer/Services/Apps/AppHubStreamer.cs | 2 +- BTCPayServer/Services/Apps/AppService.cs | 11 +- BTCPayServer/Services/Invoices/Amounts.cs | 19 ++ .../Services/Invoices/Export/InvoiceExport.cs | 14 +- .../Services/Invoices/InvoiceEntity.cs | 315 +++++++++++------- .../Services/Invoices/InvoiceRepository.cs | 45 +-- .../Services/Invoices/PaymentService.cs | 2 +- 39 files changed, 487 insertions(+), 424 deletions(-) create mode 100644 BTCPayServer/Services/Invoices/Amounts.cs diff --git a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs index c5b96ec26..9aa863470 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs @@ -1,4 +1,5 @@ #if ALTCOINS +using System; using System.Collections.Generic; using System.Linq; using BTCPayServer.Common; @@ -34,12 +35,12 @@ namespace BTCPayServer output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)); } - public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) + public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue) { //precision 0: 10 = 0.00000010 //precision 2: 10 = 0.00001000 //precision 8: 10 = 10 - var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC); + var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility); var builder = base.GenerateBIP21(cryptoInfoAddress, money); builder.QueryParams.Add("assetid", AssetId.ToString()); return builder; diff --git a/BTCPayServer.Common/BTCPayNetwork.cs b/BTCPayServer.Common/BTCPayNetwork.cs index c378f9ee3..a59ba496a 100644 --- a/BTCPayServer.Common/BTCPayNetwork.cs +++ b/BTCPayServer.Common/BTCPayNetwork.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using BTCPayServer.Common; @@ -87,13 +88,13 @@ namespace BTCPayServer }); } - public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) + public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue) { var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme); builder.Host = cryptoInfoAddress; - if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero) + if (cryptoInfoDue is not null && cryptoInfoDue.Value != 0.0m) { - builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true)); + builder.QueryParams.Add("amount", cryptoInfoDue.Value.ToString(CultureInfo.InvariantCulture)); } return builder; } diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 0e2c08891..18f8dfcc8 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -346,165 +346,213 @@ namespace BTCPayServer.Tests Assert.True(Torrc.TryParse(input, out torrc)); Assert.Equal(expected, torrc.ToString()); } + [Fact] + public void CanCalculateDust() + { + var entity = new InvoiceEntity() { Currency = "USD" }; + entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest); +#pragma warning disable CS0618 + entity.Payments = new System.Collections.Generic.List(); + entity.SetPaymentMethod(new PaymentMethod() + { + Currency = "BTC", + Rate = 34_000m + }); + entity.Price = 4000; + entity.UpdateTotals(); + var accounting = entity.GetPaymentMethods().First().Calculate(); + // Exact price should be 0.117647059..., but the payment method round up to one sat + Assert.Equal(0.11764706m, accounting.Due); + entity.Payments.Add(new PaymentEntity() + { + Currency = "BTC", + Output = new TxOut(Money.Coins(0.11764706m), new Key()), + Accounted = true + }); + entity.UpdateTotals(); + Assert.Equal(0.0m, entity.NetDue); + // The dust's value is below 1 sat + Assert.True(entity.Dust > 0.0m); + Assert.True(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC) * entity.Rates["BTC"] > entity.Dust); + Assert.True(!entity.IsOverPaid); + Assert.True(!entity.IsUnderPaid); + + // Now, imagine there is litecoin. It might seem from its + // perspecitve that there has been a slight over payment. + // However, Calculate() should just cap it to 0.0m + entity.SetPaymentMethod(new PaymentMethod() + { + Currency = "LTC", + Rate = 3400m + }); + entity.UpdateTotals(); + var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC"); + accounting = method.Calculate(); + Assert.Equal(0.0m, accounting.DueUncapped); + +#pragma warning restore CS0618 + } #if ALTCOINS [Fact] public void CanCalculateCryptoDue() { var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest); - var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] - { - new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null), - new LightningLikePaymentHandler(null, null, networkProvider, null, null, null), - }); - var entity = new InvoiceEntity(); + var entity = new InvoiceEntity() { Currency = "USD" }; entity.Networks = networkProvider; #pragma warning disable CS0618 entity.Payments = new System.Collections.Generic.List(); entity.SetPaymentMethod(new PaymentMethod() { - CryptoCode = "BTC", + Currency = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) }); entity.Price = 5000; + entity.UpdateTotals(); var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(1.1m), accounting.Due); - Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); + Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC))); + Assert.Equal(1.1m, accounting.Due); + Assert.Equal(1.1m, accounting.TotalDue); entity.Payments.Add(new PaymentEntity() { + Currency = "BTC", Output = new TxOut(Money.Coins(0.5m), new Key()), + Rate = 5000, Accounted = true, NetworkFee = 0.1m }); - + entity.UpdateTotals(); 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); + Assert.Equal(0.7m, accounting.Due); + Assert.Equal(1.2m, accounting.TotalDue); entity.Payments.Add(new PaymentEntity() { + Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true, NetworkFee = 0.1m }); - + entity.UpdateTotals(); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(0.6m), accounting.Due); - Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); + Assert.Equal(0.6m, accounting.Due); + Assert.Equal(1.3m, accounting.TotalDue); entity.Payments.Add(new PaymentEntity() { + Currency = "BTC", Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true, NetworkFee = 0.1m }); - + entity.UpdateTotals(); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Zero, accounting.Due); - Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); + Assert.Equal(0.0m, accounting.Due); + Assert.Equal(1.3m, accounting.TotalDue); entity.Payments.Add( - new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); - + new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); + entity.UpdateTotals(); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Zero, accounting.Due); - Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); + Assert.Equal(0.0m, accounting.Due); + Assert.Equal(1.3m, accounting.TotalDue); entity = new InvoiceEntity(); entity.Networks = networkProvider; entity.Price = 5000; PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); paymentMethods.Add( - new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) }); + new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) }); paymentMethods.Add( - new PaymentMethod() { CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) }); + new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) }); entity.SetPaymentMethods(paymentMethods); entity.Payments = new List(); + entity.UpdateTotals(); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(5.1m), accounting.Due); + Assert.Equal(5.1m, accounting.Due); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(10.01m), accounting.TotalDue); + Assert.Equal(10.01m, accounting.TotalDue); entity.Payments.Add(new PaymentEntity() { - CryptoCode = "BTC", + Currency = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.1m }); - + entity.UpdateTotals(); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(4.2m), accounting.Due); - Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); - Assert.Equal(Money.Coins(1.0m), accounting.Paid); - Assert.Equal(Money.Coins(5.2m), accounting.TotalDue); + Assert.Equal(4.2m, accounting.Due); + Assert.Equal(1.0m, accounting.CryptoPaid); + Assert.Equal(1.0m, accounting.Paid); + Assert.Equal(5.2m, accounting.TotalDue); Assert.Equal(2, accounting.TxRequired); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due); - Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid); - Assert.Equal(Money.Coins(2.0m), accounting.Paid); - Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue); + Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due); + Assert.Equal(0.0m, accounting.CryptoPaid); + Assert.Equal(2.0m, accounting.Paid); + Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue); entity.Payments.Add(new PaymentEntity() { - CryptoCode = "LTC", + Currency = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.01m }); - + entity.UpdateTotals(); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due); - Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); - Assert.Equal(Money.Coins(1.5m), accounting.Paid); - Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added + Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due); + Assert.Equal(1.0m, accounting.CryptoPaid); + Assert.Equal(1.5m, accounting.Paid); + Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added Assert.Equal(2, accounting.TxRequired); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due); - Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); - Assert.Equal(Money.Coins(3.0m), accounting.Paid); - Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue); + Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due); + Assert.Equal(1.0m, accounting.CryptoPaid); + Assert.Equal(3.0m, accounting.Paid); + Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue); Assert.Equal(2, accounting.TxRequired); - var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2); + var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC); entity.Payments.Add(new PaymentEntity() { - CryptoCode = "BTC", - Output = new TxOut(remaining, new Key()), + Currency = "BTC", + Output = new TxOut(Money.Coins(remaining), new Key()), Accounted = true, NetworkFee = 0.1m }); - + entity.UpdateTotals(); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Zero, accounting.Due); - Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid); - Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid); - Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); + Assert.Equal(0.0m, accounting.Due); + Assert.Equal(1.0m + remaining, accounting.CryptoPaid); + Assert.Equal(1.5m + remaining, accounting.Paid); + Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(2, accounting.TxRequired); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Zero, accounting.Due); - Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); - Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid); + Assert.Equal(0.0m, accounting.Due); + Assert.Equal(1.0m, accounting.CryptoPaid); + Assert.Equal(3.0m + remaining * 2, accounting.Paid); // Paying 2 BTC fee, LTC fee removed because fully paid - Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), + Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */, accounting.TotalDue); Assert.Equal(1, accounting.TxRequired); Assert.Equal(accounting.Paid, accounting.TotalDue); @@ -548,27 +596,29 @@ namespace BTCPayServer.Tests entity.Payments = new List(); entity.SetPaymentMethod(new PaymentMethod() { - CryptoCode = "BTC", + Currency = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) }); entity.Price = 5000; entity.PaymentTolerance = 0; - + entity.UpdateTotals(); var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(1.1m), accounting.Due); - Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); - Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue); + Assert.Equal(1.1m, accounting.Due); + Assert.Equal(1.1m, accounting.TotalDue); + Assert.Equal(1.1m, accounting.MinimumTotalDue); entity.PaymentTolerance = 10; + entity.UpdateTotals(); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue); + Assert.Equal(0.99m, accounting.MinimumTotalDue); entity.PaymentTolerance = 100; + entity.UpdateTotals(); accounting = paymentMethod.Calculate(); - Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue); + Assert.Equal(0.0000_0001m, accounting.MinimumTotalDue); } [Fact] @@ -1064,7 +1114,7 @@ namespace BTCPayServer.Tests search = new SearchString(filter); Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First()); Assert.Equal("hekki", search.TextSearch); - + // modify search filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00"; search = new SearchString(filter); @@ -1074,33 +1124,33 @@ namespace BTCPayServer.Tests Assert.Single(search.Filters["status"], "settled"); Assert.Single(search.Filters["exceptionstatus"], "paidLate"); Assert.Single(search.Filters["unusual"], "true"); - + // toggle off bool with same value var modified = new SearchString(search.Toggle("unusual", "true")); Assert.Null(modified.GetFilterBool("unusual")); - + // add to array modified = new SearchString(modified.Toggle("status", "processing")); var statusArray = modified.GetFilterArray("status"); Assert.Equal(2, statusArray.Length); Assert.Contains("processing", statusArray); Assert.Contains("settled", statusArray); - + // toggle off array with same value modified = new SearchString(modified.Toggle("status", "settled")); statusArray = modified.GetFilterArray("status"); Assert.Single(statusArray, "processing"); - + // toggle off array with null value modified = new SearchString(modified.Toggle("status", null)); Assert.Null(modified.GetFilterArray("status")); - + // toggle off date with null value modified = new SearchString(modified.Toggle("startdate", "-7d")); Assert.Single(modified.GetFilterArray("startdate"), "-7d"); modified = new SearchString(modified.Toggle("startdate", null)); Assert.Null(modified.GetFilterArray("startdate")); - + // toggle off date with same value modified = new SearchString(modified.Toggle("enddate", "-7d")); Assert.Single(modified.GetFilterArray("enddate"), "-7d"); @@ -1884,11 +1934,6 @@ namespace BTCPayServer.Tests #pragma warning disable CS0618 var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString(); var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest); - var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] - { - new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null), - new LightningLikePaymentHandler(null, null, networkProvider, null, null, null), - }); var networkBTC = networkProvider.GetNetwork("BTC"); var networkLTC = networkProvider.GetNetwork("LTC"); InvoiceEntity invoiceEntity = new InvoiceEntity(); @@ -1896,14 +1941,14 @@ namespace BTCPayServer.Tests invoiceEntity.Payments = new System.Collections.Generic.List(); invoiceEntity.Price = 100; PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); - paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, } + paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, } .SetPaymentMethodDetails( new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() { NextNetworkFee = Money.Coins(0.00000100m), DepositAddress = dummy })); - paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m } + paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m } .SetPaymentMethodDetails( new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() { @@ -1919,7 +1964,7 @@ namespace BTCPayServer.Tests new PaymentEntity() { Accounted = true, - CryptoCode = "BTC", + Currency = "BTC", NetworkFee = 0.00000100m, Network = networkProvider.GetNetwork("BTC"), } @@ -1928,34 +1973,33 @@ namespace BTCPayServer.Tests Network = networkProvider.GetNetwork("BTC"), Output = new TxOut() { Value = Money.Coins(0.00151263m) } })); + invoiceEntity.UpdateTotals(); accounting = btc.Calculate(); invoiceEntity.Payments.Add( new PaymentEntity() { Accounted = true, - CryptoCode = "BTC", + Currency = "BTC", NetworkFee = 0.00000100m, Network = networkProvider.GetNetwork("BTC") } .SetCryptoPaymentData(new BitcoinLikePaymentData() { Network = networkProvider.GetNetwork("BTC"), - Output = new TxOut() { Value = accounting.Due } + Output = new TxOut() { Value = Money.Coins(accounting.Due) } })); + invoiceEntity.UpdateTotals(); accounting = btc.Calculate(); - Assert.Equal(Money.Zero, accounting.Due); - Assert.Equal(Money.Zero, accounting.DueUncapped); + Assert.Equal(0.0m, accounting.Due); + Assert.Equal(0.0m, accounting.DueUncapped); var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = ltc.Calculate(); - Assert.Equal(Money.Zero, accounting.Due); - // LTC might have over paid due to BTC paying above what it should (round 1 satoshi up) - Assert.True(accounting.DueUncapped < Money.Zero); - - var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2); - Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode); -#pragma warning restore CS0618 + Assert.Equal(0.0m, accounting.Due); + // LTC might should be over paid due to BTC paying above what it should (round 1 satoshi up), but we handle this case + // and set DueUncapped to zero. + Assert.Equal(0.0m, accounting.DueUncapped); } [Fact] diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 34e86e01d..8a511f149 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1761,7 +1761,7 @@ namespace BTCPayServer.Tests var parsedJson = await GetExport(user); Assert.Equal(3, parsedJson.Length); - var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate; + var invoiceDueAfterFirstPayment = 3 * networkFee.ToDecimal(MoneyUnit.BTC) * invoice.Rate; var pay1str = parsedJson[0].ToString(); Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str); Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue")); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs index 5b6521dc1..865089773 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs @@ -396,7 +396,7 @@ namespace BTCPayServer.Controllers.Greenfield return this.CreateValidationError(ModelState); var accounting = invoicePaymentMethod.Calculate(); - var cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC); + var cryptoPaid = accounting.Paid; var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true); var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility); var rateResult = await _rateProvider.FetchRate( @@ -464,7 +464,7 @@ namespace BTCPayServer.Controllers.Greenfield return this.CreateValidationError(ModelState); } - var dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC); + var dueAmount = accounting.TotalDue; createPullPayment.Currency = cryptoCode; createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility); createPullPayment.AutoApproveClaims = true; @@ -580,11 +580,11 @@ namespace BTCPayServer.Controllers.Greenfield CryptoCode = method.GetId().CryptoCode, Destination = details.GetPaymentDestination(), Rate = method.Rate, - Due = accounting.DueUncapped.ToDecimal(MoneyUnit.BTC), - TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC), - PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), - Amount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC), - NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC), + Due = accounting.DueUncapped, + TotalPaid = accounting.Paid, + PaymentMethodPaid = accounting.CryptoPaid, + Amount = accounting.TotalDue, + NetworkFee = accounting.NetworkFee, PaymentLink = method.GetId().PaymentType.GetPaymentLink(method.Network, entity, details, accounting.Due, Request.GetAbsoluteRoot()), diff --git a/BTCPayServer/Controllers/UIInvoiceController.Testing.cs b/BTCPayServer/Controllers/UIInvoiceController.Testing.cs index e6693bc5e..d48595088 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.Testing.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.Testing.cs @@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers return Ok(new { Txid = txid, - AmountRemaining = (paymentMethod.Calculate().Due - amount).ToUnit(MoneyUnit.BTC), + AmountRemaining = paymentMethod.Calculate().Due - amount.ToDecimal(MoneyUnit.BTC), SuccessMessage = $"Created transaction {txid}" }); @@ -70,11 +70,11 @@ namespace BTCPayServer.Controllers { var bolt11 = BOLT11PaymentRequest.Parse(destination, network); var paymentHash = bolt11.PaymentHash?.ToString(); - var paid = new Money(response.Details.TotalAmount.ToUnit(LightMoneyUnit.Satoshi), MoneyUnit.Satoshi); + var paid = response.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC); return Ok(new { Txid = paymentHash, - AmountRemaining = (paymentMethod.Calculate().TotalDue - paid).ToUnit(MoneyUnit.BTC), + AmountRemaining = paymentMethod.Calculate().TotalDue - paid, SuccessMessage = $"Sent payment {paymentHash}" }); } diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs index becf5f85b..0560ded13 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs @@ -228,18 +228,14 @@ namespace BTCPayServer.Controllers string txId = paymentData.GetPaymentId(); string? link = GetTransactionLink(paymentMethodId, txId); - var paymentMethod = i.GetPaymentMethod(paymentMethodId); - var amount = paymentData.GetValue(); - var rate = paymentMethod.Rate; - var paid = (amount - paymentEntity.NetworkFee) * rate; - + return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment { - Amount = amount, - Paid = paid, + Amount = paymentEntity.PaidAmount.Gross, + Paid = paymentEntity.PaidAmount.Net, ReceivedDate = paymentEntity.ReceivedTime.DateTime, - PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol), - RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol), + PaidFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol), + RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol), PaymentMethod = paymentMethodId.ToPrettyString(), Link = link, Id = txId, @@ -364,8 +360,8 @@ namespace BTCPayServer.Controllers if (paymentMethod != null) { accounting = paymentMethod.Calculate(); - cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC); - dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC); + cryptoPaid = accounting.Paid; + dueAmount = accounting.TotalDue; paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility); } @@ -560,7 +556,7 @@ namespace BTCPayServer.Controllers { var accounting = data.Calculate(); var paymentMethodId = data.GetId(); - var overpaidAmount = accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC); + var overpaidAmount = accounting.OverpaidHelper; if (overpaidAmount > 0) { @@ -571,8 +567,8 @@ namespace BTCPayServer.Controllers { PaymentMethodId = paymentMethodId, PaymentMethod = paymentMethodId.ToPrettyString(), - Due = _displayFormatter.Currency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode), - Paid = _displayFormatter.Currency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode), + Due = _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode), + Paid = _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode), Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode), Address = data.GetPaymentMethodDetails().GetPaymentDestination(), Rate = ExchangeRate(data.GetId().CryptoCode, data), @@ -827,7 +823,6 @@ namespace BTCPayServer.Controllers var dto = invoice.EntityToDTO(); var accounting = paymentMethod.Calculate(); var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId]; - var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits; switch (lang?.ToLowerInvariant()) { @@ -885,10 +880,10 @@ namespace BTCPayServer.Controllers OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback, CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)), BtcAddress = paymentMethodDetails.GetPaymentDestination(), - BtcDue = accounting.Due.ShowMoney(divisibility), - BtcPaid = accounting.Paid.ShowMoney(divisibility), + BtcDue = accounting.ShowMoney(accounting.Due), + BtcPaid = accounting.ShowMoney(accounting.Paid), InvoiceCurrency = invoice.Currency, - OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility), + OrderAmount = accounting.ShowMoney(accounting.TotalDue - accounting.NetworkFee), IsUnsetTopUp = invoice.IsUnsetTopUp(), CustomerEmail = invoice.RefundMail, RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail, diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs index 21eb94631..e93aa1aa8 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.cs @@ -121,7 +121,7 @@ namespace BTCPayServer.Controllers internal async Task CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List? additionalTags = null, CancellationToken cancellationToken = default, Action? entityManipulator = null) { var storeBlob = store.GetStoreBlob(); - var entity = _InvoiceRepository.CreateNewInvoice(); + var entity = _InvoiceRepository.CreateNewInvoice(store.Id); entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration; entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration; if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime) @@ -237,7 +237,7 @@ namespace BTCPayServer.Controllers public async Task CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List? additionalTags = null, CancellationToken cancellationToken = default, Action? entityManipulator = null) { var storeBlob = store.GetStoreBlob(); - var entity = _InvoiceRepository.CreateNewInvoice(); + var entity = _InvoiceRepository.CreateNewInvoice(store.Id); entity.ServerUrl = serverUrl; entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration); entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration); @@ -314,6 +314,7 @@ namespace BTCPayServer.Controllers entity.RefundMail = entity.Metadata.BuyerEmail; } entity.Status = InvoiceStatusLegacy.New; + entity.UpdateTotals(); HashSet currencyPairsToFetch = new HashSet(); var rules = storeBlob.GetRateRules(_NetworkProvider); var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any() @@ -402,7 +403,7 @@ namespace BTCPayServer.Controllers } using (logs.Measure("Saving invoice")) { - entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, additionalSearchTerms); + await _InvoiceRepository.CreateInvoiceAsync(entity, additionalSearchTerms); foreach (var method in paymentMethods) { if (method.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod bp) @@ -506,7 +507,7 @@ namespace BTCPayServer.Controllers await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)]; if (currentRateToCrypto?.BidAsk != null) { - var amount = paymentMethod.Calculate().Due.GetValue(network as BTCPayNetwork); + var amount = paymentMethod.Calculate().Due; var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid; if (amount < limitValueCrypto && criteria.Above) diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 0a7642c98..33172d26d 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -548,7 +548,7 @@ namespace BTCPayServer lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value })); if (i.Type != InvoiceType.TopUp) { - lnurlRequest.MinSendable = new LightMoney(pm.Calculate().Due.ToDecimal(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi); + lnurlRequest.MinSendable = LightMoney.Coins(pm.Calculate().Due); if (!allowOverpay) lnurlRequest.MaxSendable = lnurlRequest.MinSendable; } diff --git a/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs b/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs index f8dd970a1..010dae521 100644 --- a/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs +++ b/BTCPayServer/Data/Payouts/BitcoinLike/BitcoinLikePayoutHandler.cs @@ -303,7 +303,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler bip21.Add(newUri.Uri.ToString()); break; case AddressClaimDestination addressClaimDestination: - var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), new Money(blob.CryptoAmount.Value, MoneyUnit.BTC)); + var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), blob.CryptoAmount.Value); bip21New.QueryParams.Add("payout", payout.Id); bip21.Add(bip21New.ToString()); break; diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 8300dc1e8..e5050a9ad 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -30,6 +30,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NBitcoin; using NBitcoin.Payment; +using NBitpayClient; using NBXplorer.Models; using Newtonsoft.Json; using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo; @@ -38,6 +39,15 @@ namespace BTCPayServer { public static class Extensions { + public static DateTimeOffset TruncateMilliSeconds(this DateTimeOffset dt) => new (dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, dt.Offset); + public static decimal? GetDue(this InvoiceCryptoInfo invoiceCryptoInfo) + { + if (invoiceCryptoInfo is null) + return null; + if (decimal.TryParse(invoiceCryptoInfo.Due, NumberStyles.Any, CultureInfo.InvariantCulture, out var v)) + return v; + return null; + } public static Task Bufferize(this IFormFile formFile) { return BufferizedFormFile.Bufferize(formFile); @@ -382,20 +392,6 @@ namespace BTCPayServer return controller.View("PostRedirect", redirectVm); } - public static string ToSql(this IQueryable query) where TEntity : class - { - var enumerator = query.Provider.Execute>(query.Expression).GetEnumerator(); - var relationalCommandCache = enumerator.Private("_relationalCommandCache"); - var selectExpression = relationalCommandCache.Private("_selectExpression"); - var factory = relationalCommandCache.Private("_querySqlGeneratorFactory"); - - var sqlGenerator = factory.Create(); - var command = sqlGenerator.GetCommand(selectExpression); - - string sql = command.CommandText; - return sql; - } - public static BTCPayNetworkProvider ConfigureNetworkProvider(this IConfiguration configuration, Logs logs) { var _networkType = DefaultConfiguration.GetNetworkType(configuration); diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index f2f721ff8..04711c308 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using BTCPayServer.Client.Models; using BTCPayServer.Events; using BTCPayServer.Logging; +using BTCPayServer.Payments; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications.Blobs; @@ -83,31 +84,13 @@ namespace BTCPayServer.HostedServices if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial) { PaidPartial = paidPartial }); } - var allPaymentMethods = invoice.GetPaymentMethods(); - var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting); - if (allPaymentMethods.Any() && paymentMethod == null) - return; - if (accounting is null && invoice.Price is 0m) - { - accounting = new PaymentMethodAccounting() - { - Due = Money.Zero, - Paid = Money.Zero, - CryptoPaid = Money.Zero, - DueUncapped = Money.Zero, - NetworkFee = Money.Zero, - TotalDue = Money.Zero, - TxCount = 0, - TxRequired = 0, - MinimumTotalDue = Money.Zero, - NetworkFeeAlreadyPaid = Money.Zero - }; - } + + var hasPayment = invoice.GetPayments(true).Any(); if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired) { var isPaid = invoice.IsUnsetTopUp() ? - accounting.Paid > Money.Zero : - accounting.Paid >= accounting.MinimumTotalDue; + hasPayment : + !invoice.IsUnderPaid; if (isPaid) { if (invoice.Status == InvoiceStatusLegacy.New) @@ -117,13 +100,15 @@ namespace BTCPayServer.HostedServices if (invoice.IsUnsetTopUp()) { invoice.ExceptionStatus = InvoiceExceptionStatus.None; - invoice.Price = (accounting.Paid - accounting.NetworkFeeAlreadyPaid).ToDecimal(MoneyUnit.BTC) * paymentMethod.Rate; - accounting = paymentMethod.Calculate(); + // We know there is at least one payment because hasPayment is true + var payment = invoice.GetPayments(true).First(); + invoice.Price = payment.InvoicePaidAmount.Net; + invoice.UpdateTotals(); context.BlobUpdated(); } else { - invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None; + invoice.ExceptionStatus = invoice.IsOverPaid ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None; } context.MarkDirty(); } @@ -135,7 +120,7 @@ namespace BTCPayServer.HostedServices } } - if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments(true).Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial) + if (hasPayment && invoice.IsUnderPaid && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial; context.MarkDirty(); @@ -145,43 +130,43 @@ namespace BTCPayServer.HostedServices // Just make sure RBF did not cancelled a payment if (invoice.Status == InvoiceStatusLegacy.Paid) { - if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver) + if (!invoice.IsUnderPaid && !invoice.IsOverPaid && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver) { invoice.ExceptionStatus = InvoiceExceptionStatus.None; context.MarkDirty(); } - if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver) + if (invoice.IsOverPaid && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver) { invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver; context.MarkDirty(); } - if (accounting.Paid < accounting.MinimumTotalDue) + if (invoice.IsUnderPaid) { invoice.Status = InvoiceStatusLegacy.New; - invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial; + invoice.ExceptionStatus = hasPayment ? InvoiceExceptionStatus.PaidPartial : InvoiceExceptionStatus.None; context.MarkDirty(); } } if (invoice.Status == InvoiceStatusLegacy.Paid) { - var confirmedAccounting = - paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)) ?? - accounting; + var unconfPayments = invoice.GetPayments(true).Where(p => !p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)).ToList(); + var unconfirmedPaid = unconfPayments.Select(p => p.InvoicePaidAmount.Net).Sum(); + var minimumDue = invoice.MinimumNetDue + unconfirmedPaid; if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed - (confirmedAccounting.Paid < accounting.MinimumTotalDue)) + (minimumDue > 0.0m)) { context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm)); invoice.Status = InvoiceStatusLegacy.Invalid; context.MarkDirty(); } - else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue) + else if (minimumDue <= 0.0m) { invoice.Status = InvoiceStatusLegacy.Confirmed; context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed)); @@ -191,9 +176,11 @@ namespace BTCPayServer.HostedServices if (invoice.Status == InvoiceStatusLegacy.Confirmed) { - var completedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)) ?? - accounting; - if (completedAccounting.Paid >= accounting.MinimumTotalDue) + var unconfPayments = invoice.GetPayments(true).Where(p => !p.GetCryptoPaymentData().PaymentCompleted(p)).ToList(); + var unconfirmedPaid = unconfPayments.Select(p => p.InvoicePaidAmount.Net).Sum(); + var minimumDue = invoice.MinimumNetDue + unconfirmedPaid; + + if (minimumDue <= 0.0m) { context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed)); invoice.Status = InvoiceStatusLegacy.Complete; @@ -203,25 +190,6 @@ namespace BTCPayServer.HostedServices } - public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting) - { - PaymentMethod result = null; - accounting = null; - decimal nearestToZero = 0.0m; - foreach (var paymentMethod in allPaymentMethods) - { - var currentAccounting = paymentMethod.Calculate(); - var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC)); - if (result == null || distanceFromZero < nearestToZero) - { - result = paymentMethod; - nearestToZero = distanceFromZero; - accounting = currentAccounting; - } - } - return result; - } - private void Watch(string invoiceId) { ArgumentNullException.ThrowIfNull(invoiceId); @@ -380,7 +348,7 @@ namespace BTCPayServer.HostedServices if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted) && (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) { - var client = _explorerClientProvider.GetExplorerClient(payment.GetCryptoCode()); + var client = _explorerClientProvider.GetExplorerClient(payment.Currency); var transactionResult = client is null ? null : await client.GetTransactionAsync(onChainPaymentData.Outpoint.Hash); var confirmationCount = transactionResult?.Confirmations ?? 0; onChainPaymentData.ConfirmationCount = confirmationCount; diff --git a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs index 451b5904b..9c0f5dad1 100644 --- a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs +++ b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs @@ -103,7 +103,7 @@ namespace BTCPayServer.HostedServices invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance && invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData: { - var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode()); + var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.Currency); var transactionId = bitcoinLikePaymentData.Outpoint.Hash; var labels = new List { diff --git a/BTCPayServer/PaymentRequest/PaymentRequestHub.cs b/BTCPayServer/PaymentRequest/PaymentRequestHub.cs index 19885caff..c6c5d12e8 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestHub.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestHub.cs @@ -167,7 +167,7 @@ namespace BTCPayServer.PaymentRequest new object[] { data.GetValue(), - invoiceEvent.Payment.GetCryptoCode(), + invoiceEvent.Payment.Currency, invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString() }, cancellationToken); } diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index bb1522493..ed1688f93 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -123,18 +123,14 @@ namespace BTCPayServer.PaymentRequest string txId = paymentData.GetPaymentId(); string link = GetTransactionLink(paymentMethodId, txId); - var paymentMethod = entity.GetPaymentMethod(paymentMethodId); - var amount = paymentData.GetValue(); - var rate = paymentMethod.Rate; - var paid = (amount - paymentEntity.NetworkFee) * rate; return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment { - Amount = amount, - Paid = paid, + Amount = paymentEntity.PaidAmount.Gross, + Paid = paymentEntity.InvoicePaidAmount.Net, ReceivedDate = paymentEntity.ReceivedTime.DateTime, - PaidFormatted = _displayFormatter.Currency(paid, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), - RateFormatted = _displayFormatter.Currency(rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), + PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), + RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), PaymentMethod = paymentMethodId.ToPrettyString(), Link = link, Id = txId, diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index f478d86e4..3000b39c4 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -211,7 +211,7 @@ namespace BTCPayServer.Payments.Bitcoin new Key().GetScriptPubKey(supportedPaymentMethod.AccountDerivation.ScriptPubKeyType()); var dust = txOut.GetDustThreshold(); var amount = paymentMethod.Calculate().Due; - if (amount < dust) + if (amount < dust.ToDecimal(MoneyUnit.BTC)) throw new PaymentMethodUnavailableException("Amount below the dust threshold. For amounts of this size, it is recommended to enable an off-chain (Lightning) payment method"); } if (preparePaymentObject is null) diff --git a/BTCPayServer/Payments/LNURLPay/PaymentTypes.LNURL.cs b/BTCPayServer/Payments/LNURLPay/PaymentTypes.LNURL.cs index fec52ed62..c4cbfd9c3 100644 --- a/BTCPayServer/Payments/LNURLPay/PaymentTypes.LNURL.cs +++ b/BTCPayServer/Payments/LNURLPay/PaymentTypes.LNURL.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client.Models; using BTCPayServer.Payments.Lightning; @@ -28,7 +29,7 @@ namespace BTCPayServer.Payments } public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, - Money cryptoInfoDue, string serverUri) + decimal cryptoInfoDue, string serverUri) { if (!paymentMethodDetails.Activated) { @@ -74,7 +75,7 @@ namespace BTCPayServer.Payments { AdditionalData = new Dictionary() { - {"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due, + {"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.GetDue().Value, serverUrl))} } }; diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 7c2252301..ded80ad98 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -73,7 +73,7 @@ namespace BTCPayServer.Payments.Lightning decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility); try { - due = paymentMethod.Calculate().Due.ToDecimal(MoneyUnit.BTC); + due = paymentMethod.Calculate().Due; } catch (Exception) { diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index a447ed577..1dd1e78d4 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -200,7 +200,7 @@ namespace BTCPayServer.Payments.Lightning if (inv.Name == InvoiceEvent.ReceivedPayment && inv.Invoice.Status == InvoiceStatusLegacy.New && inv.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) { var pm = inv.Invoice.GetPaymentMethods().First(); - if (pm.Calculate().Due.GetValue(pm.Network as BTCPayNetwork) > 0m) + if (pm.Calculate().Due > 0m) { await CreateNewLNInvoiceForBTCPayInvoice(inv.Invoice); } diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs index 1a668a14c..4a4034f1d 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs @@ -302,7 +302,7 @@ namespace BTCPayServer.Payments.PayJoin var paymentDetails = paymentMethod?.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentMethod is null || paymentDetails is null || !paymentDetails.PayjoinEnabled) continue; - due = paymentMethod.Calculate().TotalDue - output.Value; + due = Money.Coins(paymentMethod.Calculate().TotalDue) - output.Value; if (due > Money.Zero) { break; diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index 20c364fc2..7e255f6d6 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -67,7 +67,7 @@ namespace BTCPayServer.Payments } public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, - Money cryptoInfoDue, string serverUri) + decimal cryptoInfoDue, string serverUri) { if (!paymentMethodDetails.Activated) { @@ -105,7 +105,7 @@ namespace BTCPayServer.Payments { cryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls() { - BIP21 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), cryptoInfo.Due, serverUrl), + BIP21 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), cryptoInfo.GetDue().Value, serverUrl), }; } } diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs index 6ea0709f8..493c16ce1 100644 --- a/BTCPayServer/Payments/PaymentTypes.Lightning.cs +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using BTCPayServer.Client.Models; using BTCPayServer.Controllers.Greenfield; using BTCPayServer.Payments.Lightning; @@ -52,7 +53,7 @@ namespace BTCPayServer.Payments } public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, - Money cryptoInfoDue, string serverUri) + decimal cryptoInfoDue, string serverUri) { if (!paymentMethodDetails.Activated) { @@ -92,7 +93,7 @@ namespace BTCPayServer.Payments { invoiceCryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls() { - BOLT11 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due, + BOLT11 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.GetDue().Value, serverUrl) }; } diff --git a/BTCPayServer/Payments/PaymentTypes.cs b/BTCPayServer/Payments/PaymentTypes.cs index 92c75db64..c83f082e8 100644 --- a/BTCPayServer/Payments/PaymentTypes.cs +++ b/BTCPayServer/Payments/PaymentTypes.cs @@ -85,7 +85,7 @@ namespace BTCPayServer.Payments public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value); public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId); public abstract string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, - Money cryptoInfoDue, string serverUri); + decimal cryptoInfoDue, string serverUri); public abstract string InvoiceViewPaymentPartialName { get; } public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore); diff --git a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs index fe68e72c5..9aec0a34e 100644 --- a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs +++ b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs @@ -89,15 +89,7 @@ namespace BTCPayServer.Plugins.Crowdfund .GroupBy(entity => entity.Metadata.ItemCode) .Select(entities => { - var total = entities - .Sum(entity => entity.GetPayments(true) - .Sum(pay => - { - var paymentMethodId = pay.GetPaymentMethodId(); - var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee; - var rate = entity.GetPaymentMethod(paymentMethodId).Rate; - return rate * value; - })); + var total = entities.Sum(entity => entity.PaidAmount.Net); var itemCode = entities.Key; var perk = perks.FirstOrDefault(p => p.Id == itemCode); return new ItemStats @@ -167,13 +159,7 @@ namespace BTCPayServer.Plugins.Crowdfund !string.IsNullOrEmpty(entity.Metadata.ItemCode)) .GroupBy(entity => entity.Metadata.ItemCode) .ToDictionary(entities => entities.Key, entities => - entities.Sum(entity => entity.GetPayments(true).Sum(pay => - { - var paymentMethodId = pay.GetPaymentMethodId(); - var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee; - var rate = entity.GetPaymentMethod(paymentMethodId).Rate; - return rate * value; - }))); + entities.Sum(entity => entity.PaidAmount.Net)); } var perks = AppService.Parse( settings.PerksTemplate, false); diff --git a/BTCPayServer/Plugins/NFC/NFCController.cs b/BTCPayServer/Plugins/NFC/NFCController.cs index 4e1c7e79c..9704a8c99 100644 --- a/BTCPayServer/Plugins/NFC/NFCController.cs +++ b/BTCPayServer/Plugins/NFC/NFCController.cs @@ -119,7 +119,7 @@ namespace BTCPayServer.Plugins.NFC } else { - due = new LightMoney(lnPaymentMethod.Calculate().Due); + due = LightMoney.Coins(lnPaymentMethod.Calculate().Due); } if (info.MinWithdrawable > due || due > info.MaxWithdrawable) @@ -135,10 +135,10 @@ namespace BTCPayServer.Plugins.NFC if (lnurlPaymentMethod is not null) { - Money due; + decimal due; if (invoice.Type == InvoiceType.TopUp && request.Amount is not null) { - due = new Money(request.Amount.Value, MoneyUnit.Satoshi); + due = new Money(request.Amount.Value, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC); } else if (invoice.Type == InvoiceType.TopUp) { @@ -152,7 +152,7 @@ namespace BTCPayServer.Plugins.NFC try { httpClient = CreateHttpClient(info.Callback); - var amount = LightMoney.Satoshis(due.Satoshi); + var amount = LightMoney.Coins(due); var actionPath = Url.Action(nameof(UILNURLController.GetLNURLForInvoice), "UILNURL", new { invoiceId = request.InvoiceId, cryptoCode = "BTC", amount = amount.MilliSatoshi }); var url = Request.GetAbsoluteUri(actionPath); diff --git a/BTCPayServer/Program.cs b/BTCPayServer/Program.cs index 9f8be22c7..1e14ac739 100644 --- a/BTCPayServer/Program.cs +++ b/BTCPayServer/Program.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; [assembly: InternalsVisibleTo("BTCPayServer.Tests")] + namespace BTCPayServer { class Program diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs index 8d019ab0f..2e0f0c412 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs @@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments { var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network, null, - new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due, + new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.GetDue().Value, null); model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; } diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs index 375d757c0..5be8982be 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs @@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId); } - public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) + public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, decimal cryptoInfoDue, string serverUri) { return paymentMethodDetails.Activated - ? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}" + ? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue}" : string.Empty; } diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs index 905e96770..4b299769d 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs @@ -119,16 +119,16 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment) { _logger.LogInformation( - $"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}"); + $"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.Currency} {payment.GetCryptoPaymentData().GetPaymentId()}"); var paymentData = (MoneroLikePaymentData)payment.GetCryptoPaymentData(); var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance); if (paymentMethod != null && paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero && monero.Activated && monero.GetPaymentDestination() == paymentData.GetDestination() && - paymentMethod.Calculate().Due > Money.Zero) + paymentMethod.Calculate().Due > 0.0m) { - var walletClient = _moneroRpcProvider.WalletRpcClients[payment.GetCryptoCode()]; + var walletClient = _moneroRpcProvider.WalletRpcClients[payment.Currency]; var address = await walletClient.SendCommandAsync( "create_address", diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs index b5a7fa71b..998087924 100644 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs +++ b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs @@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments { var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); model.InvoiceBitcoinUrl = ZcashPaymentType.Instance.GetPaymentLink(network, null, - new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due, + new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.GetDue().Value, null); model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; } diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentType.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentType.cs index 62f9d51d4..4ad64acfa 100644 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentType.cs +++ b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentType.cs @@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId); } - public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) + public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, decimal cryptoInfoDue, string serverUri) { return paymentMethodDetails.Activated - ? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}" + ? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue}" : string.Empty; } diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs index 08333161a..8ce3b9312 100644 --- a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs +++ b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs @@ -114,16 +114,16 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment) { _logger.LogInformation( - $"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}"); + $"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.Currency} {payment.GetCryptoPaymentData().GetPaymentId()}"); var paymentData = (ZcashLikePaymentData)payment.GetCryptoPaymentData(); var paymentMethod = invoice.GetPaymentMethod(payment.Network, ZcashPaymentType.Instance); if (paymentMethod != null && paymentMethod.GetPaymentMethodDetails() is ZcashLikeOnChainPaymentMethodDetails Zcash && Zcash.Activated && Zcash.GetPaymentDestination() == paymentData.GetDestination() && - paymentMethod.Calculate().Due > Money.Zero) + paymentMethod.Calculate().Due > 0.0m) { - var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.GetCryptoCode()]; + var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.Currency]; var address = await walletClient.SendCommandAsync( "create_address", diff --git a/BTCPayServer/Services/Apps/AppHubStreamer.cs b/BTCPayServer/Services/Apps/AppHubStreamer.cs index b8fe95aa7..6c705ffc2 100644 --- a/BTCPayServer/Services/Apps/AppHubStreamer.cs +++ b/BTCPayServer/Services/Apps/AppHubStreamer.cs @@ -40,7 +40,7 @@ namespace BTCPayServer.Services.Apps await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.PaymentReceived, new object[] { data.GetValue(), - invoiceEvent.Payment.GetCryptoCode(), + invoiceEvent.Payment.Currency, invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString() }, cancellationToken); } diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index bb16a0dcf..2565b1777 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -183,18 +183,11 @@ namespace BTCPayServer.Services.Apps } } else - { - var fiatPrice = e.GetPayments(true).Sum(pay => - { - var paymentMethodId = pay.GetPaymentMethodId(); - var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee; - var rate = e.GetPaymentMethod(paymentMethodId).Rate; - return rate * value; - }); + {; res.Add(new InvoiceStatsItem { ItemCode = e.Metadata.ItemCode, - FiatPrice = fiatPrice, + FiatPrice = e.PaidAmount.Net, Date = e.InvoiceTime.Date }); } diff --git a/BTCPayServer/Services/Invoices/Amounts.cs b/BTCPayServer/Services/Invoices/Amounts.cs new file mode 100644 index 000000000..c4e118523 --- /dev/null +++ b/BTCPayServer/Services/Invoices/Amounts.cs @@ -0,0 +1,19 @@ +namespace BTCPayServer.Services.Invoices +{ + public class Amounts + { + public string Currency { get; set; } + /// + /// An amount with fee included + /// + public decimal Gross { get; set; } + /// + /// An amount without fee included + /// + public decimal Net { get; set; } + public override string ToString() + { + return $"{Currency}: Net={Net}, Gross={Gross}"; + } + } +} diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index ade3fe720..ff0a2e2c3 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -64,23 +64,19 @@ namespace BTCPayServer.Services.Invoices.Export { foreach (var payment in payments) { - var cryptoCode = payment.GetPaymentMethodId().CryptoCode; var pdata = payment.GetCryptoPaymentData(); - - var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId()); - var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee; - invoiceDue -= paidAfterNetworkFees * pmethod.Rate; + invoiceDue -= payment.InvoicePaidAmount.Net; var target = new ExportInvoiceHolder { ReceivedDate = payment.ReceivedTime.UtcDateTime, PaymentId = pdata.GetPaymentId(), - CryptoCode = cryptoCode, - ConversionRate = pmethod.Rate, + CryptoCode = payment.Currency, + ConversionRate = payment.Rate, PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(), Destination = pdata.GetDestination(), - Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture), - PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture), + Paid = payment.PaidAmount.Gross.ToString(CultureInfo.InvariantCulture), + PaidCurrency = Math.Round(payment.InvoicePaidAmount.Gross, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture), // Adding NetworkFee because Paid doesn't take into account network fees // 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 diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 1a32a52f1..19098af03 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -378,6 +378,82 @@ namespace BTCPayServer.Services.Invoices #pragma warning restore CS0618 } + [JsonIgnore] + public Dictionary Rates + { + get; + private set; + } + public void UpdateTotals() + { + Rates = new Dictionary(); + foreach (var p in GetPaymentMethods()) + { + Rates.TryAdd(p.Currency, p.Rate); + } + PaidAmount = new Amounts() + { + Currency = Currency + }; + foreach (var payment in GetPayments(false)) + { + payment.Rate = Rates[payment.Currency]; + payment.InvoiceEntity = this; + payment.UpdateAmounts(); + if (payment.Accounted) + { + PaidAmount.Gross += payment.InvoicePaidAmount.Gross; + PaidAmount.Net += payment.InvoicePaidAmount.Net; + } + } + NetDue = Price - PaidAmount.Net; + MinimumNetDue = Price * (1.0m - ((decimal)PaymentTolerance / 100.0m)) - PaidAmount.Net; + PaidFee = PaidAmount.Gross - PaidAmount.Net; + if (NetDue < 0.0m) + { + // If any payment method exactly pay the invoice, the overpayment is caused by + // rounding limitation of the underlying payment method. + // Document this overpayment as dust, and set the net due to 0 + if (GetPaymentMethods().Any(p => p.Calculate().DueUncapped == 0.0m)) + { + Dust = -NetDue; + NetDue = 0.0m; + } + } + } + + /// + /// Overpaid amount caused by payment method + /// Example: If you need to pay 124.4 sats, the on-chain payment need to be technically rounded to 125 sats, the extra 0.6 sats shouldn't be considered an over payment. + /// + [JsonIgnore] + public decimal Dust { get; set; } + + /// + /// The due to consider the invoice paid (can be negative if over payment) + /// + [JsonIgnore] + public decimal NetDue + { + get; + set; + } + /// + /// Minumum due to consider the invoice paid (can be negative if overpaid) + /// + [JsonIgnore] + public decimal MinimumNetDue { get; set; } + public bool IsUnderPaid => MinimumNetDue > 0; + [JsonIgnore] + public bool IsOverPaid => NetDue < 0; + + + /// + /// Total of network fee paid by accounted payments + /// + [JsonIgnore] + public decimal PaidFee { get; set; } + [JsonIgnore] public InvoiceStatusLegacy Status { get; set; } [JsonProperty(PropertyName = "status")] @@ -399,7 +475,7 @@ namespace BTCPayServer.Services.Invoices } public List GetPayments(string cryptoCode, bool accountedOnly) { - return GetPayments(accountedOnly).Where(p => p.CryptoCode == cryptoCode).ToList(); + return GetPayments(accountedOnly).Where(p => p.Currency == cryptoCode).ToList(); } public List GetPayments(BTCPayNetworkBase network, bool accountedOnly) { @@ -554,13 +630,13 @@ namespace BTCPayServer.Services.Invoices if (paymentId.PaymentType == PaymentTypes.BTCLike) { var minerInfo = new MinerFeeInfo(); - minerInfo.TotalFee = accounting.NetworkFee.Satoshi; + minerInfo.TotalFee = accounting.ToSmallestUnit(accounting.NetworkFee); minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate .GetFee(1).Satoshi; dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); #pragma warning disable 618 - if (info.CryptoCode == "BTC") + if (info.Currency == "BTC") { dto.BTCPrice = cryptoInfo.Price; dto.Rate = cryptoInfo.Rate; @@ -576,8 +652,8 @@ namespace BTCPayServer.Services.Invoices dto.CryptoInfo.Add(cryptoInfo); dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls); - dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi); - dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi); + dto.PaymentSubtotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(subtotalPrice)); + dto.PaymentTotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(accounting.TotalDue)); dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency() { Enabled = true @@ -640,12 +716,12 @@ namespace BTCPayServer.Services.Invoices { continue; } - r.CryptoCode = paymentMethodId.CryptoCode; + r.Currency = paymentMethodId.CryptoCode; r.PaymentType = paymentMethodId.PaymentType.ToString(); r.ParentEntity = this; if (Networks != null) { - r.Network = Networks.GetNetwork(r.CryptoCode); + r.Network = Networks.GetNetwork(r.Currency); if (r.Network is null) continue; } @@ -671,7 +747,7 @@ namespace BTCPayServer.Services.Invoices foreach (var v in paymentMethods) { var clone = serializer.ToObject(serializer.ToString(v)); - clone.CryptoCode = null; + clone.Currency = null; clone.PaymentType = null; obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone)))); } @@ -681,6 +757,7 @@ namespace BTCPayServer.Services.Invoices cryptoData.ParentEntity = this; } #pragma warning restore CS0618 + UpdateTotals(); } public InvoiceState GetInvoiceState() @@ -748,6 +825,8 @@ namespace BTCPayServer.Services.Invoices { return Type == InvoiceType.TopUp && Price == 0.0m; } + + public Amounts PaidAmount { get; set; } } public enum InvoiceStatusLegacy @@ -897,30 +976,31 @@ namespace BTCPayServer.Services.Invoices public class PaymentMethodAccounting { + public int Divisibility { get; set; } /// Total amount of this invoice - public Money TotalDue { get; set; } + public decimal TotalDue { get; set; } /// Amount of crypto remaining to pay this invoice - public Money Due { get; set; } + public decimal Due { get; set; } /// Same as Due, can be negative - public Money DueUncapped { get; set; } + public decimal DueUncapped { get; set; } /// If DueUncapped is negative, that means user overpaid invoice - public Money OverpaidHelper + public decimal OverpaidHelper { - get { return DueUncapped > Money.Zero ? Money.Zero : -DueUncapped; } + get { return DueUncapped > 0.0m ? 0.0m : -DueUncapped; } } /// /// Total amount of the invoice paid after conversion to this crypto currency /// - public Money Paid { get; set; } + public decimal Paid { get; set; } /// /// Total amount of the invoice paid in this currency /// - public Money CryptoPaid { get; set; } + public decimal CryptoPaid { get; set; } /// /// Number of transactions required to pay @@ -934,15 +1014,25 @@ namespace BTCPayServer.Services.Invoices /// /// Total amount of network fee to pay to the invoice /// - public Money NetworkFee { get; set; } + public decimal NetworkFee { get; set; } /// /// Total amount of network fee to pay to the invoice /// - public Money NetworkFeeAlreadyPaid { get; set; } + public decimal NetworkFeeAlreadyPaid { get; set; } /// /// Minimum required to be paid in order to accept invoice as paid /// - public Money MinimumTotalDue { get; set; } + public decimal MinimumTotalDue { get; set; } + + public decimal ToSmallestUnit(decimal v) + { + for (int i = 0; i < Divisibility; i++) + { + v *= 10.0m; + } + return v; + } + public string ShowMoney(decimal v) => MoneyExtensions.ShowMoney(v, Divisibility); } public interface IPaymentMethod @@ -959,8 +1049,7 @@ namespace BTCPayServer.Services.Invoices [JsonIgnore] public BTCPayNetworkBase Network { get; set; } [JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)] - [Obsolete("Use GetId().CryptoCode instead")] - public string CryptoCode { get; set; } + public string Currency { get; set; } [JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)] [Obsolete("Use GetId().PaymentType instead")] public string PaymentType { get; set; } @@ -975,14 +1064,14 @@ namespace BTCPayServer.Services.Invoices public PaymentMethodId GetId() { #pragma warning disable CS0618 // Type or member is obsolete - return new PaymentMethodId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(PaymentType)); + return new PaymentMethodId(Currency, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(PaymentType)); #pragma warning restore CS0618 // Type or member is obsolete } public void SetId(PaymentMethodId id) { #pragma warning disable CS0618 // Type or member is obsolete - CryptoCode = id.CryptoCode; + Currency = id.CryptoCode; PaymentType = id.PaymentType.ToString(); #pragma warning restore CS0618 // Type or member is obsolete } @@ -1053,7 +1142,6 @@ namespace BTCPayServer.Services.Invoices DepositAddress = bitcoinPaymentMethod.DepositAddress; } PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod)); - #pragma warning restore CS0618 // Type or member is obsolete return this; } @@ -1068,86 +1156,58 @@ namespace BTCPayServer.Services.Invoices [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")] public string DepositAddress { get; set; } - public PaymentMethodAccounting Calculate(Func paymentPredicate = null) + public PaymentMethodAccounting Calculate() { - paymentPredicate = paymentPredicate ?? new Func((p) => true); - var paymentMethods = ParentEntity.GetPaymentMethods(); - - var totalDue = ParentEntity.Price / Rate; - var paid = 0m; - var cryptoPaid = 0.0m; - + var i = ParentEntity; int precision = Network?.Divisibility ?? 8; - - var totalDueNoNetworkCost = Coins(Extensions.RoundUp(totalDue, precision)); - bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision); - int txRequired = 0; - decimal networkFeeAlreadyPaid = 0.0m; - _ = ParentEntity.GetPayments(true) - .Where(p => paymentPredicate(p)) - .OrderBy(p => p.ReceivedTime) - .Select(_ => - { - var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision); - networkFeeAlreadyPaid += txFee; - paid += _.GetValue(paymentMethods, GetId(), null, precision); - if (!paidEnough) - { - totalDue += txFee; - } - - paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision); - if (GetId() == _.GetPaymentMethodId()) - { - cryptoPaid += _.GetCryptoPaymentData().GetValue(); - txRequired++; - } - - return _; - }).ToArray(); - var accounting = new PaymentMethodAccounting(); - accounting.TxCount = txRequired; - if (!paidEnough) + var thisPaymentMethodPayments = i.GetPayments(true).Where(p => GetId() == p.GetPaymentMethodId()).ToList(); + accounting.TxCount = thisPaymentMethodPayments.Count; + accounting.TxRequired = accounting.TxCount; + var grossDue = i.Price + i.PaidFee; + if (i.MinimumNetDue > 0.0m) { - txRequired++; - totalDue += GetTxFee(); + accounting.TxRequired++; + grossDue += Rate * (GetPaymentMethodDetails()?.GetNextNetworkFee() ?? 0m); } + accounting.Divisibility = precision; + accounting.TotalDue = Coins(grossDue / Rate, precision); + accounting.Paid = Coins(i.PaidAmount.Gross / Rate, precision); + accounting.CryptoPaid = Coins(thisPaymentMethodPayments.Sum(p => p.PaidAmount.Gross), precision); - accounting.TotalDue = Coins(Extensions.RoundUp(totalDue, precision)); - accounting.Paid = Coins(Extensions.RoundUp(paid, precision)); - accounting.TxRequired = txRequired; - accounting.CryptoPaid = Coins(Extensions.RoundUp(cryptoPaid, precision)); - accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero); - accounting.DueUncapped = accounting.TotalDue - accounting.Paid; - accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost; - accounting.NetworkFeeAlreadyPaid = Coins(Extensions.RoundUp(networkFeeAlreadyPaid, precision)); - // If the total due is 0, there is no payment tolerance to calculate - var minimumTotalDueSatoshi = accounting.TotalDue.Satoshi == 0 - ? 0 - : Math.Max(1.0m, - accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m))); - accounting.MinimumTotalDue = Money.Satoshis(minimumTotalDueSatoshi); + // This one deal with the fact where it might looks like a slight over payment due to the dust of another payment method. + // So if we detect the NetDue is zero, just cap dueUncapped to 0 + var dueUncapped = i.NetDue == 0.0m ? 0.0m : grossDue - i.PaidAmount.Gross; + accounting.DueUncapped = Coins(dueUncapped / Rate, precision); + accounting.Due = Max(accounting.DueUncapped, 0.0m); + + accounting.NetworkFee = Coins((grossDue - i.Price) / Rate, precision); + accounting.NetworkFeeAlreadyPaid = Coins(i.PaidFee / Rate, precision); + + accounting.MinimumTotalDue = Max(Smallest(precision), Coins((grossDue * (1.0m - ((decimal)i.PaymentTolerance / 100.0m))) / Rate, precision)); return accounting; } - const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m; - private Money Coins(decimal v) + private decimal Smallest(int precision) { - if (v > MaxCoinValue) - v = MaxCoinValue; - // Clamp the value to not crash on degenerate invoices - v *= 1_0000_0000m; - if (v > long.MaxValue) - return Money.Satoshis(long.MaxValue); - if (v < long.MinValue) - return Money.Satoshis(long.MinValue); - return Money.Satoshis(v); + decimal a = 1.0m; + for (int i = 0; i < precision; i++) + { + a /= 10.0m; + } + return a; } - private decimal GetTxFee() + decimal Max(decimal a, decimal b) => a > b ? a : b; + + const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m; + internal static decimal Coins(decimal v, int precision) { - return GetPaymentMethodDetails()?.GetNextNetworkFee() ?? 0m; + v = Extensions.RoundUp(v, precision); + // Clamp the value to not crash on degenerate invoices + if (v > MaxCoinValue) + v = MaxCoinValue; + return v; } } @@ -1209,19 +1269,60 @@ namespace BTCPayServer.Services.Invoices get; set; } - - [Obsolete("Use GetpaymentMethodId().CryptoCode instead")] - public string CryptoCode + string _Currency; + [JsonProperty("cryptoCode")] + public string Currency { - get; - set; + get + { + return _Currency ?? "BTC"; + } + set + { + _Currency = value; + } } [Obsolete("Use GetCryptoPaymentData() instead")] public string CryptoPaymentData { get; set; } [Obsolete("Use GetpaymentMethodId().PaymentType instead")] public string CryptoPaymentDataType { get; set; } + [JsonIgnore] + public decimal Rate { get; set; } + [JsonIgnore] + /// + public string InvoiceCurrency => InvoiceEntity.Currency; + /// The amount paid by this payment in the + /// + [JsonIgnore] + public Amounts PaidAmount { get; set; } + /// + /// The amount paid by this payment in the + /// + [JsonIgnore] + public Amounts InvoicePaidAmount { get; set; } + [JsonIgnore] + public InvoiceEntity InvoiceEntity { get; set; } + public void UpdateAmounts() + { + var pd = GetCryptoPaymentData(); + if (pd is null) + return; + var value = pd.GetValue(); + PaidAmount = new Amounts() + { + Currency = Currency, + Gross = value, + Net = value - NetworkFee + }; + InvoicePaidAmount = new Amounts() + { + Currency = InvoiceCurrency, + Gross = PaidAmount.Gross * Rate, + Net = PaidAmount.Net * Rate + }; + } public CryptoPaymentData GetCryptoPaymentData() { @@ -1280,21 +1381,6 @@ namespace BTCPayServer.Services.Invoices #pragma warning restore CS0618 return this; } - internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value, int precision) - { - - value = value ?? this.GetCryptoPaymentData().GetValue(); - var to = paymentMethodId; - var from = this.GetPaymentMethodId(); - if (to == from) - return decimal.Round(value.Value, precision); - var fromRate = paymentMethods[from].Rate; - var toRate = paymentMethods[to].Rate; - - var fiatValue = fromRate * decimal.Round(value.Value, precision); - var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate; - return otherCurrencyValue; - } public PaymentMethodId GetPaymentMethodId() { @@ -1308,16 +1394,9 @@ namespace BTCPayServer.Services.Invoices { return null; } - return new PaymentMethodId(CryptoCode ?? "BTC", paymentType); + return new PaymentMethodId(Currency ?? "BTC", paymentType); #pragma warning restore CS0618 // Type or member is obsolete } - - public string GetCryptoCode() - { -#pragma warning disable CS0618 - return CryptoCode ?? "BTC"; -#pragma warning restore CS0618 - } } /// /// A record of a payment diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index c494315cb..69724fadd 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -12,7 +12,6 @@ using BTCPayServer.Logging; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Payments; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using NBitcoin; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -30,16 +29,13 @@ namespace BTCPayServer.Services.Invoices NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings); } - public Logs Logs { get; } - private readonly ApplicationDbContextFactory _applicationDbContextFactory; private readonly EventAggregator _eventAggregator; private readonly BTCPayNetworkProvider _btcPayNetworkProvider; public InvoiceRepository(ApplicationDbContextFactory contextFactory, - BTCPayNetworkProvider networks, EventAggregator eventAggregator, Logs logs) + BTCPayNetworkProvider networks, EventAggregator eventAggregator) { - Logs = logs; _applicationDbContextFactory = contextFactory; _btcPayNetworkProvider = networks; _eventAggregator = eventAggregator; @@ -54,14 +50,20 @@ namespace BTCPayServer.Services.Invoices .FirstOrDefaultAsync(); } - public InvoiceEntity CreateNewInvoice() + public InvoiceEntity CreateNewInvoice(string storeId) { return new InvoiceEntity() { + Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)), + StoreId = storeId, Networks = _btcPayNetworkProvider, Version = InvoiceEntity.Lastest_Version, - InvoiceTime = DateTimeOffset.UtcNow, - Metadata = new InvoiceMetadata() + // Truncating was an unintended side effect of previous code. Might want to remove that one day + InvoiceTime = DateTimeOffset.UtcNow.TruncateMilliSeconds(), + Metadata = new InvoiceMetadata(), +#pragma warning disable CS0618 + Payments = new List() +#pragma warning restore CS0618 }; } @@ -173,21 +175,14 @@ namespace BTCPayServer.Services.Invoices await ctx.SaveChangesAsync(); } - public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice, string[] additionalSearchTerms = null) + public async Task CreateInvoiceAsync(InvoiceEntity invoice, string[] additionalSearchTerms = null) { var textSearch = new HashSet(); - invoice = Clone(invoice); - invoice.Networks = _btcPayNetworkProvider; - invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); -#pragma warning disable CS0618 - invoice.Payments = new List(); -#pragma warning restore CS0618 - invoice.StoreId = storeId; using (var context = _applicationDbContextFactory.CreateContext()) { var invoiceData = new Data.InvoiceData() { - StoreDataId = storeId, + StoreDataId = invoice.StoreId, Id = invoice.Id, Created = invoice.InvoiceTime, OrderId = invoice.Metadata.OrderId, @@ -245,16 +240,6 @@ namespace BTCPayServer.Services.Invoices await context.SaveChangesAsync().ConfigureAwait(false); } - - - return invoice; - } - - private InvoiceEntity Clone(InvoiceEntity invoice) - { - var temp = new InvoiceData(); - temp.SetBlob(invoice); - return temp.GetBlob(_btcPayNetworkProvider); } public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs) @@ -617,6 +602,7 @@ namespace BTCPayServer.Services.Invoices entity.Metadata.BuyerEmail = entity.RefundMail; } entity.Archived = invoice.Archived; + entity.UpdateTotals(); return entity; } @@ -828,9 +814,8 @@ namespace BTCPayServer.Services.Invoices { var paymentMethodContribution = new InvoiceStatistics.Contribution(); paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId(); - paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee; - var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMethodId).Rate; - paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value; + paymentMethodContribution.CurrencyValue = pay.InvoicePaidAmount.Net; + paymentMethodContribution.Value = pay.PaidAmount.Net; return paymentMethodContribution; }) .ToArray(); diff --git a/BTCPayServer/Services/Invoices/PaymentService.cs b/BTCPayServer/Services/Invoices/PaymentService.cs index 440367f2d..39416aa47 100644 --- a/BTCPayServer/Services/Invoices/PaymentService.cs +++ b/BTCPayServer/Services/Invoices/PaymentService.cs @@ -51,7 +51,7 @@ namespace BTCPayServer.Services.Invoices { Version = 1, #pragma warning disable CS0618 - CryptoCode = network.CryptoCode, + Currency = network.CryptoCode, #pragma warning restore CS0618 ReceivedTime = date.UtcDateTime, Accounted = accounted,