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
This commit is contained in:
Nicolas Dorier 2023-07-19 18:47:32 +09:00 committed by GitHub
parent a7def63137
commit 22435a2bf5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 487 additions and 424 deletions

View file

@ -1,4 +1,5 @@
#if ALTCOINS #if ALTCOINS
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using BTCPayServer.Common; using BTCPayServer.Common;
@ -34,12 +35,12 @@ namespace BTCPayServer
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)); 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 0: 10 = 0.00000010
//precision 2: 10 = 0.00001000 //precision 2: 10 = 0.00001000
//precision 8: 10 = 10 //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); var builder = base.GenerateBIP21(cryptoInfoAddress, money);
builder.QueryParams.Add("assetid", AssetId.ToString()); builder.QueryParams.Add("assetid", AssetId.ToString());
return builder; return builder;

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using BTCPayServer.Common; 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); var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
builder.Host = cryptoInfoAddress; 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; return builder;
} }

View file

@ -346,165 +346,213 @@ namespace BTCPayServer.Tests
Assert.True(Torrc.TryParse(input, out torrc)); Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString()); 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<PaymentEntity>();
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 #if ALTCOINS
[Fact] [Fact]
public void CanCalculateCryptoDue() public void CanCalculateCryptoDue()
{ {
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest); var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] var entity = new InvoiceEntity() { Currency = "USD" };
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider; entity.Networks = networkProvider;
#pragma warning disable CS0618 #pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>(); entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() entity.SetPaymentMethod(new PaymentMethod()
{ {
CryptoCode = "BTC", Currency = "BTC",
Rate = 5000, Rate = 5000,
NextNetworkFee = Money.Coins(0.1m) NextNetworkFee = Money.Coins(0.1m)
}); });
entity.Price = 5000; entity.Price = 5000;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); Assert.Equal(1.1m, accounting.Due);
Assert.Equal(1.1m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC",
Output = new TxOut(Money.Coins(0.5m), new Key()), Output = new TxOut(Money.Coins(0.5m), new Key()),
Rate = 5000,
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1 //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(0.7m, accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue); Assert.Equal(1.2m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC",
Output = new TxOut(Money.Coins(0.2m), new Key()), Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due); Assert.Equal(0.6m, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); Assert.Equal(1.3m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC",
Output = new TxOut(Money.Coins(0.6m), new Key()), Output = new TxOut(Money.Coins(0.6m), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); Assert.Equal(1.3m, accounting.TotalDue);
entity.Payments.Add( 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(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue); Assert.Equal(1.3m, accounting.TotalDue);
entity = new InvoiceEntity(); entity = new InvoiceEntity();
entity.Networks = networkProvider; entity.Networks = networkProvider;
entity.Price = 5000; entity.Price = 5000;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add( 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( 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.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>(); entity.Payments = new List<PaymentEntity>();
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); 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)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue); Assert.Equal(10.01m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
CryptoCode = "BTC", Currency = "BTC",
Output = new TxOut(Money.Coins(1.0m), new Key()), Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m), accounting.Due); Assert.Equal(4.2m, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.0m), accounting.Paid); Assert.Equal(1.0m, accounting.Paid);
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue); Assert.Equal(5.2m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due); Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid); Assert.Equal(0.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(2.0m), accounting.Paid); Assert.Equal(2.0m, accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue); Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
CryptoCode = "LTC", Currency = "LTC",
Output = new TxOut(Money.Coins(1.0m), new Key()), Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.01m NetworkFee = 0.01m
}); });
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due); Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m), accounting.Paid); Assert.Equal(1.5m, accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due); Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m), accounting.Paid); Assert.Equal(3.0m, accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue); Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); 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() entity.Payments.Add(new PaymentEntity()
{ {
CryptoCode = "BTC", Currency = "BTC",
Output = new TxOut(remaining, new Key()), Output = new TxOut(Money.Coins(remaining), new Key()),
Accounted = true, Accounted = true,
NetworkFee = 0.1m NetworkFee = 0.1m
}); });
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid); Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid); Assert.Equal(1.5m + remaining, accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid); Assert.Equal(3.0m + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully 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); accounting.TotalDue);
Assert.Equal(1, accounting.TxRequired); Assert.Equal(1, accounting.TxRequired);
Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(accounting.Paid, accounting.TotalDue);
@ -548,27 +596,29 @@ namespace BTCPayServer.Tests
entity.Payments = new List<PaymentEntity>(); entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() entity.SetPaymentMethod(new PaymentMethod()
{ {
CryptoCode = "BTC", Currency = "BTC",
Rate = 5000, Rate = 5000,
NextNetworkFee = Money.Coins(0.1m) NextNetworkFee = Money.Coins(0.1m)
}); });
entity.Price = 5000; entity.Price = 5000;
entity.PaymentTolerance = 0; entity.PaymentTolerance = 0;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(1.1m, accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); Assert.Equal(1.1m, accounting.TotalDue);
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue); Assert.Equal(1.1m, accounting.MinimumTotalDue);
entity.PaymentTolerance = 10; entity.PaymentTolerance = 10;
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue); Assert.Equal(0.99m, accounting.MinimumTotalDue);
entity.PaymentTolerance = 100; entity.PaymentTolerance = 100;
entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue); Assert.Equal(0.0000_0001m, accounting.MinimumTotalDue);
} }
[Fact] [Fact]
@ -1884,11 +1934,6 @@ namespace BTCPayServer.Tests
#pragma warning disable CS0618 #pragma warning disable CS0618
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString(); var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest); 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 networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC"); var networkLTC = networkProvider.GetNetwork("LTC");
InvoiceEntity invoiceEntity = new InvoiceEntity(); InvoiceEntity invoiceEntity = new InvoiceEntity();
@ -1896,14 +1941,14 @@ namespace BTCPayServer.Tests
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>(); invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.Price = 100; invoiceEntity.Price = 100;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); 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( .SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{ {
NextNetworkFee = Money.Coins(0.00000100m), NextNetworkFee = Money.Coins(0.00000100m),
DepositAddress = dummy 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( .SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{ {
@ -1919,7 +1964,7 @@ namespace BTCPayServer.Tests
new PaymentEntity() new PaymentEntity()
{ {
Accounted = true, Accounted = true,
CryptoCode = "BTC", Currency = "BTC",
NetworkFee = 0.00000100m, NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC"), Network = networkProvider.GetNetwork("BTC"),
} }
@ -1928,34 +1973,33 @@ namespace BTCPayServer.Tests
Network = networkProvider.GetNetwork("BTC"), Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(0.00151263m) } Output = new TxOut() { Value = Money.Coins(0.00151263m) }
})); }));
invoiceEntity.UpdateTotals();
accounting = btc.Calculate(); accounting = btc.Calculate();
invoiceEntity.Payments.Add( invoiceEntity.Payments.Add(
new PaymentEntity() new PaymentEntity()
{ {
Accounted = true, Accounted = true,
CryptoCode = "BTC", Currency = "BTC",
NetworkFee = 0.00000100m, NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC") Network = networkProvider.GetNetwork("BTC")
} }
.SetCryptoPaymentData(new BitcoinLikePaymentData() .SetCryptoPaymentData(new BitcoinLikePaymentData()
{ {
Network = networkProvider.GetNetwork("BTC"), Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = accounting.Due } Output = new TxOut() { Value = Money.Coins(accounting.Due) }
})); }));
invoiceEntity.UpdateTotals();
accounting = btc.Calculate(); accounting = btc.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(Money.Zero, accounting.DueUncapped); Assert.Equal(0.0m, accounting.DueUncapped);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = ltc.Calculate(); accounting = ltc.Calculate();
Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(0.0m, accounting.Due);
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up) // LTC might should be over paid due to BTC paying above what it should (round 1 satoshi up), but we handle this case
Assert.True(accounting.DueUncapped < Money.Zero); // and set DueUncapped to zero.
Assert.Equal(0.0m, accounting.DueUncapped);
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2);
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
#pragma warning restore CS0618
} }
[Fact] [Fact]

