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
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Common;
@ -34,12 +35,12 @@ namespace BTCPayServer
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
}
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
{
//precision 0: 10 = 0.00000010
//precision 2: 10 = 0.00001000
//precision 8: 10 = 10
var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility);
var builder = base.GenerateBIP21(cryptoInfoAddress, money);
builder.QueryParams.Add("assetid", AssetId.ToString());
return builder;

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using BTCPayServer.Common;
@ -87,13 +88,13 @@ namespace BTCPayServer
});
}
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
{
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
builder.Host = cryptoInfoAddress;
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero)
if (cryptoInfoDue is not null && cryptoInfoDue.Value != 0.0m)
{
builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true));
builder.QueryParams.Add("amount", cryptoInfoDue.Value.ToString(CultureInfo.InvariantCulture));
}
return builder;
}

View file

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

View file

@ -1761,7 +1761,7 @@ namespace BTCPayServer.Tests
var parsedJson = await GetExport(user);
Assert.Equal(3, parsedJson.Length);
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
var invoiceDueAfterFirstPayment = 3 * networkFee.ToDecimal(MoneyUnit.BTC) * invoice.Rate;
var pay1str = parsedJson[0].ToString();
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));

View file

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

View file

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

View file

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

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

View file

@ -548,7 +548,7 @@ namespace BTCPayServer
lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
if (i.Type != InvoiceType.TopUp)
{
lnurlRequest.MinSendable = new LightMoney(pm.Calculate().Due.ToDecimal(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi);
lnurlRequest.MinSendable = LightMoney.Coins(pm.Calculate().Due);
if (!allowOverpay)
lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
}

View file

@ -303,7 +303,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
bip21.Add(newUri.Uri.ToString());
break;
case AddressClaimDestination addressClaimDestination:
var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), new Money(blob.CryptoAmount.Value, MoneyUnit.BTC));
var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), blob.CryptoAmount.Value);
bip21New.QueryParams.Add("payout", payout.Id);
bip21.Add(bip21New.ToString());
break;

View file

@ -30,6 +30,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.Models;
using Newtonsoft.Json;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
@ -38,6 +39,15 @@ namespace BTCPayServer
{
public static class Extensions
{
public static DateTimeOffset TruncateMilliSeconds(this DateTimeOffset dt) => new (dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, dt.Offset);
public static decimal? GetDue(this InvoiceCryptoInfo invoiceCryptoInfo)
{
if (invoiceCryptoInfo is null)
return null;
if (decimal.TryParse(invoiceCryptoInfo.Due, NumberStyles.Any, CultureInfo.InvariantCulture, out var v))
return v;
return null;
}
public static Task<BufferizedFormFile> Bufferize(this IFormFile formFile)
{
return BufferizedFormFile.Bufferize(formFile);
@ -382,20 +392,6 @@ namespace BTCPayServer
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)
{
var _networkType = DefaultConfiguration.GetNetworkType(configuration);

View file

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

View file

@ -103,7 +103,7 @@ namespace BTCPayServer.HostedServices
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData:
{
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.Currency);
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
var labels = new List<Attachment>
{

View file

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

View file

@ -123,18 +123,14 @@ namespace BTCPayServer.PaymentRequest
string txId = paymentData.GetPaymentId();
string link = GetTransactionLink(paymentMethodId, txId);
var paymentMethod = entity.GetPaymentMethod(paymentMethodId);
var amount = paymentData.GetValue();
var rate = paymentMethod.Rate;
var paid = (amount - paymentEntity.NetworkFee) * rate;
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
{
Amount = amount,
Paid = paid,
Amount = paymentEntity.PaidAmount.Gross,
Paid = paymentEntity.InvoicePaidAmount.Net,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _displayFormatter.Currency(paid, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link,
Id = txId,

View file

@ -211,7 +211,7 @@ namespace BTCPayServer.Payments.Bitcoin
new Key().GetScriptPubKey(supportedPaymentMethod.AccountDerivation.ScriptPubKeyType());
var dust = txOut.GetDustThreshold();
var amount = paymentMethod.Calculate().Due;
if (amount < dust)
if (amount < dust.ToDecimal(MoneyUnit.BTC))
throw new PaymentMethodUnavailableException("Amount below the dust threshold. For amounts of this size, it is recommended to enable an off-chain (Lightning) payment method");
}
if (preparePaymentObject is null)

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments.Lightning;
@ -28,7 +29,7 @@ namespace BTCPayServer.Payments
}
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri)
decimal cryptoInfoDue, string serverUri)
{
if (!paymentMethodDetails.Activated)
{
@ -74,7 +75,7 @@ namespace BTCPayServer.Payments
{
AdditionalData = new Dictionary<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))}
}
};

