diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index 587de9a5a..7aaac48db 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -320,7 +320,7 @@ namespace BTCPayServer.Tests await TestUtils.EventuallyAsync(async () => { var invoice = await invoiceRepository.GetInvoice(invoiceId); - var payments = invoice.GetPayments(); + var payments = invoice.GetPayments(false); Assert.Equal(2, payments.Count); var originalPayment = payments[0]; var coinjoinPayment = payments[1]; @@ -1088,7 +1088,7 @@ retry: { var invoiceEntity = await tester.PayTester.GetService().GetInvoice(invoice7.Id); 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); }); ////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen); @@ -1117,8 +1117,8 @@ retry: { var invoiceEntity = await tester.PayTester.GetService().GetInvoice(invoice7.Id); Assert.Equal(InvoiceStatusLegacy.New, invoiceEntity.Status); - Assert.True(invoiceEntity.GetPayments().All(p => !p.Accounted)); - ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData().First().PayjoinInformation.ContributedOutPoints[0]; + Assert.True(invoiceEntity.GetPayments(false).All(p => !p.Accounted)); + ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(false).First().PayjoinInformation.ContributedOutPoints[0]; }); var payjoinRepository = tester.PayTester.GetService(); // The outpoint should now be available for next pj selection diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index e77ac21d3..3e538ddf3 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1639,8 +1639,8 @@ namespace BTCPayServer.Tests { var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id); Assert.Equal(InvoiceStatusLegacy.New, i.Status); - Assert.Single(i.GetPayments()); - Assert.False(i.GetPayments().First().Accounted); + Assert.Single(i.GetPayments(false)); + Assert.False(i.GetPayments(false).First().Accounted); }); Logs.Tester.LogInformation( @@ -1672,8 +1672,8 @@ namespace BTCPayServer.Tests await TestUtils.EventuallyAsync(async () => { var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id); - var btcPayments = invoiceEntity.GetAllBitcoinPaymentData().ToArray(); - var payments = invoiceEntity.GetPayments().ToArray(); + var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(false).ToArray(); + var payments = invoiceEntity.GetPayments(false).ToArray(); Assert.Equal(tx1, btcPayments[0].Outpoint.Hash); Assert.False(payments[0].Accounted); Assert.Equal(tx1Bump, payments[1].Outpoint.Hash); diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index a2b1b4262..a78c3110f 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -283,7 +283,7 @@ namespace BTCPayServer.Controllers.GreenField [Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")] - public async Task GetInvoicePaymentMethods(string storeId, string invoiceId) + public async Task GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true) { var store = HttpContext.GetStoreData(); if (store == null) @@ -297,7 +297,7 @@ namespace BTCPayServer.Controllers.GreenField return InvoiceNotFound(); } - return Ok(ToPaymentMethodModels(invoice)); + return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments)); } [Authorize(Policy = Policies.CanViewInvoices, @@ -336,14 +336,14 @@ namespace BTCPayServer.Controllers.GreenField 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( method => { var accounting = method.Calculate(); var details = method.GetPaymentMethodDetails(); - var payments = method.ParentEntity.GetPayments().Where(paymentEntity => + var payments = method.ParentEntity.GetPayments(includeAccountedPaymentOnly).Where(paymentEntity => paymentEntity.GetPaymentMethodId() == method.GetId()); return new InvoicePaymentMethodDataModel() diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 24b466674..49ead84b1 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -359,7 +359,7 @@ namespace BTCPayServer.Controllers return new InvoiceDetailsModel { Archived = invoice.Archived, - Payments = invoice.GetPayments(), + Payments = invoice.GetPayments(false), CryptoPayments = invoice.GetPaymentMethods().Select( data => { @@ -561,7 +561,7 @@ namespace BTCPayServer.Controllers Status = invoice.StatusString, #pragma warning restore CS0618 // Type or member is obsolete 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, AvailableCryptos = invoice.GetPaymentMethods() .Where(i => i.Network != null) diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 731a2d3ab..d0e14c65c 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -133,9 +133,9 @@ namespace BTCPayServer finally { try { webSocket.Dispose(); } catch { } } } - public static IEnumerable GetAllBitcoinPaymentData(this InvoiceEntity invoice) + public static IEnumerable GetAllBitcoinPaymentData(this InvoiceEntity invoice, bool accountedOnly) { - return invoice.GetPayments() + return invoice.GetPayments(accountedOnly) .Where(p => p.GetPaymentMethodId()?.PaymentType == PaymentTypes.BTCLike) .Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData()) .Where(data => data != null); diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 75168bc18..d07568a14 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -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; context.MarkDirty(); @@ -335,7 +335,7 @@ namespace BTCPayServer.HostedServices { bool extendInvoiceMonitoring = false; var updateConfirmationCountIfNeeded = invoice - .GetPayments() + .GetPayments(false) .Select>(async payment => { var paymentData = payment.GetCryptoPaymentData(); diff --git a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs index 8b44bcc27..d71286625 100644 --- a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs +++ b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs @@ -46,7 +46,7 @@ namespace BTCPayServer.HostedServices 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 && pData.PayjoinInformation?.CoinjoinTransactionHash == transactionId)) { diff --git a/BTCPayServer/PaymentRequest/PaymentRequestService.cs b/BTCPayServer/PaymentRequest/PaymentRequestService.cs index 5c1c5302c..bd248ca53 100644 --- a/BTCPayServer/PaymentRequest/PaymentRequestService.cs +++ b/BTCPayServer/PaymentRequest/PaymentRequestService.cs @@ -111,8 +111,7 @@ namespace BTCPayServer.PaymentRequest State = state, StateFormatted = state.ToString(), Payments = entity - .GetPayments() - .Where(p => p.Accounted) + .GetPayments(true) .Select(paymentEntity => { var paymentData = paymentEntity.GetCryptoPaymentData(); diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 0604924a9..ef4d64a14 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -157,7 +157,7 @@ namespace BTCPayServer.Payments.Bitcoin output.matchedOutput.Value, output.outPoint, 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) { var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network); @@ -220,7 +220,7 @@ namespace BTCPayServer.Payments.Bitcoin { List updatedPaymentEntities = new List(); - var transactions = await wallet.GetTransactions(invoice.GetAllBitcoinPaymentData() + var transactions = await wallet.GetTransactions(invoice.GetAllBitcoinPaymentData(false) .Select(p => p.Outpoint.Hash) .ToArray(), true); bool? originalPJBroadcasted = null; @@ -228,7 +228,7 @@ namespace BTCPayServer.Payments.Bitcoin bool cjPJBroadcasted = false; PayjoinInformation payjoinInformation = null; var paymentEntitiesByPrevOut = new Dictionary(); - foreach (var payment in invoice.GetPayments(wallet.Network)) + foreach (var payment in invoice.GetPayments(wallet.Network, false)) { if (payment.GetPaymentMethodId()?.PaymentType != PaymentTypes.BTCLike) continue; @@ -347,7 +347,7 @@ namespace BTCPayServer.Payments.Bitcoin var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true); if (invoice == null) 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); if (strategy == null) continue; diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs index 02278bb89..b580a79fb 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs @@ -300,7 +300,7 @@ namespace BTCPayServer.Payments.PayJoin paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); paymentAddressIndex = paymentDetails.KeyPath; - if (invoice.GetAllBitcoinPaymentData().Any()) + if (invoice.GetAllBitcoinPaymentData(false).Any()) { ctx.DoNotBroadcast(); return UnprocessableEntity(CreatePayjoinError("already-paid", diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs index f470460bd..a96a5908e 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs @@ -242,7 +242,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services .Select(entity => ( Invoice: entity, 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(), Invoice: entity)) )).Where(tuple => tuple.PaymentMethodDetails?.GetPaymentMethodDetails()?.Activated is true).ToList(); diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs index 38c9ae6fb..2b946abdd 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs @@ -372,7 +372,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services private IEnumerable GetAllMoneroLikePayments(InvoiceEntity invoice, string cryptoCode) { - return invoice.GetPayments() + return invoice.GetPayments(false) .Where(p => p.GetPaymentMethodId() == new PaymentMethodId(cryptoCode, MoneroPaymentType.Instance)); } } diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index 431d26d97..5681d848d 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -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 // then mark the invoice as complete - var payments = p.GetPayments(); + var payments = p.GetPayments(true); if (payments.Count == 0 && p.ExceptionStatus == InvoiceExceptionStatus.Marked && p.Status == InvoiceStatusLegacy.Complete) diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index dfa7e97ce..f0a65f80f 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -59,11 +59,8 @@ namespace BTCPayServer.Services.Invoices.Export var currency = Currencies.GetNumberFormatInfo(invoice.Currency, true); var invoiceDue = invoice.Price; // 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 pdata = payment.GetCryptoPaymentData(); diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 09300f28d..13bbc69ec 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -326,17 +326,17 @@ namespace BTCPayServer.Services.Invoices public List Payments { get; set; } #pragma warning disable CS0618 - public List GetPayments() + public List GetPayments(bool accountedOnly) { - return Payments?.Where(entity => entity.GetPaymentMethodId() != null).ToList() ?? new List(); + return Payments?.Where(entity => entity.GetPaymentMethodId() != null && (!accountedOnly || entity.Accounted)).ToList() ?? new List(); } - public List GetPayments(string cryptoCode) + public List GetPayments(string cryptoCode, bool accountedOnly) { - return GetPayments().Where(p => p.CryptoCode == cryptoCode).ToList(); + return GetPayments(accountedOnly).Where(p => p.CryptoCode == cryptoCode).ToList(); } - public List GetPayments(BTCPayNetworkBase network) + public List GetPayments(BTCPayNetworkBase network, bool accountedOnly) { - return GetPayments(network.CryptoCode); + return GetPayments(network.CryptoCode, accountedOnly); } #pragma warning restore CS0618 public bool Refundable { get; set; } @@ -449,7 +449,7 @@ namespace BTCPayServer.Services.Invoices var paymentId = info.GetId(); 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(); return new InvoicePaymentInfo() @@ -980,8 +980,8 @@ namespace BTCPayServer.Services.Invoices bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision); int txRequired = 0; - _ = ParentEntity.GetPayments() - .Where(p => p.Accounted && paymentPredicate(p)) + _ = ParentEntity.GetPayments(true) + .Where(p => paymentPredicate(p)) .OrderBy(p => p.ReceivedTime) .Select(_ => { diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index 07fd66e3c..85583c506 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -347,6 +347,16 @@ "schema": { "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",