View file

@ -1761,7 +1761,7 @@ namespace BTCPayServer.Tests
var parsedJson = await GetExport(user); var parsedJson = await GetExport(user);
Assert.Equal(3, parsedJson.Length); 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(); var pay1str = parsedJson[0].ToString();
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str); Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue")); Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));

View file

@ -396,7 +396,7 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
var accounting = invoicePaymentMethod.Calculate(); var accounting = invoicePaymentMethod.Calculate();
var cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC); var cryptoPaid = accounting.Paid;
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true); var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility); var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate( var rateResult = await _rateProvider.FetchRate(
@ -464,7 +464,7 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
} }
var dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC); var dueAmount = accounting.TotalDue;
createPullPayment.Currency = cryptoCode; createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility); createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
createPullPayment.AutoApproveClaims = true; createPullPayment.AutoApproveClaims = true;
@ -580,11 +580,11 @@ namespace BTCPayServer.Controllers.Greenfield
CryptoCode = method.GetId().CryptoCode, CryptoCode = method.GetId().CryptoCode,
Destination = details.GetPaymentDestination(), Destination = details.GetPaymentDestination(),
Rate = method.Rate, Rate = method.Rate,
Due = accounting.DueUncapped.ToDecimal(MoneyUnit.BTC), Due = accounting.DueUncapped,
TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC), TotalPaid = accounting.Paid,
PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), PaymentMethodPaid = accounting.CryptoPaid,
Amount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC), Amount = accounting.TotalDue,
NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC), NetworkFee = accounting.NetworkFee,
PaymentLink = PaymentLink =
method.GetId().PaymentType.GetPaymentLink(method.Network, entity, details, accounting.Due, method.GetId().PaymentType.GetPaymentLink(method.Network, entity, details, accounting.Due,
Request.GetAbsoluteRoot()), Request.GetAbsoluteRoot()),

View file

@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers
return Ok(new return Ok(new
{ {
Txid = txid, Txid = txid,
AmountRemaining = (paymentMethod.Calculate().Due - amount).ToUnit(MoneyUnit.BTC), AmountRemaining = paymentMethod.Calculate().Due - amount.ToDecimal(MoneyUnit.BTC),
SuccessMessage = $"Created transaction {txid}" SuccessMessage = $"Created transaction {txid}"
}); });
@ -70,11 +70,11 @@ namespace BTCPayServer.Controllers
{ {
var bolt11 = BOLT11PaymentRequest.Parse(destination, network); var bolt11 = BOLT11PaymentRequest.Parse(destination, network);
var paymentHash = bolt11.PaymentHash?.ToString(); 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 return Ok(new
{ {
Txid = paymentHash, Txid = paymentHash,
AmountRemaining = (paymentMethod.Calculate().TotalDue - paid).ToUnit(MoneyUnit.BTC), AmountRemaining = paymentMethod.Calculate().TotalDue - paid,
SuccessMessage = $"Sent payment {paymentHash}" SuccessMessage = $"Sent payment {paymentHash}"
}); });
} }

View file

@ -228,18 +228,14 @@ namespace BTCPayServer.Controllers
string txId = paymentData.GetPaymentId(); string txId = paymentData.GetPaymentId();
string? link = GetTransactionLink(paymentMethodId, txId); 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 return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
{ {
Amount = amount, Amount = paymentEntity.PaidAmount.Gross,
Paid = paid, Paid = paymentEntity.PaidAmount.Net,
ReceivedDate = paymentEntity.ReceivedTime.DateTime, ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol), PaidFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol), RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaymentMethod = paymentMethodId.ToPrettyString(), PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link, Link = link,
Id = txId, Id = txId,
@ -364,8 +360,8 @@ namespace BTCPayServer.Controllers
if (paymentMethod != null) if (paymentMethod != null)
{ {
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC); cryptoPaid = accounting.Paid;
dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC); dueAmount = accounting.TotalDue;
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility); paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
} }
@ -560,7 +556,7 @@ namespace BTCPayServer.Controllers
{ {
var accounting = data.Calculate(); var accounting = data.Calculate();
var paymentMethodId = data.GetId(); var paymentMethodId = data.GetId();
var overpaidAmount = accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC); var overpaidAmount = accounting.OverpaidHelper;
if (overpaidAmount > 0) if (overpaidAmount > 0)
{ {
@ -571,8 +567,8 @@ namespace BTCPayServer.Controllers
{ {
PaymentMethodId = paymentMethodId, PaymentMethodId = paymentMethodId,
PaymentMethod = paymentMethodId.ToPrettyString(), PaymentMethod = paymentMethodId.ToPrettyString(),
Due = _displayFormatter.Currency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode), Due = _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode),
Paid = _displayFormatter.Currency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode), Paid = _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode),
Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode), Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode),
Address = data.GetPaymentMethodDetails().GetPaymentDestination(), Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
Rate = ExchangeRate(data.GetId().CryptoCode, data), Rate = ExchangeRate(data.GetId().CryptoCode, data),
@ -827,7 +823,6 @@ namespace BTCPayServer.Controllers
var dto = invoice.EntityToDTO(); var dto = invoice.EntityToDTO();
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId]; var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
switch (lang?.ToLowerInvariant()) switch (lang?.ToLowerInvariant())
{ {
@ -885,10 +880,10 @@ namespace BTCPayServer.Controllers
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback, OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)), CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(), BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ShowMoney(divisibility), BtcDue = accounting.ShowMoney(accounting.Due),
BtcPaid = accounting.Paid.ShowMoney(divisibility), BtcPaid = accounting.ShowMoney(accounting.Paid),
InvoiceCurrency = invoice.Currency, InvoiceCurrency = invoice.Currency,
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility), OrderAmount = accounting.ShowMoney(accounting.TotalDue - accounting.NetworkFee),
IsUnsetTopUp = invoice.IsUnsetTopUp(), IsUnsetTopUp = invoice.IsUnsetTopUp(),
CustomerEmail = invoice.RefundMail, CustomerEmail = invoice.RefundMail,
RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail, RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail,