View file

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

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)
{
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);
}

View file

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

View file

@ -67,7 +67,7 @@ namespace BTCPayServer.Payments
}
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri)
decimal cryptoInfoDue, string serverUri)
{
if (!paymentMethodDetails.Activated)
{
@ -105,7 +105,7 @@ namespace BTCPayServer.Payments
{
cryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls()
{
BIP21 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), cryptoInfo.Due, serverUrl),
BIP21 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), cryptoInfo.GetDue().Value, serverUrl),
};
}
}

View file

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

View file

@ -85,7 +85,7 @@ namespace BTCPayServer.Payments
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
public abstract string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri);
decimal cryptoInfoDue, string serverUri);
public abstract string InvoiceViewPaymentPartialName { get; }
public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore);

View file

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

View file

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

View file

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

View file

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

View file

@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
}
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri)
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, decimal cryptoInfoDue, string serverUri)
{
return paymentMethodDetails.Activated
? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"
? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue}"
: string.Empty;
}

View file

@ -119,16 +119,16 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
{
_logger.LogInformation(
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}");
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.Currency} {payment.GetCryptoPaymentData().GetPaymentId()}");
var paymentData = (MoneroLikePaymentData)payment.GetCryptoPaymentData();
var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance);
if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero &&
monero.Activated &&
monero.GetPaymentDestination() == paymentData.GetDestination() &&
paymentMethod.Calculate().Due > Money.Zero)
paymentMethod.Calculate().Due > 0.0m)
{
var walletClient = _moneroRpcProvider.WalletRpcClients[payment.GetCryptoCode()];
var walletClient = _moneroRpcProvider.WalletRpcClients[payment.Currency];
var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
"create_address",

View file

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

View file

@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
}
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri)
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, decimal cryptoInfoDue, string serverUri)
{
return paymentMethodDetails.Activated
? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"
? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue}"
: string.Empty;
}

View file

@ -114,16 +114,16 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
{
_logger.LogInformation(
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}");
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.Currency} {payment.GetCryptoPaymentData().GetPaymentId()}");
var paymentData = (ZcashLikePaymentData)payment.GetCryptoPaymentData();
var paymentMethod = invoice.GetPaymentMethod(payment.Network, ZcashPaymentType.Instance);
if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is ZcashLikeOnChainPaymentMethodDetails Zcash &&
Zcash.Activated &&
Zcash.GetPaymentDestination() == paymentData.GetDestination() &&
paymentMethod.Calculate().Due > Money.Zero)
paymentMethod.Calculate().Due > 0.0m)
{
var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.GetCryptoCode()];
var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.Currency];
var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
"create_address",

View file

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

View file

@ -183,18 +183,11 @@ namespace BTCPayServer.Services.Apps
}
}
else
{
var fiatPrice = e.GetPayments(true).Sum(pay =>
{
var paymentMethodId = pay.GetPaymentMethodId();
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
var rate = e.GetPaymentMethod(paymentMethodId).Rate;
return rate * value;
});
{;
res.Add(new InvoiceStatsItem
{
ItemCode = e.Metadata.ItemCode,
FiatPrice = fiatPrice,
FiatPrice = e.PaidAmount.Net,
Date = e.InvoiceTime.Date
});
}

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)
{
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
var pdata = payment.GetCryptoPaymentData();
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId());
var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee;
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
invoiceDue -= payment.InvoicePaidAmount.Net;
var target = new ExportInvoiceHolder
{
ReceivedDate = payment.ReceivedTime.UtcDateTime,
PaymentId = pdata.GetPaymentId(),
CryptoCode = cryptoCode,
ConversionRate = pmethod.Rate,
CryptoCode = payment.Currency,
ConversionRate = payment.Rate,
PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(),
Destination = pdata.GetDestination(),
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
Paid = payment.PaidAmount.Gross.ToString(CultureInfo.InvariantCulture),
PaidCurrency = Math.Round(payment.InvoicePaidAmount.Gross, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
// Adding NetworkFee because Paid doesn't take into account network fees
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
// and merchant effectivelly would receive 0 BTC, invoice won't be paid

View file

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

View file

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

View file

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