Make sure to only select accounted payments where we should (#2523)

This commit is contained in:
Nicolas Dorier
2021-05-14 16:16:19 +09:00
committed by GitHub
parent 776ded0b7e
commit c551e5cd0a
16 changed files with 48 additions and 42 deletions

View File

@@ -320,7 +320,7 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () => await TestUtils.EventuallyAsync(async () =>
{ {
var invoice = await invoiceRepository.GetInvoice(invoiceId); var invoice = await invoiceRepository.GetInvoice(invoiceId);
var payments = invoice.GetPayments(); var payments = invoice.GetPayments(false);
Assert.Equal(2, payments.Count); Assert.Equal(2, payments.Count);
var originalPayment = payments[0]; var originalPayment = payments[0];
var coinjoinPayment = payments[1]; var coinjoinPayment = payments[1];
@@ -1088,7 +1088,7 @@ retry:
{ {
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id); var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.Paid, invoiceEntity.Status); Assert.Equal(InvoiceStatusLegacy.Paid, invoiceEntity.Status);
Assert.Contains(invoiceEntity.GetPayments(), p => p.Accounted && Assert.Contains(invoiceEntity.GetPayments(false), p => p.Accounted &&
((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null); ((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null);
}); });
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen); ////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
@@ -1117,8 +1117,8 @@ retry:
{ {
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id); var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.New, invoiceEntity.Status); Assert.Equal(InvoiceStatusLegacy.New, invoiceEntity.Status);
Assert.True(invoiceEntity.GetPayments().All(p => !p.Accounted)); Assert.True(invoiceEntity.GetPayments(false).All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData().First().PayjoinInformation.ContributedOutPoints[0]; ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(false).First().PayjoinInformation.ContributedOutPoints[0];
}); });
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>(); var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
// The outpoint should now be available for next pj selection // The outpoint should now be available for next pj selection

View File