View file

@ -121,7 +121,7 @@ namespace BTCPayServer.Controllers
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null) internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
{ {
var storeBlob = store.GetStoreBlob(); 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.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration;
entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration; entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration;
if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime) if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime)
@ -237,7 +237,7 @@ namespace BTCPayServer.Controllers
public async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null) public async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
{ {
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var entity = _InvoiceRepository.CreateNewInvoice(); var entity = _InvoiceRepository.CreateNewInvoice(store.Id);
entity.ServerUrl = serverUrl; entity.ServerUrl = serverUrl;
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration); entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration); entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
@ -314,6 +314,7 @@ namespace BTCPayServer.Controllers
entity.RefundMail = entity.Metadata.BuyerEmail; entity.RefundMail = entity.Metadata.BuyerEmail;
} }
entity.Status = InvoiceStatusLegacy.New; entity.Status = InvoiceStatusLegacy.New;
entity.UpdateTotals();
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>(); HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider); var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any() 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")) using (logs.Measure("Saving invoice"))
{ {
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, additionalSearchTerms); await _InvoiceRepository.CreateInvoiceAsync(entity, additionalSearchTerms);
foreach (var method in paymentMethods) foreach (var method in paymentMethods)
{ {
if (method.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod bp) if (method.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod bp)
@ -506,7 +507,7 @@ namespace BTCPayServer.Controllers
await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)]; await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)];
if (currentRateToCrypto?.BidAsk != null) 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; var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid;
if (amount < limitValueCrypto && criteria.Above) if (amount < limitValueCrypto && criteria.Above)

View file

@ -548,7 +548,7 @@ namespace BTCPayServer
lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value })); lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
if (i.Type != InvoiceType.TopUp) 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) if (!allowOverpay)
lnurlRequest.MaxSendable = lnurlRequest.MinSendable; lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
} }

View file

@ -303,7 +303,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
bip21.Add(newUri.Uri.ToString()); bip21.Add(newUri.Uri.ToString());
break; break;
case AddressClaimDestination addressClaimDestination: 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); bip21New.QueryParams.Add("payout", payout.Id);
bip21.Add(bip21New.ToString()); bip21.Add(bip21New.ToString());
break; break;

View file

@ -30,6 +30,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using NBitcoin.Payment; using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo; using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
@ -38,6 +39,15 @@ namespace BTCPayServer
{ {
public static class Extensions 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<BufferizedFormFile> Bufferize(this IFormFile formFile) public static Task<BufferizedFormFile> Bufferize(this IFormFile formFile)
{ {
return BufferizedFormFile.Bufferize(formFile); return BufferizedFormFile.Bufferize(formFile);
@ -382,20 +392,6 @@ namespace BTCPayServer
return controller.View("PostRedirect", redirectVm); return controller.View("PostRedirect", redirectVm);
} }
public static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
{
var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
var relationalCommandCache = enumerator.Private("_relationalCommandCache");
var selectExpression = relationalCommandCache.Private<Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression>("_selectExpression");
var factory = relationalCommandCache.Private<Microsoft.EntityFrameworkCore.Query.IQuerySqlGeneratorFactory>("_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) public static BTCPayNetworkProvider ConfigureNetworkProvider(this IConfiguration configuration, Logs logs)
{ {
var _networkType = DefaultConfiguration.GetNetworkType(configuration); var _networkType = DefaultConfiguration.GetNetworkType(configuration);

View file

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs; using BTCPayServer.Services.Notifications.Blobs;
@ -83,31 +84,13 @@ namespace BTCPayServer.HostedServices
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial) { PaidPartial = paidPartial }); context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial) { PaidPartial = paidPartial });
} }
var allPaymentMethods = invoice.GetPaymentMethods();
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting); var hasPayment = invoice.GetPayments(true).Any();
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
};
}
if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired) if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired)
{ {
var isPaid = invoice.IsUnsetTopUp() ? var isPaid = invoice.IsUnsetTopUp() ?
accounting.Paid > Money.Zero : hasPayment :
accounting.Paid >= accounting.MinimumTotalDue; !invoice.IsUnderPaid;
if (isPaid) if (isPaid)
{ {
if (invoice.Status == InvoiceStatusLegacy.New) if (invoice.Status == InvoiceStatusLegacy.New)
@ -117,13 +100,15 @@ namespace BTCPayServer.HostedServices
if (invoice.IsUnsetTopUp()) if (invoice.IsUnsetTopUp())
{ {
invoice.ExceptionStatus = InvoiceExceptionStatus.None; invoice.ExceptionStatus = InvoiceExceptionStatus.None;
invoice.Price = (accounting.Paid - accounting.NetworkFeeAlreadyPaid).ToDecimal(MoneyUnit.BTC) * paymentMethod.Rate; // We know there is at least one payment because hasPayment is true
accounting = paymentMethod.Calculate(); var payment = invoice.GetPayments(true).First();
invoice.Price = payment.InvoicePaidAmount.Net;
invoice.UpdateTotals();
context.BlobUpdated(); context.BlobUpdated();
} }
else else
{ {
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None; invoice.ExceptionStatus = invoice.IsOverPaid ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
} }
context.MarkDirty(); 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; invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
context.MarkDirty(); context.MarkDirty();
@ -145,43 +130,43 @@ namespace BTCPayServer.HostedServices
// Just make sure RBF did not cancelled a payment // Just make sure RBF did not cancelled a payment
if (invoice.Status == InvoiceStatusLegacy.Paid) 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; invoice.ExceptionStatus = InvoiceExceptionStatus.None;
context.MarkDirty(); context.MarkDirty();
} }
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver) if (invoice.IsOverPaid && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
{ {
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver; invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver;
context.MarkDirty(); context.MarkDirty();
} }
if (accounting.Paid < accounting.MinimumTotalDue) if (invoice.IsUnderPaid)
{ {
invoice.Status = InvoiceStatusLegacy.New; invoice.Status = InvoiceStatusLegacy.New;
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial; invoice.ExceptionStatus = hasPayment ? InvoiceExceptionStatus.PaidPartial : InvoiceExceptionStatus.None;
context.MarkDirty(); context.MarkDirty();
} }
} }
if (invoice.Status == InvoiceStatusLegacy.Paid) if (invoice.Status == InvoiceStatusLegacy.Paid)
{ {
var confirmedAccounting = var unconfPayments = invoice.GetPayments(true).Where(p => !p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)).ToList();
paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)) ?? var unconfirmedPaid = unconfPayments.Select(p => p.InvoicePaidAmount.Net).Sum();
accounting; var minimumDue = invoice.MinimumNetDue + unconfirmedPaid;
if (// Is after the monitoring deadline if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow) (invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&& &&
// And not enough amount confirmed // And not enough amount confirmed
(confirmedAccounting.Paid < accounting.MinimumTotalDue)) (minimumDue > 0.0m))
{ {
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm)); context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm));
invoice.Status = InvoiceStatusLegacy.Invalid; invoice.Status = InvoiceStatusLegacy.Invalid;
context.MarkDirty(); context.MarkDirty();
} }
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue) else if (minimumDue <= 0.0m)
{ {
invoice.Status = InvoiceStatusLegacy.Confirmed; invoice.Status = InvoiceStatusLegacy.Confirmed;
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed)); context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed));
@ -191,9 +176,11 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == InvoiceStatusLegacy.Confirmed) if (invoice.Status == InvoiceStatusLegacy.Confirmed)
{ {
var completedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)) ?? var unconfPayments = invoice.GetPayments(true).Where(p => !p.GetCryptoPaymentData().PaymentCompleted(p)).ToList();
accounting; var unconfirmedPaid = unconfPayments.Select(p => p.InvoicePaidAmount.Net).Sum();
if (completedAccounting.Paid >= accounting.MinimumTotalDue) var minimumDue = invoice.MinimumNetDue + unconfirmedPaid;
if (minimumDue <= 0.0m)
{ {
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed)); context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed));
invoice.Status = InvoiceStatusLegacy.Complete; 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) private void Watch(string invoiceId)
{ {
ArgumentNullException.ThrowIfNull(invoiceId); ArgumentNullException.ThrowIfNull(invoiceId);
@ -380,7 +348,7 @@ namespace BTCPayServer.HostedServices
if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted) if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) && (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 transactionResult = client is null ? null : await client.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
var confirmationCount = transactionResult?.Confirmations ?? 0; var confirmationCount = transactionResult?.Confirmations ?? 0;
onChainPaymentData.ConfirmationCount = confirmationCount; onChainPaymentData.ConfirmationCount = confirmationCount;

View file

@ -103,7 +103,7 @@ namespace BTCPayServer.HostedServices
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance && invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData: 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 transactionId = bitcoinLikePaymentData.Outpoint.Hash;
var labels = new List<Attachment> var labels = new List<Attachment>
{ {

View file

@ -167,7 +167,7 @@ namespace BTCPayServer.PaymentRequest
new object[] new object[]
{ {
data.GetValue(), data.GetValue(),
invoiceEvent.Payment.GetCryptoCode(), invoiceEvent.Payment.Currency,
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString() invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
}, cancellationToken); }, cancellationToken);
} }

