diff --git a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs index 87f3215db..ba52577ae 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Invoices.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Invoices.cs @@ -85,5 +85,13 @@ namespace BTCPayServer.Client method: HttpMethod.Post), token); return await HandleResponse(response); } + + public virtual async Task ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod, CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate", + method: HttpMethod.Post), token); + await HandleResponse(response); + } } } diff --git a/BTCPayServer.Client/Models/InvoicePaymentMethodDataModel.cs b/BTCPayServer.Client/Models/InvoicePaymentMethodDataModel.cs index 94a3df6fb..fa07b4e09 100644 --- a/BTCPayServer.Client/Models/InvoicePaymentMethodDataModel.cs +++ b/BTCPayServer.Client/Models/InvoicePaymentMethodDataModel.cs @@ -8,6 +8,7 @@ namespace BTCPayServer.Client.Models { public class InvoicePaymentMethodDataModel { + public bool Activated { get; set; } public string Destination { get; set; } public string PaymentLink { get; set; } diff --git a/BTCPayServer.Client/Models/StoreBaseData.cs b/BTCPayServer.Client/Models/StoreBaseData.cs index fb14f096b..ff4b9be62 100644 --- a/BTCPayServer.Client/Models/StoreBaseData.cs +++ b/BTCPayServer.Client/Models/StoreBaseData.cs @@ -35,6 +35,7 @@ namespace BTCPayServer.Client.Models public bool LightningAmountInSatoshi { get; set; } public bool LightningPrivateRouteHints { get; set; } public bool OnChainWithLnInvoiceFallback { get; set; } + public bool LazyPaymentMethods { get; set; } public bool RedirectAutomatically { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] @@ -53,8 +54,6 @@ namespace BTCPayServer.Client.Models public string HtmlTitle { get; set; } - - [JsonConverter(typeof(StringEnumConverter))] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never; diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index ec2624c6b..f38559f89 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1093,6 +1093,24 @@ namespace BTCPayServer.Tests { Assert.Equal("pt-PT", langs.FindBestMatch(match).Code); } + + //payment method activation tests + var store = await client.GetStore(user.StoreId); + Assert.False(store.LazyPaymentMethods); + store.LazyPaymentMethods = true; + store = await client.UpdateStore(store.Id, + JObject.FromObject(store).ToObject()); + Assert.True(store.LazyPaymentMethods); + + invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() {Amount = 1, Currency = "USD"}); + paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id); + Assert.Single(paymentMethods); + Assert.False(paymentMethods.First().Activated); + await client.ActivateInvoicePaymentMethod(user.StoreId, invoice.Id, + paymentMethods.First().PaymentMethod); + paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id); + Assert.Single(paymentMethods); + Assert.True(paymentMethods.First().Activated); } } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 945f7e08c..2c81b82ec 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -11,6 +11,7 @@ using BTCPayServer.Services.Wallets; using BTCPayServer.Tests.Logging; using BTCPayServer.Views.Manage; using BTCPayServer.Views.Server; +using BTCPayServer.Views.Stores; using BTCPayServer.Views.Wallets; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; diff --git a/BTCPayServer/Controllers/GreenField/InvoiceController.cs b/BTCPayServer/Controllers/GreenField/InvoiceController.cs index 374f4ed0a..e1c5e702f 100644 --- a/BTCPayServer/Controllers/GreenField/InvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/InvoiceController.cs @@ -25,14 +25,22 @@ namespace BTCPayServer.Controllers.GreenField private readonly InvoiceController _invoiceController; private readonly InvoiceRepository _invoiceRepository; private readonly LinkGenerator _linkGenerator; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly EventAggregator _eventAggregator; + private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; public LanguageService LanguageService { get; } - public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository, LinkGenerator linkGenerator, LanguageService languageService) + public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository, + LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider, + EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary) { _invoiceController = invoiceController; _invoiceRepository = invoiceRepository; _linkGenerator = linkGenerator; + _btcPayNetworkProvider = btcPayNetworkProvider; + _eventAggregator = eventAggregator; + _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; LanguageService = languageService; } @@ -268,6 +276,32 @@ namespace BTCPayServer.Controllers.GreenField return Ok(ToPaymentMethodModels(invoice)); } + + [Authorize(Policy = Policies.CanViewInvoices, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate")] + public async Task ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return NotFound(); + } + + var invoice = await _invoiceRepository.GetInvoice(invoiceId, true); + if (invoice?.StoreId != store.Id) + { + return NotFound(); + } + + if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId)) + { + await _invoiceRepository.ActivateInvoicePaymentMethod(_eventAggregator, _btcPayNetworkProvider, + _paymentMethodHandlerDictionary, store, invoice, paymentMethodId); + return Ok(); + } + return BadRequest(); + } private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity) { @@ -281,6 +315,7 @@ namespace BTCPayServer.Controllers.GreenField return new InvoicePaymentMethodDataModel() { + Activated = details.Activated, PaymentMethod = method.GetId().ToStringNormalized(), Destination = details.GetPaymentDestination(), Rate = method.Rate, diff --git a/BTCPayServer/Controllers/GreenField/StoresController.cs b/BTCPayServer/Controllers/GreenField/StoresController.cs index fbb04bdd4..e3f98c850 100644 --- a/BTCPayServer/Controllers/GreenField/StoresController.cs +++ b/BTCPayServer/Controllers/GreenField/StoresController.cs @@ -131,6 +131,7 @@ namespace BTCPayServer.Controllers.GreenField LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints, OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback, RedirectAutomatically = storeBlob.RedirectAutomatically, + LazyPaymentMethods = storeBlob.LazyPaymentMethods, ShowRecommendedFee = storeBlob.ShowRecommendedFee, RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget, DefaultLang = storeBlob.DefaultLang, @@ -167,6 +168,7 @@ namespace BTCPayServer.Controllers.GreenField blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi; blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints; blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback; + blob.LazyPaymentMethods = restModel.LazyPaymentMethods; blob.RedirectAutomatically = restModel.RedirectAutomatically; blob.ShowRecommendedFee = restModel.ShowRecommendedFee; blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget; diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 090dccac3..c21a3cfc3 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -14,6 +14,7 @@ using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Filters; +using BTCPayServer.Logging; using BTCPayServer.HostedServices; using BTCPayServer.Models; using BTCPayServer.Models.InvoicingModels; @@ -503,9 +504,9 @@ namespace BTCPayServer.Controllers { if (!isDefaultPaymentId) return null; - var paymentMethodTemp = invoice.GetPaymentMethods() - .Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode) - .FirstOrDefault(); + var paymentMethodTemp = invoice + .GetPaymentMethods() + .FirstOrDefault(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode); if (paymentMethodTemp == null) paymentMethodTemp = invoice.GetPaymentMethods().First(); network = paymentMethodTemp.Network; @@ -514,6 +515,12 @@ namespace BTCPayServer.Controllers var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails(); + if (!paymentMethodDetails.Activated) + { + await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider, + _paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId()); + return await GetInvoiceModel(invoiceId, paymentMethodId, lang); + } var dto = invoice.EntityToDTO(); var storeBlob = store.GetStoreBlob(); var accounting = paymentMethod.Calculate(); @@ -529,6 +536,7 @@ namespace BTCPayServer.Controllers var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits; var model = new PaymentModel() { + Activated = paymentMethodDetails.Activated, CryptoCode = network.CryptoCode, RootPath = this.Request.PathBase.Value.WithTrailingSlash(), OrderId = invoice.Metadata.OrderId, diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 4871c65ac..c63041d5f 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -44,12 +44,10 @@ namespace BTCPayServer.Controllers private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; private readonly ApplicationDbContextFactory _dbContextFactory; private readonly PullPaymentHostedService _paymentHostedService; - readonly IServiceProvider _ServiceProvider; public WebhookNotificationManager WebhookNotificationManager { get; } public InvoiceController( - IServiceProvider serviceProvider, InvoiceRepository invoiceRepository, CurrencyNameTable currencyNameTable, UserManager userManager, @@ -63,7 +61,6 @@ namespace BTCPayServer.Controllers PullPaymentHostedService paymentHostedService, WebhookNotificationManager webhookNotificationManager) { - _ServiceProvider = serviceProvider; _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); @@ -321,7 +318,16 @@ namespace BTCPayServer.Controllers { var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:"; var storeBlob = store.GetStoreBlob(); - var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network); + + object preparePayment; + if (storeBlob.LazyPaymentMethods) + { + preparePayment = null; + } + else + { + preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network); + } var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)]; if (rate.BidAsk == null) { diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 52b870fd9..5b26530ef 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -406,6 +406,7 @@ namespace BTCPayServer.Controllers vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; + vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods; vm.RedirectAutomatically = storeBlob.RedirectAutomatically; vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee; vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget; @@ -477,6 +478,7 @@ namespace BTCPayServer.Controllers }).ToList(); blob.RequiresRefundEmail = model.RequiresRefundEmail; + blob.LazyPaymentMethods = model.LazyPaymentMethods; blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints; blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback; diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs index 2a6ac0534..ea40ea7e3 100644 --- a/BTCPayServer/Data/StoreBlob.cs +++ b/BTCPayServer/Data/StoreBlob.cs @@ -38,6 +38,7 @@ namespace BTCPayServer.Data public bool LightningAmountInSatoshi { get; set; } public bool LightningPrivateRouteHints { get; set; } public bool OnChainWithLnInvoiceFallback { get; set; } + public bool LazyPaymentMethods { get; set; } public bool RedirectAutomatically { get; set; } public bool ShowRecommendedFee { get; set; } public int RecommendedFeeBlockTarget { get; set; } diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 3f57e2a6d..d193d4ce6 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -109,7 +109,7 @@ namespace BTCPayServer.HostedServices // We keep backward compatibility with bitpay by passing BTC info to the notification // we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked) - var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike)); + var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike) && !string.IsNullOrEmpty(c.Address)); if (btcCryptoInfo != null) { #pragma warning disable CS0618 diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs index 24cf0446d..ebc21c9c7 100644 --- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs +++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs @@ -69,6 +69,7 @@ namespace BTCPayServer.Models.InvoicingModels public string RootPath { get; set; } public decimal CoinSwitchAmountMarkupPercentage { get; set; } public bool RedirectAutomatically { get; set; } + public bool Activated { get; set; } public string InvoiceCurrency { get; set; } } } diff --git a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs index 7a8648101..928bbf5a8 100644 --- a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using BTCPayServer.Data; using BTCPayServer.Payments; using BTCPayServer.Services; using Microsoft.AspNetCore.Mvc.Rendering; @@ -45,6 +44,9 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "Include lightning invoice fallback to on-chain BIP21 payment url")] public bool OnChainWithLnInvoiceFallback { get; set; } + [Display(Name = "Only enable the payment method after user explicitly chooses it")] + public bool LazyPaymentMethods { get; set; } + [Display(Name = "Redirect invoice to redirect url automatically after paid")] public bool RedirectAutomatically { get; set; } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs index cbd7cab9c..28b522d2c 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs @@ -23,7 +23,7 @@ namespace BTCPayServer.Payments.Bitcoin public decimal GetFeeRate() { - return FeeRate.SatoshiPerByte; + return FeeRate?.SatoshiPerByte ?? 0; } public void SetPaymentDetails(IPaymentMethodDetails newPaymentMethodDetails) @@ -31,6 +31,7 @@ namespace BTCPayServer.Payments.Bitcoin DepositAddress = newPaymentMethodDetails.GetPaymentDestination(); KeyPath = (newPaymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.KeyPath; } + public bool Activated { get; set; } = true; public NetworkFeeMode NetworkFeeMode { get; set; } FeeRate _NetworkFeeRate; diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index d4ce9ad3d..9117ba979 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -64,16 +64,24 @@ namespace BTCPayServer.Payments.Bitcoin model.PaymentMethodName = GetPaymentMethodName(network); var lightningFallback = ""; - if (network.SupportLightning && storeBlob.OnChainWithLnInvoiceFallback) + if (model.Activated && network.SupportLightning && storeBlob.OnChainWithLnInvoiceFallback) { var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a => a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike)); - if (!String.IsNullOrEmpty(lightningInfo?.PaymentUrls?.BOLT11)) + if (!string.IsNullOrEmpty(lightningInfo?.PaymentUrls?.BOLT11)) lightningFallback = "&" + lightningInfo.PaymentUrls.BOLT11.Replace("lightning:", "lightning=", StringComparison.OrdinalIgnoreCase); } - model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21 + lightningFallback; - model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; + if (model.Activated) + { + model.InvoiceBitcoinUrl = (cryptoInfo.PaymentUrls?.BIP21 ?? "") + lightningFallback; + model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; + } + else + { + model.InvoiceBitcoinUrl = ""; + model.InvoiceBitcoinUrlQR = ""; + } // Most wallets still don't support BITCOIN: schema, so we're leaving this for better days // Ref: https://github.com/btcpayserver/btcpayserver/pull/2060#issuecomment-723828348 @@ -145,12 +153,19 @@ namespace BTCPayServer.Payments.Bitcoin DerivationSchemeSettings supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject) { + if (preparePaymentObject is null) + { + return new BitcoinLikeOnChainPaymentMethod() + { + Activated = false + }; + } if (!_ExplorerProvider.IsAvailable(network)) throw new PaymentMethodUnavailableException($"Full node not available"); var prepare = (Prepare)preparePaymentObject; var onchainMethod = new BitcoinLikeOnChainPaymentMethod(); var blob = store.GetStoreBlob(); - + onchainMethod.Activated = true; // TODO: this needs to be refactored to move this logic into BitcoinLikeOnChainPaymentMethod // This is likely a constructor code onchainMethod.NetworkFeeMode = blob.NetworkFeeMode; diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 992d3700c..0604924a9 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -352,7 +352,6 @@ namespace BTCPayServer.Payments.Bitcoin if (strategy == null) continue; var cryptoId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); - var paymentMethod = invoice.GetPaymentMethod(cryptoId).GetPaymentMethodDetails() as BitcoinLikeOnChainPaymentMethod; if (!invoice.Support(cryptoId)) continue; @@ -403,6 +402,7 @@ namespace BTCPayServer.Payments.Bitcoin var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike); if (paymentMethod != null && paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc && + btc.Activated && btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.ScriptPubKey && paymentMethod.Calculate().Due > Money.Zero) { diff --git a/BTCPayServer/Payments/IPaymentMethodDetails.cs b/BTCPayServer/Payments/IPaymentMethodDetails.cs index 865ff7fd5..85706e339 100644 --- a/BTCPayServer/Payments/IPaymentMethodDetails.cs +++ b/BTCPayServer/Payments/IPaymentMethodDetails.cs @@ -16,5 +16,7 @@ namespace BTCPayServer.Payments /// /// decimal GetNextNetworkFee(); + + bool Activated {get;set;} } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index b994cbf7b..d710c76e8 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -54,6 +54,13 @@ namespace BTCPayServer.Payments.Lightning LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject) { + if (preparePaymentObject is null) + { + return new LightningLikePaymentMethodDetails() + { + Activated = false + }; + } //direct casting to (BTCPayNetwork) is fixed in other pull requests with better generic interfacing for handlers var storeBlob = store.GetStoreBlob(); var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network); @@ -99,6 +106,7 @@ namespace BTCPayServer.Payments.Lightning var nodeInfo = await test; return new LightningLikePaymentMethodDetails { + Activated = true, BOLT11 = lightningInvoice.BOLT11, InvoiceId = lightningInvoice.Id, NodeInfo = nodeInfo.ToString() @@ -191,8 +199,8 @@ namespace BTCPayServer.Payments.Lightning var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); var network = _networkProvider.GetNetwork(model.CryptoCode); model.PaymentMethodName = GetPaymentMethodName(network); - model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BOLT11; - model.InvoiceBitcoinUrlQR = $"lightning:{cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant().Substring("LIGHTNING:".Length)}"; + model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls?.BOLT11; + model.InvoiceBitcoinUrlQR = $"lightning:{cryptoInfo.PaymentUrls?.BOLT11?.ToUpperInvariant()?.Substring("LIGHTNING:".Length)}"; model.PeerInfo = ((LightningLikePaymentMethodDetails) paymentMethod.GetPaymentMethodDetails()).NodeInfo; if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC") @@ -238,5 +246,12 @@ namespace BTCPayServer.Payments.Lightning { return $"{network.DisplayName} (Lightning)"; } + + public override object PreparePayment(LightningSupportedPaymentMethod supportedPaymentMethod, StoreData store, + BTCPayNetworkBase network) + { + // pass a non null obj, so that if lazy payment feature is used, it has a marker to trigger activation + return new { }; + } } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs index 749728205..cd94537c0 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs @@ -25,5 +25,6 @@ namespace BTCPayServer.Payments.Lightning { return 0.0m; } + public bool Activated { get; set; } } } diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index af9885001..9bbc74059 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -112,7 +112,7 @@ namespace BTCPayServer.Payments.Lightning .Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike)) { var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails; - if (lightningMethod == null) + if (lightningMethod == null || !lightningMethod.Activated) continue; var lightningSupportedMethod = invoice.GetSupportedPaymentMethod() .FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode); diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs index 3438e8c38..121155b5f 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs @@ -262,7 +262,7 @@ namespace BTCPayServer.Payments.PayJoin var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; - if (paymentDetails is null || !paymentDetails.PayjoinEnabled) + if (paymentDetails is null || !paymentDetails.PayjoinEnabled || !paymentDetails.Activated) continue; if (invoice.GetAllBitcoinPaymentData().Any()) { diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index 8861ea3ec..cd5a81610 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -70,6 +70,10 @@ namespace BTCPayServer.Payments public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) { + if (!paymentMethodDetails.Activated) + { + return string.Empty; + } var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue); if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true && serverUri != null) diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs index 2e63f6773..5d34b4d80 100644 --- a/BTCPayServer/Payments/PaymentTypes.Lightning.cs +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -52,6 +52,10 @@ namespace BTCPayServer.Payments public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) { + if (!paymentMethodDetails.Activated) + { + return string.Empty; + } var lnInvoiceTrimmedOfScheme = paymentMethodDetails.GetPaymentDestination().ToLowerInvariant() .Replace("lightning:", "", StringComparison.InvariantCultureIgnoreCase); diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikeOnChainPaymentMethodDetails.cs b/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikeOnChainPaymentMethodDetails.cs index 3a7bb0b2d..cf87b45fc 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikeOnChainPaymentMethodDetails.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikeOnChainPaymentMethodDetails.cs @@ -34,6 +34,8 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments { DepositAddress = newPaymentDestination; } + + public bool Activated { get; set; } public long Index { get; set; } public string XPub { get; set; } public string DepositAddress { get; set; } diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikePaymentMethodHandler.cs b/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikePaymentMethodHandler.cs index 786148672..c230bc185 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikePaymentMethodHandler.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikePaymentMethodHandler.cs @@ -35,6 +35,13 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments EthereumSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, EthereumBTCPayNetwork network, object preparePaymentObject) { + if (preparePaymentObject is null) + { + return new EthereumLikeOnChainPaymentMethodDetails() + { + Activated = false + }; + } if (!_ethereumService.IsAvailable(network.CryptoCode, out var error)) throw new PaymentMethodUnavailableException(error??$"Not configured yet"); var invoice = paymentMethod.ParentEntity; @@ -47,7 +54,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments return new EthereumLikeOnChainPaymentMethodDetails() { - DepositAddress = address.Address, Index = address.Index, XPub = address.XPub + DepositAddress = address.Address, Index = address.Index, XPub = address.XPub, Activated = true }; } @@ -79,7 +86,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments model.PaymentMethodName = GetPaymentMethodName(network); model.CryptoImage = GetCryptoImage(network); model.InvoiceBitcoinUrl = ""; - model.InvoiceBitcoinUrlQR = cryptoInfo.Address; + model.InvoiceBitcoinUrlQR = cryptoInfo.Address ?? ""; } public override string GetCryptoImage(PaymentMethodId paymentMethodId) diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs index e82c7bce3..f470460bd 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Services/EthereumWatcher.cs @@ -217,7 +217,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() {InvoiceId = invoiceIds}); invoices = invoices - .Where(entity => PaymentMethods.Any(id => entity.GetPaymentMethod(id) != null)) + .Where(entity => PaymentMethods.Any(id => entity.GetPaymentMethod(id)?.GetPaymentMethodDetails()?.Activated is true)) .ToArray(); await UpdatePaymentStates(invoices, cancellationToken); @@ -245,7 +245,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services ExistingPayments: entity.GetPayments(network).Select(paymentEntity => (Payment: paymentEntity, PaymentData: (EthereumLikePaymentData)paymentEntity.GetCryptoPaymentData(), Invoice: entity)) - )).Where(tuple => tuple.PaymentMethodDetails != null).ToList(); + )).Where(tuple => tuple.PaymentMethodDetails?.GetPaymentMethodDetails()?.Activated is true).ToList(); var existingPaymentData = expandedInvoices.SelectMany(tuple => tuple.ExistingPayments.Where(valueTuple => valueTuple.Payment.Accounted)).ToList(); diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs index c70ec910b..41227780c 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs @@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments { return 0.0m; } - + public bool Activated { get; set; } = true; public long AccountIndex { get; set; } public long AddressIndex { get; set; } public string DepositAddress { get; set; } diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs index 8e66f646f..eb91ebb2e 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs @@ -34,6 +34,14 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments public override async Task CreatePaymentMethodDetails(InvoiceLogs logs, MoneroSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, MoneroLikeSpecificBtcPayNetwork network, object preparePaymentObject) { + + if (preparePaymentObject is null) + { + return new MoneroLikeOnChainPaymentMethodDetails() + { + Activated = false + }; + } if (!_moneroRpcProvider.IsAvailable(network.CryptoCode)) throw new PaymentMethodUnavailableException($"Node or wallet not available"); @@ -49,7 +57,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments NextNetworkFee = MoneroMoney.Convert(feeRatePerByte * 100), AccountIndex = supportedPaymentMethod.AccountIndex, AddressIndex = address.AddressIndex, - DepositAddress = address.Address + DepositAddress = address.Address, + Activated = true }; } @@ -77,15 +86,22 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments StoreBlob storeBlob, IPaymentMethod paymentMethod) { var paymentMethodId = paymentMethod.GetId(); - var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); var network = _networkProvider.GetNetwork(model.CryptoCode); model.PaymentMethodName = GetPaymentMethodName(network); model.CryptoImage = GetCryptoImage(network); - model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network, new MoneroLikeOnChainPaymentMethodDetails() + if (model.Activated) { - DepositAddress = cryptoInfo.Address - }, cryptoInfo.Due, null); - model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; + var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); + model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network, + new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due, + null); + model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; + } + else + { + model.InvoiceBitcoinUrl = ""; + model.InvoiceBitcoinUrlQR = ""; + } } public override string GetCryptoImage(PaymentMethodId paymentMethodId) { diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs index cdfe86260..04c3d66f5 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs @@ -51,8 +51,9 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) { - return - $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"; + return paymentMethodDetails.Activated + ? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}" + : string.Empty; } public override string InvoiceViewPaymentPartialName { get; } = "Monero/ViewMoneroLikePaymentData"; diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs index 2fa9a5de0..38c9ae6fb 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs @@ -121,6 +121,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services 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) { @@ -363,7 +364,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services } var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() { InvoiceId = invoiceIds }); - invoices = invoices.Where(entity => entity.GetPaymentMethod(new PaymentMethodId(cryptoCode, MoneroPaymentType.Instance)) != null).ToArray(); + invoices = invoices.Where(entity => entity.GetPaymentMethod(new PaymentMethodId(cryptoCode, MoneroPaymentType.Instance)) + ?.GetPaymentMethodDetails().Activated is true).ToArray(); _logger.LogInformation($"Updating pending payments for {cryptoCode} in {string.Join(',', invoiceIds)}"); await UpdatePaymentStates(cryptoCode, invoices); } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 350c73a09..45e3c3797 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -185,7 +185,7 @@ namespace BTCPayServer.Services.Invoices foreach (var strat in strategies.Properties()) { var paymentMethodId = PaymentMethodId.Parse(strat.Name); - var network = Networks.GetNetwork(paymentMethodId.CryptoCode); + var network = Networks.GetNetwork(paymentMethodId.CryptoCode); if (network != null) { if (network == Networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike) @@ -374,7 +374,7 @@ namespace BTCPayServer.Services.Invoices }).ToList(); - if (paymentId.PaymentType == PaymentTypes.LightningLike) + if (details?.Activated is true && paymentId.PaymentType == PaymentTypes.LightningLike) { cryptoInfo.PaymentUrls = new InvoicePaymentUrls() { @@ -382,7 +382,7 @@ namespace BTCPayServer.Services.Invoices ServerUrl) }; } - else if (paymentId.PaymentType == PaymentTypes.BTCLike) + else if (details?.Activated is true && paymentId.PaymentType == PaymentTypes.BTCLike) { var minerInfo = new MinerFeeInfo(); minerInfo.TotalFee = accounting.NetworkFee.Satoshi; @@ -936,10 +936,7 @@ namespace BTCPayServer.Services.Invoices private decimal GetTxFee() { - var method = GetPaymentMethodDetails(); - if (method == null) - return 0.0m; - return method.GetNextNetworkFee(); + return GetPaymentMethodDetails()?.GetNextNetworkFee()?? 0m; } } diff --git a/BTCPayServer/Services/Invoices/InvoiceExtensions.cs b/BTCPayServer/Services/Invoices/InvoiceExtensions.cs new file mode 100644 index 000000000..118540a66 --- /dev/null +++ b/BTCPayServer/Services/Invoices/InvoiceExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Logging; +using BTCPayServer.Payments; + +namespace BTCPayServer.Services.Invoices +{ + public static class InvoiceExtensions + { + + public static async Task ActivateInvoicePaymentMethod(this InvoiceRepository invoiceRepository, + EventAggregator eventAggregator, BTCPayNetworkProvider btcPayNetworkProvider, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, + StoreData store,InvoiceEntity invoice, PaymentMethodId paymentMethodId) + { + var eligibleMethodToActivate = invoice.GetPaymentMethod(paymentMethodId); + if (!eligibleMethodToActivate.GetPaymentMethodDetails().Activated) + { + var payHandler = paymentMethodHandlerDictionary[paymentMethodId]; + var supportPayMethod = invoice.GetSupportedPaymentMethod() + .Single(method => method.PaymentId == paymentMethodId); + var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); + var network = btcPayNetworkProvider.GetNetwork(paymentMethodId.CryptoCode); + var prepare = payHandler.PreparePayment(supportPayMethod, store, network); + InvoiceLogs logs = new InvoiceLogs(); + try + { + logs.Write($"{paymentMethodId}: Activating", InvoiceEventData.EventSeverity.Info); + var newDetails = await + payHandler.CreatePaymentMethodDetails(logs, supportPayMethod, paymentMethod, store, network, + prepare); + eligibleMethodToActivate.SetPaymentMethodDetails(newDetails); + await invoiceRepository.UpdateInvoicePaymentMethod(invoice.Id, eligibleMethodToActivate); + } + catch (PaymentMethodUnavailableException ex) + { + logs.Write($"{paymentMethodId}: Payment method unavailable ({ex.Message})", InvoiceEventData.EventSeverity.Error); + } + catch (Exception ex) + { + logs.Write($"{paymentMethodId}: Unexpected exception ({ex})", InvoiceEventData.EventSeverity.Error); + } + + await invoiceRepository.AddInvoiceLogs(invoice.Id, logs); + eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoice.Id)); + } + } + + } +} diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 45b6536f4..926006a13 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Logging; using NBitcoin; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NLog; using Encoders = NBitcoin.DataEncoders.Encoders; using InvoiceData = BTCPayServer.Data.InvoiceData; @@ -180,8 +181,12 @@ namespace BTCPayServer.Services.Invoices { if (paymentMethod.Network == null) throw new InvalidOperationException("CryptoCode unsupported"); - var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(); - + var details = paymentMethod.GetPaymentMethodDetails(); + if (!details.Activated) + { + continue; + } + var paymentDestination = details.GetPaymentDestination(); string address = GetDestination(paymentMethod); await context.AddressInvoices.AddAsync(new AddressInvoiceData() { @@ -244,7 +249,13 @@ namespace BTCPayServer.Services.Invoices if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike) { var network = (BTCPayNetwork)paymentMethod.Network; - return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).GetDepositAddress(network.NBitcoinNetwork).ScriptPubKey.Hash.ToString(); + var details = + (Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails(); + if (!details.Activated) + { + return null; + } + return details.GetDepositAddress(network.NBitcoinNetwork).ScriptPubKey.Hash.ToString(); } /////////////// return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(); @@ -294,6 +305,39 @@ namespace BTCPayServer.Services.Invoices return true; } + public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod) + { + using (var context = _ContextFactory.CreateContext()) + { + var invoice = await context.Invoices.FindAsync(invoiceId); + if (invoice == null) + return; + var network = paymentMethod.Network; + var invoiceEntity = invoice.GetBlob(_Networks); + var newDetails = paymentMethod.GetPaymentMethodDetails(); + var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId()); + if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated) + { + await context.AddressInvoices.AddAsync(new AddressInvoiceData() + { + InvoiceDataId = invoiceId, + CreatedTime = DateTimeOffset.UtcNow + } + .Set(GetDestination(paymentMethod), paymentMethod.GetId())); + await context.HistoricalAddressInvoices.AddAsync(new HistoricalAddressInvoiceData() + { + InvoiceDataId = invoiceId, + Assigned = DateTimeOffset.UtcNow + }.SetAddress(paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(), network.CryptoCode)); + } + invoiceEntity.SetPaymentMethod(paymentMethod); + invoice.Blob = ToBytes(invoiceEntity, network); + AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination()); + await context.SaveChangesAsync(); + + } + } + public async Task AddPendingInvoiceIfNotPresent(string invoiceId) { using (var context = _ContextFactory.CreateContext()) diff --git a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml index 4cdeaea4b..4c6b5d008 100644 --- a/BTCPayServer/Views/Invoice/Checkout-Body.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout-Body.cshtml @@ -149,7 +149,7 @@ - @@ -184,7 +184,7 @@
- diff --git a/BTCPayServer/Views/Stores/CheckoutExperience.cshtml b/BTCPayServer/Views/Stores/CheckoutExperience.cshtml index f1613a169..29ab74977 100644 --- a/BTCPayServer/Views/Stores/CheckoutExperience.cshtml +++ b/BTCPayServer/Views/Stores/CheckoutExperience.cshtml @@ -80,6 +80,12 @@
+
+
+ + +
+
diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json index a062ae2fb..0c0f16537 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json @@ -542,6 +542,71 @@ } ] } + }, + "/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate": { + "post": { + "tags": [ + "Invoices" + ], + "summary": "Activate Payment Method", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to query", + "schema": { + "type": "string" + } + }, + { + "name": "invoiceId", + "in": "path", + "required": true, + "description": "The invoice to update", + "schema": { + "type": "string" + } + }, + { + "name": "paymentMethod", + "in": "path", + "required": true, + "description": "The payment method to activate", + "schema": { + "type": "string" + } + } + ], + "description": "Activate an invoice payment method (if lazy payments mode is enabled)", + "operationId": "Invoices_ActivatePaymentMethod", + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "A list of errors that occurred when updating the invoice", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to activate the invoice payment method" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canviewinvoices" + ], + "Basic": [] + } + ] + } } }, "components": { @@ -729,7 +794,7 @@ "items": { "type": "string" }, - "description": "A specific set of payment methods to use for this invoice (ie. BTC, BTC-LightningNetwork). By default, select all payment methods activated in the store." + "description": "A specific set of payment methods to use for this invoice (ie. BTC, BTC-LightningNetwork). By default, select all payment methods enabled in the store." }, "expirationMinutes": { "nullable": true, @@ -837,6 +902,10 @@ "$ref": "#/components/schemas/Payment" }, "description": "Payments made with this payment method." + }, + "activated": { + "type": "boolean", + "description": "If the payment method is activated (when lazy payments option is enabled" } } }, diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json index 7484c676e..4ee35ca2d 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json @@ -381,6 +381,11 @@ "default": false, "description": "If true, payjoin will be proposed in the checkout page if possible. ([More information](https://docs.btcpayserver.org/Payjoin/))" }, + "lazyPaymentMethods": { + "type": "boolean", + "default": false, + "description": "If true, payment methods are enabled individually upon user interaction in the invoice" + }, "defaultPaymentMethod": { "type": "string", "example": "BTC",