@@ -1639,8 +1639,8 @@ namespace BTCPayServer.Tests
{ {
var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id); var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id);
Assert.Equal(InvoiceStatusLegacy.New, i.Status); Assert.Equal(InvoiceStatusLegacy.New, i.Status);
Assert.Single(i.GetPayments()); Assert.Single(i.GetPayments(false));
Assert.False(i.GetPayments().First().Accounted); Assert.False(i.GetPayments(false).First().Accounted);
}); });
Logs.Tester.LogInformation( Logs.Tester.LogInformation(
@@ -1672,8 +1672,8 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () => await TestUtils.EventuallyAsync(async () =>
{ {
var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id); var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData().ToArray(); var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(false).ToArray();
var payments = invoiceEntity.GetPayments().ToArray(); var payments = invoiceEntity.GetPayments(false).ToArray();
Assert.Equal(tx1, btcPayments[0].Outpoint.Hash); Assert.Equal(tx1, btcPayments[0].Outpoint.Hash);
Assert.False(payments[0].Accounted); Assert.False(payments[0].Accounted);
Assert.Equal(tx1Bump, payments[1].Outpoint.Hash); Assert.Equal(tx1Bump, payments[1].Outpoint.Hash);

View File

@@ -283,7 +283,7 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanViewInvoices, [Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)] AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")] [HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId) public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true)
{ {
var store = HttpContext.GetStoreData(); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
@@ -297,7 +297,7 @@ namespace BTCPayServer.Controllers.GreenField
return InvoiceNotFound(); return InvoiceNotFound();
} }
return Ok(ToPaymentMethodModels(invoice)); return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments));
} }
[Authorize(Policy = Policies.CanViewInvoices, [Authorize(Policy = Policies.CanViewInvoices,
@@ -336,14 +336,14 @@ namespace BTCPayServer.Controllers.GreenField
return this.CreateAPIError(404, "store-not-found", "The store was not found"); return this.CreateAPIError(404, "store-not-found", "The store was not found");
} }
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity) private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly)
{ {
return entity.GetPaymentMethods().Select( return entity.GetPaymentMethods().Select(
method => method =>
{ {
var accounting = method.Calculate(); var accounting = method.Calculate();
var details = method.GetPaymentMethodDetails(); var details = method.GetPaymentMethodDetails();
var payments = method.ParentEntity.GetPayments().Where(paymentEntity => var payments = method.ParentEntity.GetPayments(includeAccountedPaymentOnly).Where(paymentEntity =>
paymentEntity.GetPaymentMethodId() == method.GetId()); paymentEntity.GetPaymentMethodId() == method.GetId());
return new InvoicePaymentMethodDataModel() return new InvoicePaymentMethodDataModel()

View File

@@ -359,7 +359,7 @@ namespace BTCPayServer.Controllers
return new InvoiceDetailsModel return new InvoiceDetailsModel
{ {
Archived = invoice.Archived, Archived = invoice.Archived,
Payments = invoice.GetPayments(), Payments = invoice.GetPayments(false),
CryptoPayments = invoice.GetPaymentMethods().Select( CryptoPayments = invoice.GetPaymentMethods().Select(
data => data =>
{ {
@@ -561,7 +561,7 @@ namespace BTCPayServer.Controllers
Status = invoice.StatusString, Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
NetworkFee = paymentMethodDetails.GetNextNetworkFee(), NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1, IsMultiCurrency = invoice.GetPayments(false).Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
StoreId = store.Id, StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods() AvailableCryptos = invoice.GetPaymentMethods()
.Where(i => i.Network != null) .Where(i => i.Network != null)

View File

@@ -133,9 +133,9 @@ namespace BTCPayServer
finally { try { webSocket.Dispose(); } catch { } } finally { try { webSocket.Dispose(); } catch { } }
} }
public static IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(this InvoiceEntity invoice) public static IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(this InvoiceEntity invoice, bool accountedOnly)
{ {
return invoice.GetPayments() return invoice.GetPayments(accountedOnly)
.Where(p => p.GetPaymentMethodId()?.PaymentType == PaymentTypes.BTCLike) .Where(p => p.GetPaymentMethodId()?.PaymentType == PaymentTypes.BTCLike)
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData()) .Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData())
.Where(data => data != null); .Where(data => data != null);

View File

@@ -101,7 +101,7 @@ namespace BTCPayServer.HostedServices
} }
} }
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial) if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments(true).Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
{ {
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial; invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
context.MarkDirty(); context.MarkDirty();
@@ -335,7 +335,7 @@ namespace BTCPayServer.HostedServices
{ {
bool extendInvoiceMonitoring = false; bool extendInvoiceMonitoring = false;
var updateConfirmationCountIfNeeded = invoice var updateConfirmationCountIfNeeded = invoice
.GetPayments() .GetPayments(false)
.Select<PaymentEntity, Task<PaymentEntity>>(async payment => .Select<PaymentEntity, Task<PaymentEntity>>(async payment =>
{ {
var paymentData = payment.GetCryptoPaymentData(); var paymentData = payment.GetCryptoPaymentData();

View File

@@ -46,7 +46,7 @@ namespace BTCPayServer.HostedServices
UpdateTransactionLabel.InvoiceLabelTemplate(invoiceEvent.Invoice.Id) UpdateTransactionLabel.InvoiceLabelTemplate(invoiceEvent.Invoice.Id)
}; };
if (invoiceEvent.Invoice.GetPayments(invoiceEvent.Payment.GetCryptoCode()).Any(entity => if (invoiceEvent.Invoice.GetPayments(invoiceEvent.Payment.GetCryptoCode(), false).Any(entity =>
entity.GetCryptoPaymentData() is BitcoinLikePaymentData pData && entity.GetCryptoPaymentData() is BitcoinLikePaymentData pData &&
pData.PayjoinInformation?.CoinjoinTransactionHash == transactionId)) pData.PayjoinInformation?.CoinjoinTransactionHash == transactionId))
{ {

View File

@@ -111,8 +111,7 @@ namespace BTCPayServer.PaymentRequest
State = state, State = state,
StateFormatted = state.ToString(), StateFormatted = state.ToString(),
Payments = entity Payments = entity
.GetPayments() .GetPayments(true)
.Where(p => p.Accounted)
.Select(paymentEntity => .Select(paymentEntity =>
{ {
var paymentData = paymentEntity.GetCryptoPaymentData(); var paymentData = paymentEntity.GetCryptoPaymentData();

View File

@@ -157,7 +157,7 @@ namespace BTCPayServer.Payments.Bitcoin
output.matchedOutput.Value, output.outPoint, output.matchedOutput.Value, output.outPoint,
evt.TransactionData.Transaction.RBF, output.Item1.KeyPath); evt.TransactionData.Transaction.RBF, output.Item1.KeyPath);
var alreadyExist = invoice.GetAllBitcoinPaymentData().Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); var alreadyExist = invoice.GetAllBitcoinPaymentData(false).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
if (!alreadyExist) if (!alreadyExist)
{ {
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network);
@@ -220,7 +220,7 @@ namespace BTCPayServer.Payments.Bitcoin
{ {
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>(); List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
var transactions = await wallet.GetTransactions(invoice.GetAllBitcoinPaymentData() var transactions = await wallet.GetTransactions(invoice.GetAllBitcoinPaymentData(false)
.Select(p => p.Outpoint.Hash) .Select(p => p.Outpoint.Hash)
.ToArray(), true); .ToArray(), true);
bool? originalPJBroadcasted = null; bool? originalPJBroadcasted = null;
@@ -228,7 +228,7 @@ namespace BTCPayServer.Payments.Bitcoin
bool cjPJBroadcasted = false; bool cjPJBroadcasted = false;
PayjoinInformation payjoinInformation = null; PayjoinInformation payjoinInformation = null;
var paymentEntitiesByPrevOut = new Dictionary<OutPoint, PaymentEntity>(); var paymentEntitiesByPrevOut = new Dictionary<OutPoint, PaymentEntity>();
foreach (var payment in invoice.GetPayments(wallet.Network)) foreach (var payment in invoice.GetPayments(wallet.Network, false))
{ {
if (payment.GetPaymentMethodId()?.PaymentType != PaymentTypes.BTCLike) if (payment.GetPaymentMethodId()?.PaymentType != PaymentTypes.BTCLike)
continue; continue;
@@ -347,7 +347,7 @@ namespace BTCPayServer.Payments.Bitcoin
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null) if (invoice == null)
continue; continue;
var alreadyAccounted = invoice.GetAllBitcoinPaymentData().Select(p => p.Outpoint).ToHashSet(); var alreadyAccounted = invoice.GetAllBitcoinPaymentData(false).Select(p => p.Outpoint).ToHashSet();
var strategy = GetDerivationStrategy(invoice, network); var strategy = GetDerivationStrategy(invoice, network);
if (strategy == null) if (strategy == null)
continue; continue;

View File

@@ -300,7 +300,7 @@ namespace BTCPayServer.Payments.PayJoin
paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork);
paymentAddressIndex = paymentDetails.KeyPath; paymentAddressIndex = paymentDetails.KeyPath;
if (invoice.GetAllBitcoinPaymentData().Any()) if (invoice.GetAllBitcoinPaymentData(false).Any())
{ {
ctx.DoNotBroadcast(); ctx.DoNotBroadcast();
return UnprocessableEntity(CreatePayjoinError("already-paid", return UnprocessableEntity(CreatePayjoinError("already-paid",

View File

@@ -242,7 +242,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
.Select(entity => ( .Select(entity => (
Invoice: entity, Invoice: entity,
PaymentMethodDetails: entity.GetPaymentMethods().TryGet(paymentMethodId), PaymentMethodDetails: entity.GetPaymentMethods().TryGet(paymentMethodId),
ExistingPayments: entity.GetPayments(network).Select(paymentEntity => (Payment: paymentEntity, ExistingPayments: entity.GetPayments(network, true).Select(paymentEntity => (Payment: paymentEntity,
PaymentData: (EthereumLikePaymentData)paymentEntity.GetCryptoPaymentData(), PaymentData: (EthereumLikePaymentData)paymentEntity.GetCryptoPaymentData(),
Invoice: entity)) Invoice: entity))
)).Where(tuple => tuple.PaymentMethodDetails?.GetPaymentMethodDetails()?.Activated is true).ToList(); )).Where(tuple => tuple.PaymentMethodDetails?.GetPaymentMethodDetails()?.Activated is true).ToList();

View File

@@ -372,7 +372,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
private IEnumerable<PaymentEntity> GetAllMoneroLikePayments(InvoiceEntity invoice, string cryptoCode) private IEnumerable<PaymentEntity> GetAllMoneroLikePayments(InvoiceEntity invoice, string cryptoCode)
{ {
return invoice.GetPayments() return invoice.GetPayments(false)
.Where(p => p.GetPaymentMethodId() == new PaymentMethodId(cryptoCode, MoneroPaymentType.Instance)); .Where(p => p.GetPaymentMethodId() == new PaymentMethodId(cryptoCode, MoneroPaymentType.Instance));
} }
} }

View File

@@ -359,7 +359,7 @@ namespace BTCPayServer.Services.Apps
// If the user get a donation via other mean, he can register an invoice manually for such amount // If the user get a donation via other mean, he can register an invoice manually for such amount
// then mark the invoice as complete // then mark the invoice as complete
var payments = p.GetPayments(); var payments = p.GetPayments(true);
if (payments.Count == 0 && if (payments.Count == 0 &&
p.ExceptionStatus == InvoiceExceptionStatus.Marked && p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatusLegacy.Complete) p.Status == InvoiceStatusLegacy.Complete)

View File

@@ -59,11 +59,8 @@ namespace BTCPayServer.Services.Invoices.Export
var currency = Currencies.GetNumberFormatInfo(invoice.Currency, true); var currency = Currencies.GetNumberFormatInfo(invoice.Currency, true);
var invoiceDue = invoice.Price; var invoiceDue = invoice.Price;
// in this first version we are only exporting invoices that were paid // in this first version we are only exporting invoices that were paid
foreach (var payment in invoice.GetPayments()) foreach (var payment in invoice.GetPayments(true))
{ {
// not accounted payments are payments which got double spent like RBfed
if (!payment.Accounted)
continue;
var cryptoCode = payment.GetPaymentMethodId().CryptoCode; var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
var pdata = payment.GetCryptoPaymentData(); var pdata = payment.GetCryptoPaymentData();

View File

@@ -326,17 +326,17 @@ namespace BTCPayServer.Services.Invoices
public List<PaymentEntity> Payments { get; set; } public List<PaymentEntity> Payments { get; set; }
#pragma warning disable CS0618 #pragma warning disable CS0618
public List<PaymentEntity> GetPayments() public List<PaymentEntity> GetPayments(bool accountedOnly)
{ {
return Payments?.Where(entity => entity.GetPaymentMethodId() != null).ToList() ?? new List<PaymentEntity>(); return Payments?.Where(entity => entity.GetPaymentMethodId() != null && (!accountedOnly || entity.Accounted)).ToList() ?? new List<PaymentEntity>();
} }
public List<PaymentEntity> GetPayments(string cryptoCode) public List<PaymentEntity> GetPayments(string cryptoCode, bool accountedOnly)
{ {
return GetPayments().Where(p => p.CryptoCode == cryptoCode).ToList(); return GetPayments(accountedOnly).Where(p => p.CryptoCode == cryptoCode).ToList();
} }
public List<PaymentEntity> GetPayments(BTCPayNetworkBase network) public List<PaymentEntity> GetPayments(BTCPayNetworkBase network, bool accountedOnly)
{ {
return GetPayments(network.CryptoCode); return GetPayments(network.CryptoCode, accountedOnly);
} }
#pragma warning restore CS0618 #pragma warning restore CS0618
public bool Refundable { get; set; } public bool Refundable { get; set; }
@@ -449,7 +449,7 @@ namespace BTCPayServer.Services.Invoices
var paymentId = info.GetId(); var paymentId = info.GetId();
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}"; cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}";
cryptoInfo.Payments = GetPayments(info.Network).Select(entity => cryptoInfo.Payments = GetPayments(info.Network, true).Select(entity =>
{ {
var data = entity.GetCryptoPaymentData(); var data = entity.GetCryptoPaymentData();
return new InvoicePaymentInfo() return new InvoicePaymentInfo()
@@ -980,8 +980,8 @@ namespace BTCPayServer.Services.Invoices
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision); bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
int txRequired = 0; int txRequired = 0;
_ = ParentEntity.GetPayments() _ = ParentEntity.GetPayments(true)
.Where(p => p.Accounted && paymentPredicate(p)) .Where(p => paymentPredicate(p))
.OrderBy(p => p.ReceivedTime) .OrderBy(p => p.ReceivedTime)
.Select(_ => .Select(_ =>
{ {

View File

@@ -347,6 +347,16 @@
"schema": { "schema": {
"type": "string" "type": "string"
} }
},
{
"name": "onlyAccountedPayments",
"in": "query",
"required": false,
"description": "If default or true, only returns payments which are accounted (in Bitcoin, this mean not returning RBF'd or double spent payments)",
"schema": {
"type": "boolean",
"default": true
}
} }
], ],
"description": "View information about the specified invoice's payment methods", "description": "View information about the specified invoice's payment methods",