View file

@ -123,18 +123,14 @@ namespace BTCPayServer.PaymentRequest
string txId = paymentData.GetPaymentId(); string txId = paymentData.GetPaymentId();
string link = GetTransactionLink(paymentMethodId, txId); 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 return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
{ {
Amount = amount, Amount = paymentEntity.PaidAmount.Gross,
Paid = paid, Paid = paymentEntity.InvoicePaidAmount.Net,
ReceivedDate = paymentEntity.ReceivedTime.DateTime, ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _displayFormatter.Currency(paid, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol), RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaymentMethod = paymentMethodId.ToPrettyString(), PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link, Link = link,
Id = txId, Id = txId,

View file

@ -211,7 +211,7 @@ namespace BTCPayServer.Payments.Bitcoin
new Key().GetScriptPubKey(supportedPaymentMethod.AccountDerivation.ScriptPubKeyType()); new Key().GetScriptPubKey(supportedPaymentMethod.AccountDerivation.ScriptPubKeyType());
var dust = txOut.GetDustThreshold(); var dust = txOut.GetDustThreshold();
var amount = paymentMethod.Calculate().Due; 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"); 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) if (preparePaymentObject is null)

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
@ -28,7 +29,7 @@ namespace BTCPayServer.Payments
} }
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri) decimal cryptoInfoDue, string serverUri)
{ {
if (!paymentMethodDetails.Activated) if (!paymentMethodDetails.Activated)
{ {
@ -74,7 +75,7 @@ namespace BTCPayServer.Payments
{ {
AdditionalData = new Dictionary<string, JToken>() AdditionalData = new Dictionary<string, JToken>()
{ {
{"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due, {"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.GetDue().Value,
serverUrl))} serverUrl))}
} }
}; };

View file

@ -73,7 +73,7 @@ namespace BTCPayServer.Payments.Lightning
decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility); decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility);
try try
{ {
due = paymentMethod.Calculate().Due.ToDecimal(MoneyUnit.BTC); due = paymentMethod.Calculate().Due;
} }
catch (Exception) catch (Exception)
{ {

View file

@ -200,7 +200,7 @@ namespace BTCPayServer.Payments.Lightning
if (inv.Name == InvoiceEvent.ReceivedPayment && inv.Invoice.Status == InvoiceStatusLegacy.New && inv.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) if (inv.Name == InvoiceEvent.ReceivedPayment && inv.Invoice.Status == InvoiceStatusLegacy.New && inv.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
{ {
var pm = inv.Invoice.GetPaymentMethods().First(); 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); await CreateNewLNInvoiceForBTCPayInvoice(inv.Invoice);
} }

View file

@ -302,7 +302,7 @@ namespace BTCPayServer.Payments.PayJoin
var paymentDetails = paymentMethod?.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; var paymentDetails = paymentMethod?.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (paymentMethod is null || paymentDetails is null || !paymentDetails.PayjoinEnabled) if (paymentMethod is null || paymentDetails is null || !paymentDetails.PayjoinEnabled)
continue; continue;
due = paymentMethod.Calculate().TotalDue - output.Value; due = Money.Coins(paymentMethod.Calculate().TotalDue) - output.Value;
if (due > Money.Zero) if (due > Money.Zero)
{ {
break; break;

View file

@ -67,7 +67,7 @@ namespace BTCPayServer.Payments
} }
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri) decimal cryptoInfoDue, string serverUri)
{ {
if (!paymentMethodDetails.Activated) if (!paymentMethodDetails.Activated)
{ {
@ -105,7 +105,7 @@ namespace BTCPayServer.Payments
{ {
cryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls() 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),
}; };
} }
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Controllers.Greenfield; using BTCPayServer.Controllers.Greenfield;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
@ -52,7 +53,7 @@ namespace BTCPayServer.Payments
} }
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri) decimal cryptoInfoDue, string serverUri)
{ {
if (!paymentMethodDetails.Activated) if (!paymentMethodDetails.Activated)
{ {
@ -92,7 +93,7 @@ namespace BTCPayServer.Payments
{ {
invoiceCryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls() 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) serverUrl)
}; };
} }

View file

@ -85,7 +85,7 @@ namespace BTCPayServer.Payments
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value); public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId); public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
public abstract string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, 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 string InvoiceViewPaymentPartialName { get; }
public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore); public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore);

View file

@ -89,15 +89,7 @@ namespace BTCPayServer.Plugins.Crowdfund
.GroupBy(entity => entity.Metadata.ItemCode) .GroupBy(entity => entity.Metadata.ItemCode)
.Select(entities => .Select(entities =>
{ {
var total = entities var total = entities.Sum(entity => entity.PaidAmount.Net);
.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 itemCode = entities.Key; var itemCode = entities.Key;
var perk = perks.FirstOrDefault(p => p.Id == itemCode); var perk = perks.FirstOrDefault(p => p.Id == itemCode);
return new ItemStats return new ItemStats
@ -167,13 +159,7 @@ namespace BTCPayServer.Plugins.Crowdfund
!string.IsNullOrEmpty(entity.Metadata.ItemCode)) !string.IsNullOrEmpty(entity.Metadata.ItemCode))
.GroupBy(entity => entity.Metadata.ItemCode) .GroupBy(entity => entity.Metadata.ItemCode)
.ToDictionary(entities => entities.Key, entities => .ToDictionary(entities => entities.Key, entities =>
entities.Sum(entity => entity.GetPayments(true).Sum(pay => entities.Sum(entity => entity.PaidAmount.Net));
{
var paymentMethodId = pay.GetPaymentMethodId();
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
return rate * value;
})));
} }
var perks = AppService.Parse( settings.PerksTemplate, false); var perks = AppService.Parse( settings.PerksTemplate, false);

View file

@ -119,7 +119,7 @@ namespace BTCPayServer.Plugins.NFC
} }
else else
{ {
due = new LightMoney(lnPaymentMethod.Calculate().Due); due = LightMoney.Coins(lnPaymentMethod.Calculate().Due);
} }
if (info.MinWithdrawable > due || due > info.MaxWithdrawable) if (info.MinWithdrawable > due || due > info.MaxWithdrawable)
@ -135,10 +135,10 @@ namespace BTCPayServer.Plugins.NFC
if (lnurlPaymentMethod is not null) if (lnurlPaymentMethod is not null)
{ {
Money due; decimal due;
if (invoice.Type == InvoiceType.TopUp && request.Amount is not null) 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) else if (invoice.Type == InvoiceType.TopUp)
{ {
@ -152,7 +152,7 @@ namespace BTCPayServer.Plugins.NFC
try try
{ {
httpClient = CreateHttpClient(info.Callback); httpClient = CreateHttpClient(info.Callback);
var amount = LightMoney.Satoshis(due.Satoshi); var amount = LightMoney.Coins(due);
var actionPath = Url.Action(nameof(UILNURLController.GetLNURLForInvoice), "UILNURL", var actionPath = Url.Action(nameof(UILNURLController.GetLNURLForInvoice), "UILNURL",
new { invoiceId = request.InvoiceId, cryptoCode = "BTC", amount = amount.MilliSatoshi }); new { invoiceId = request.InvoiceId, cryptoCode = "BTC", amount = amount.MilliSatoshi });
var url = Request.GetAbsoluteUri(actionPath); var url = Request.GetAbsoluteUri(actionPath);

View file

@ -14,6 +14,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
[assembly: InternalsVisibleTo("BTCPayServer.Tests")] [assembly: InternalsVisibleTo("BTCPayServer.Tests")]
namespace BTCPayServer namespace BTCPayServer
{ {
class Program class Program

View file

@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
{ {
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network, null, model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network, null,
new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due, new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.GetDue().Value,
null); null);
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
} }

View file

@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId); 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 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; : string.Empty;
} }

View file

@ -119,16 +119,16 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment) private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
{ {
_logger.LogInformation( _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 paymentData = (MoneroLikePaymentData)payment.GetCryptoPaymentData();
var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance); var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance);
if (paymentMethod != null && if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero && paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero &&
monero.Activated && monero.Activated &&
monero.GetPaymentDestination() == paymentData.GetDestination() && 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<CreateAddressRequest, CreateAddressResponse>( var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
"create_address", "create_address",

View file

@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
{ {
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
model.InvoiceBitcoinUrl = ZcashPaymentType.Instance.GetPaymentLink(network, null, model.InvoiceBitcoinUrl = ZcashPaymentType.Instance.GetPaymentLink(network, null,
new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due, new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.GetDue().Value,
null); null);
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
} }

View file

@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId); 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 return paymentMethodDetails.Activated
? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}" ? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue}"
: string.Empty; : string.Empty;
} }

View file

@ -114,16 +114,16 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment) private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
{ {
_logger.LogInformation( _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 paymentData = (ZcashLikePaymentData)payment.GetCryptoPaymentData();
var paymentMethod = invoice.GetPaymentMethod(payment.Network, ZcashPaymentType.Instance); var paymentMethod = invoice.GetPaymentMethod(payment.Network, ZcashPaymentType.Instance);
if (paymentMethod != null && if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is ZcashLikeOnChainPaymentMethodDetails Zcash && paymentMethod.GetPaymentMethodDetails() is ZcashLikeOnChainPaymentMethodDetails Zcash &&
Zcash.Activated && Zcash.Activated &&
Zcash.GetPaymentDestination() == paymentData.GetDestination() && 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<CreateAddressRequest, CreateAddressResponse>( var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
"create_address", "create_address",

View file

@ -40,7 +40,7 @@ namespace BTCPayServer.Services.Apps
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.PaymentReceived, new object[] await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.PaymentReceived, new object[]
{ {
data.GetValue(), data.GetValue(),
invoiceEvent.Payment.GetCryptoCode(), invoiceEvent.Payment.Currency,
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString() invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
}, cancellationToken); }, cancellationToken);
} }

View file

@ -183,18 +183,11 @@ namespace BTCPayServer.Services.Apps
} }
} }
else 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 res.Add(new InvoiceStatsItem
{ {
ItemCode = e.Metadata.ItemCode, ItemCode = e.Metadata.ItemCode,
FiatPrice = fiatPrice, FiatPrice = e.PaidAmount.Net,
Date = e.InvoiceTime.Date Date = e.InvoiceTime.Date
}); });
} }

View file

@ -0,0 +1,19 @@
namespace BTCPayServer.Services.Invoices
{
public class Amounts
{
public string Currency { get; set; }
/// <summary>
/// An amount with fee included
/// </summary>
public decimal Gross { get; set; }
/// <summary>
/// An amount without fee included
/// </summary>
public decimal Net { get; set; }
public override string ToString()
{
return $"{Currency}: Net={Net}, Gross={Gross}";
}
}
}

View file

@ -64,23 +64,19 @@ namespace BTCPayServer.Services.Invoices.Export
{ {
foreach (var payment in payments) foreach (var payment in payments)
{ {
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
var pdata = payment.GetCryptoPaymentData(); var pdata = payment.GetCryptoPaymentData();
invoiceDue -= payment.InvoicePaidAmount.Net;
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId());
var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee;
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
var target = new ExportInvoiceHolder var target = new ExportInvoiceHolder
{ {
ReceivedDate = payment.ReceivedTime.UtcDateTime, ReceivedDate = payment.ReceivedTime.UtcDateTime,
PaymentId = pdata.GetPaymentId(), PaymentId = pdata.GetPaymentId(),
CryptoCode = cryptoCode, CryptoCode = payment.Currency,
ConversionRate = pmethod.Rate, ConversionRate = payment.Rate,
PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(), PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(),
Destination = pdata.GetDestination(), Destination = pdata.GetDestination(),
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture), Paid = payment.PaidAmount.Gross.ToString(CultureInfo.InvariantCulture),
PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).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 // 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 // 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 // and merchant effectivelly would receive 0 BTC, invoice won't be paid

View file

@ -378,6 +378,82 @@ namespace BTCPayServer.Services.Invoices
#pragma warning restore CS0618 #pragma warning restore CS0618
} }
[JsonIgnore]
public Dictionary<string, decimal> Rates
{
get;
private set;
}
public void UpdateTotals()
{
Rates = new Dictionary<string, decimal>();
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;
}
}
}
/// <summary>
/// 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.
/// </summary>
[JsonIgnore]
public decimal Dust { get; set; }
/// <summary>
/// The due to consider the invoice paid (can be negative if over payment)
/// </summary>
[JsonIgnore]
public decimal NetDue
{
get;
set;
}
/// <summary>
/// Minumum due to consider the invoice paid (can be negative if overpaid)
/// </summary>
[JsonIgnore]
public decimal MinimumNetDue { get; set; }
public bool IsUnderPaid => MinimumNetDue > 0;
[JsonIgnore]
public bool IsOverPaid => NetDue < 0;
/// <summary>
/// Total of network fee paid by accounted payments
/// </summary>
[JsonIgnore]
public decimal PaidFee { get; set; }
[JsonIgnore] [JsonIgnore]
public InvoiceStatusLegacy Status { get; set; } public InvoiceStatusLegacy Status { get; set; }
[JsonProperty(PropertyName = "status")] [JsonProperty(PropertyName = "status")]
@ -399,7 +475,7 @@ namespace BTCPayServer.Services.Invoices
} }
public List<PaymentEntity> GetPayments(string cryptoCode, bool accountedOnly) public List<PaymentEntity> 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<PaymentEntity> GetPayments(BTCPayNetworkBase network, bool accountedOnly) public List<PaymentEntity> GetPayments(BTCPayNetworkBase network, bool accountedOnly)
{ {
@ -554,13 +630,13 @@ namespace BTCPayServer.Services.Invoices
if (paymentId.PaymentType == PaymentTypes.BTCLike) if (paymentId.PaymentType == PaymentTypes.BTCLike)
{ {
var minerInfo = new MinerFeeInfo(); var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi; minerInfo.TotalFee = accounting.ToSmallestUnit(accounting.NetworkFee);
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate
.GetFee(1).Satoshi; .GetFee(1).Satoshi;
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
#pragma warning disable 618 #pragma warning disable 618
if (info.CryptoCode == "BTC") if (info.Currency == "BTC")
{ {
dto.BTCPrice = cryptoInfo.Price; dto.BTCPrice = cryptoInfo.Price;
dto.Rate = cryptoInfo.Rate; dto.Rate = cryptoInfo.Rate;
@ -576,8 +652,8 @@ namespace BTCPayServer.Services.Invoices
dto.CryptoInfo.Add(cryptoInfo); dto.CryptoInfo.Add(cryptoInfo);
dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls); dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls);
dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi); dto.PaymentSubtotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(subtotalPrice));
dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi); dto.PaymentTotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(accounting.TotalDue));
dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency() dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency()
{ {
Enabled = true Enabled = true
@ -640,12 +716,12 @@ namespace BTCPayServer.Services.Invoices
{ {
continue; continue;
} }
r.CryptoCode = paymentMethodId.CryptoCode; r.Currency = paymentMethodId.CryptoCode;
r.PaymentType = paymentMethodId.PaymentType.ToString(); r.PaymentType = paymentMethodId.PaymentType.ToString();
r.ParentEntity = this; r.ParentEntity = this;
if (Networks != null) if (Networks != null)
{ {
r.Network = Networks.GetNetwork<BTCPayNetworkBase>(r.CryptoCode); r.Network = Networks.GetNetwork<BTCPayNetworkBase>(r.Currency);
if (r.Network is null) if (r.Network is null)
continue; continue;
} }
@ -671,7 +747,7 @@ namespace BTCPayServer.Services.Invoices
foreach (var v in paymentMethods) foreach (var v in paymentMethods)
{ {
var clone = serializer.ToObject<PaymentMethod>(serializer.ToString(v)); var clone = serializer.ToObject<PaymentMethod>(serializer.ToString(v));
clone.CryptoCode = null; clone.Currency = null;
clone.PaymentType = null; clone.PaymentType = null;
obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone)))); obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone))));
} }
@ -681,6 +757,7 @@ namespace BTCPayServer.Services.Invoices
cryptoData.ParentEntity = this; cryptoData.ParentEntity = this;
} }
#pragma warning restore CS0618 #pragma warning restore CS0618
UpdateTotals();
} }
public InvoiceState GetInvoiceState() public InvoiceState GetInvoiceState()
@ -748,6 +825,8 @@ namespace BTCPayServer.Services.Invoices
{ {
return Type == InvoiceType.TopUp && Price == 0.0m; return Type == InvoiceType.TopUp && Price == 0.0m;
} }
public Amounts PaidAmount { get; set; }
} }
public enum InvoiceStatusLegacy public enum InvoiceStatusLegacy
@ -897,30 +976,31 @@ namespace BTCPayServer.Services.Invoices
public class PaymentMethodAccounting public class PaymentMethodAccounting
{ {
public int Divisibility { get; set; }
/// <summary>Total amount of this invoice</summary> /// <summary>Total amount of this invoice</summary>
public Money TotalDue { get; set; } public decimal TotalDue { get; set; }
/// <summary>Amount of crypto remaining to pay this invoice</summary> /// <summary>Amount of crypto remaining to pay this invoice</summary>
public Money Due { get; set; } public decimal Due { get; set; }
/// <summary>Same as Due, can be negative</summary> /// <summary>Same as Due, can be negative</summary>
public Money DueUncapped { get; set; } public decimal DueUncapped { get; set; }
/// <summary>If DueUncapped is negative, that means user overpaid invoice</summary> /// <summary>If DueUncapped is negative, that means user overpaid invoice</summary>
public Money OverpaidHelper public decimal OverpaidHelper
{ {
get { return DueUncapped > Money.Zero ? Money.Zero : -DueUncapped; } get { return DueUncapped > 0.0m ? 0.0m : -DueUncapped; }
} }
/// <summary> /// <summary>
/// Total amount of the invoice paid after conversion to this crypto currency /// Total amount of the invoice paid after conversion to this crypto currency
/// </summary> /// </summary>
public Money Paid { get; set; } public decimal Paid { get; set; }
/// <summary> /// <summary>
/// Total amount of the invoice paid in this currency /// Total amount of the invoice paid in this currency
/// </summary> /// </summary>
public Money CryptoPaid { get; set; } public decimal CryptoPaid { get; set; }
/// <summary> /// <summary>
/// Number of transactions required to pay /// Number of transactions required to pay
@ -934,15 +1014,25 @@ namespace BTCPayServer.Services.Invoices
/// <summary> /// <summary>
/// Total amount of network fee to pay to the invoice /// Total amount of network fee to pay to the invoice
/// </summary> /// </summary>
public Money NetworkFee { get; set; } public decimal NetworkFee { get; set; }
/// <summary> /// <summary>
/// Total amount of network fee to pay to the invoice /// Total amount of network fee to pay to the invoice
/// </summary> /// </summary>
public Money NetworkFeeAlreadyPaid { get; set; } public decimal NetworkFeeAlreadyPaid { get; set; }
/// <summary> /// <summary>
/// Minimum required to be paid in order to accept invoice as paid /// Minimum required to be paid in order to accept invoice as paid
/// </summary> /// </summary>
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 public interface IPaymentMethod
@ -959,8 +1049,7 @@ namespace BTCPayServer.Services.Invoices
[JsonIgnore] [JsonIgnore]
public BTCPayNetworkBase Network { get; set; } public BTCPayNetworkBase Network { get; set; }
[JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)] [JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
[Obsolete("Use GetId().CryptoCode instead")] public string Currency { get; set; }
public string CryptoCode { get; set; }
[JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)] [JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)]
[Obsolete("Use GetId().PaymentType instead")] [Obsolete("Use GetId().PaymentType instead")]
public string PaymentType { get; set; } public string PaymentType { get; set; }
@ -975,14 +1064,14 @@ namespace BTCPayServer.Services.Invoices
public PaymentMethodId GetId() public PaymentMethodId GetId()
{ {
#pragma warning disable CS0618 // Type or member is obsolete #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 #pragma warning restore CS0618 // Type or member is obsolete
} }
public void SetId(PaymentMethodId id) public void SetId(PaymentMethodId id)
{ {
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
CryptoCode = id.CryptoCode; Currency = id.CryptoCode;
PaymentType = id.PaymentType.ToString(); PaymentType = id.PaymentType.ToString();
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
} }
@ -1053,7 +1142,6 @@ namespace BTCPayServer.Services.Invoices
DepositAddress = bitcoinPaymentMethod.DepositAddress; DepositAddress = bitcoinPaymentMethod.DepositAddress;
} }
PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod)); PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod));
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
return this; return this;
} }
@ -1068,86 +1156,58 @@ namespace BTCPayServer.Services.Invoices
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")] [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
public string DepositAddress { get; set; } public string DepositAddress { get; set; }
public PaymentMethodAccounting Calculate(Func<PaymentEntity, bool> paymentPredicate = null) public PaymentMethodAccounting Calculate()
{ {
paymentPredicate = paymentPredicate ?? new Func<PaymentEntity, bool>((p) => true); var i = ParentEntity;
var paymentMethods = ParentEntity.GetPaymentMethods();
var totalDue = ParentEntity.Price / Rate;
var paid = 0m;
var cryptoPaid = 0.0m;
int precision = Network?.Divisibility ?? 8; 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(); var accounting = new PaymentMethodAccounting();
accounting.TxCount = txRequired; var thisPaymentMethodPayments = i.GetPayments(true).Where(p => GetId() == p.GetPaymentMethodId()).ToList();
if (!paidEnough) accounting.TxCount = thisPaymentMethodPayments.Count;
accounting.TxRequired = accounting.TxCount;
var grossDue = i.Price + i.PaidFee;
if (i.MinimumNetDue > 0.0m)
{ {
txRequired++; accounting.TxRequired++;
totalDue += GetTxFee(); 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)); // This one deal with the fact where it might looks like a slight over payment due to the dust of another payment method.
accounting.Paid = Coins(Extensions.RoundUp(paid, precision)); // So if we detect the NetDue is zero, just cap dueUncapped to 0
accounting.TxRequired = txRequired; var dueUncapped = i.NetDue == 0.0m ? 0.0m : grossDue - i.PaidAmount.Gross;
accounting.CryptoPaid = Coins(Extensions.RoundUp(cryptoPaid, precision)); accounting.DueUncapped = Coins(dueUncapped / Rate, precision);
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero); accounting.Due = Max(accounting.DueUncapped, 0.0m);
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost; accounting.NetworkFee = Coins((grossDue - i.Price) / Rate, precision);
accounting.NetworkFeeAlreadyPaid = Coins(Extensions.RoundUp(networkFeeAlreadyPaid, precision)); accounting.NetworkFeeAlreadyPaid = Coins(i.PaidFee / Rate, precision);
// If the total due is 0, there is no payment tolerance to calculate
var minimumTotalDueSatoshi = accounting.TotalDue.Satoshi == 0 accounting.MinimumTotalDue = Max(Smallest(precision), Coins((grossDue * (1.0m - ((decimal)i.PaymentTolerance / 100.0m))) / Rate, precision));
? 0
: Math.Max(1.0m,
accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m)));
accounting.MinimumTotalDue = Money.Satoshis(minimumTotalDueSatoshi);
return accounting; return accounting;
} }
const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m; private decimal Smallest(int precision)
private Money Coins(decimal v)
{ {
if (v > MaxCoinValue) decimal a = 1.0m;
v = MaxCoinValue; for (int i = 0; i < precision; i++)
// Clamp the value to not crash on degenerate invoices {
v *= 1_0000_0000m; a /= 10.0m;
if (v > long.MaxValue) }
return Money.Satoshis(long.MaxValue); return a;
if (v < long.MinValue)
return Money.Satoshis(long.MinValue);
return Money.Satoshis(v);
} }
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; get; set;
} }
string _Currency;
[Obsolete("Use GetpaymentMethodId().CryptoCode instead")] [JsonProperty("cryptoCode")]
public string CryptoCode public string Currency
{ {
get; get
set; {
return _Currency ?? "BTC";
}
set
{
_Currency = value;
}
} }
[Obsolete("Use GetCryptoPaymentData() instead")] [Obsolete("Use GetCryptoPaymentData() instead")]
public string CryptoPaymentData { get; set; } public string CryptoPaymentData { get; set; }
[Obsolete("Use GetpaymentMethodId().PaymentType instead")] [Obsolete("Use GetpaymentMethodId().PaymentType instead")]
public string CryptoPaymentDataType { get; set; } public string CryptoPaymentDataType { get; set; }
[JsonIgnore]
public decimal Rate { get; set; }
[JsonIgnore]
/// <summary>
public string InvoiceCurrency => InvoiceEntity.Currency;
/// The amount paid by this payment in the <see cref="Currency"/>
/// </summary>
[JsonIgnore]
public Amounts PaidAmount { get; set; }
/// <summary>
/// The amount paid by this payment in the <see cref="InvoiceCurrency"/>
/// </summary>
[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() public CryptoPaymentData GetCryptoPaymentData()
{ {
@ -1280,21 +1381,6 @@ namespace BTCPayServer.Services.Invoices
#pragma warning restore CS0618 #pragma warning restore CS0618
return this; 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() public PaymentMethodId GetPaymentMethodId()
{ {
@ -1308,16 +1394,9 @@ namespace BTCPayServer.Services.Invoices
{ {
return null; return null;
} }
return new PaymentMethodId(CryptoCode ?? "BTC", paymentType); return new PaymentMethodId(Currency ?? "BTC", paymentType);
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
} }
public string GetCryptoCode()
{
#pragma warning disable CS0618
return CryptoCode ?? "BTC";
#pragma warning restore CS0618
}
} }
/// <summary> /// <summary>
/// A record of a payment /// A record of a payment

View file

@ -12,7 +12,6 @@ using BTCPayServer.Logging;
using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -30,16 +29,13 @@ namespace BTCPayServer.Services.Invoices
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings); NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
} }
public Logs Logs { get; }
private readonly ApplicationDbContextFactory _applicationDbContextFactory; private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, public InvoiceRepository(ApplicationDbContextFactory contextFactory,
BTCPayNetworkProvider networks, EventAggregator eventAggregator, Logs logs) BTCPayNetworkProvider networks, EventAggregator eventAggregator)
{ {
Logs = logs;
_applicationDbContextFactory = contextFactory; _applicationDbContextFactory = contextFactory;
_btcPayNetworkProvider = networks; _btcPayNetworkProvider = networks;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@ -54,14 +50,20 @@ namespace BTCPayServer.Services.Invoices
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
public InvoiceEntity CreateNewInvoice() public InvoiceEntity CreateNewInvoice(string storeId)
{ {
return new InvoiceEntity() return new InvoiceEntity()
{ {
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)),
StoreId = storeId,
Networks = _btcPayNetworkProvider, Networks = _btcPayNetworkProvider,
Version = InvoiceEntity.Lastest_Version, Version = InvoiceEntity.Lastest_Version,
InvoiceTime = DateTimeOffset.UtcNow, // Truncating was an unintended side effect of previous code. Might want to remove that one day
Metadata = new InvoiceMetadata() InvoiceTime = DateTimeOffset.UtcNow.TruncateMilliSeconds(),
Metadata = new InvoiceMetadata(),
#pragma warning disable CS0618
Payments = new List<PaymentEntity>()
#pragma warning restore CS0618
}; };
} }
@ -173,21 +175,14 @@ namespace BTCPayServer.Services.Invoices
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, string[] additionalSearchTerms = null) public async Task CreateInvoiceAsync(InvoiceEntity invoice, string[] additionalSearchTerms = null)
{ {
var textSearch = new HashSet<string>(); var textSearch = new HashSet<string>();
invoice = Clone(invoice);
invoice.Networks = _btcPayNetworkProvider;
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
#pragma warning disable CS0618
invoice.Payments = new List<PaymentEntity>();
#pragma warning restore CS0618
invoice.StoreId = storeId;
using (var context = _applicationDbContextFactory.CreateContext()) using (var context = _applicationDbContextFactory.CreateContext())
{ {
var invoiceData = new Data.InvoiceData() var invoiceData = new Data.InvoiceData()
{ {
StoreDataId = storeId, StoreDataId = invoice.StoreId,
Id = invoice.Id, Id = invoice.Id,
Created = invoice.InvoiceTime, Created = invoice.InvoiceTime,
OrderId = invoice.Metadata.OrderId, OrderId = invoice.Metadata.OrderId,
@ -245,16 +240,6 @@ namespace BTCPayServer.Services.Invoices
await context.SaveChangesAsync().ConfigureAwait(false); 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) public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs)
@ -617,6 +602,7 @@ namespace BTCPayServer.Services.Invoices
entity.Metadata.BuyerEmail = entity.RefundMail; entity.Metadata.BuyerEmail = entity.RefundMail;
} }
entity.Archived = invoice.Archived; entity.Archived = invoice.Archived;
entity.UpdateTotals();
return entity; return entity;
} }
@ -828,9 +814,8 @@ namespace BTCPayServer.Services.Invoices
{ {
var paymentMethodContribution = new InvoiceStatistics.Contribution(); var paymentMethodContribution = new InvoiceStatistics.Contribution();
paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId(); paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId();
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee; paymentMethodContribution.CurrencyValue = pay.InvoicePaidAmount.Net;
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMethodId).Rate; paymentMethodContribution.Value = pay.PaidAmount.Net;
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
return paymentMethodContribution; return paymentMethodContribution;
}) })
.ToArray(); .ToArray();

View file

@ -51,7 +51,7 @@ namespace BTCPayServer.Services.Invoices
{ {
Version = 1, Version = 1,
#pragma warning disable CS0618 #pragma warning disable CS0618
CryptoCode = network.CryptoCode, Currency = network.CryptoCode,
#pragma warning restore CS0618 #pragma warning restore CS0618
ReceivedTime = date.UtcDateTime, ReceivedTime = date.UtcDateTime,
Accounted = accounted, Accounted = accounted,