Make Invoice Create Faster And Fix Gap Limit Issue (#1843)

* Make Invoice Create Faster And Fix Gap Limit Issue

This make address reserve only when user "activate" paymet method in ui. optional setting in store checkout ui.

* Fix swagger documentation around Lazy payment methods

* fix changed code signature

* Add missing GreenField API for activate feature

* Fix checkout experience styling for activate feature

* Fix issue with Checkout activate button

* Make lightning also work with activation

* Make sure PreparePaymentModel is still called on payment handlers even when unactivated

* Make payment  link return empty if not activated

* Add activate payment method method to client and add test

* remove debugger

* add e2e test

* Rearranging lazy payments position in UI to be near dependent Unified QR code

* fix rebase conflicts

* Make lazy payment method mode activate on UI load.

Co-authored-by: Kukks <evilkukka@gmail.com>
Co-authored-by: rockstardev <rockstardev@users.noreply.github.com>
Co-authored-by: Andrew Camilleri <kukks@btcpayserver.org>
This commit is contained in:
xpayserver
2021-04-07 06:08:42 +02:00
committed by GitHub
parent b2c72f1d75
commit 475809b1a0
38 changed files with 377 additions and 50 deletions

View File

@@ -85,5 +85,13 @@ namespace BTCPayServer.Client
method: HttpMethod.Post), token); method: HttpMethod.Post), token);
return await HandleResponse<InvoiceData>(response); return await HandleResponse<InvoiceData>(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);
}
} }
} }

View File

@@ -8,6 +8,7 @@ namespace BTCPayServer.Client.Models
{ {
public class InvoicePaymentMethodDataModel public class InvoicePaymentMethodDataModel
{ {
public bool Activated { get; set; }
public string Destination { get; set; } public string Destination { get; set; }
public string PaymentLink { get; set; } public string PaymentLink { get; set; }

View File

@@ -35,6 +35,7 @@ namespace BTCPayServer.Client.Models
public bool LightningAmountInSatoshi { get; set; } public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; } public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; } public bool OnChainWithLnInvoiceFallback { get; set; }
public bool LazyPaymentMethods { get; set; }
public bool RedirectAutomatically { get; set; } public bool RedirectAutomatically { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
@@ -53,8 +54,6 @@ namespace BTCPayServer.Client.Models
public string HtmlTitle { get; set; } public string HtmlTitle { get; set; }
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never; public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;

View File

@@ -1093,6 +1093,24 @@ namespace BTCPayServer.Tests
{ {
Assert.Equal("pt-PT", langs.FindBestMatch(match).Code); 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<UpdateStoreRequest>());
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);
} }
} }

View File

@@ -11,6 +11,7 @@ using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage; using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server; using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets; using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View File

@@ -25,14 +25,22 @@ namespace BTCPayServer.Controllers.GreenField
private readonly InvoiceController _invoiceController; private readonly InvoiceController _invoiceController;
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly EventAggregator _eventAggregator;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
public LanguageService LanguageService { get; } 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; _invoiceController = invoiceController;
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_btcPayNetworkProvider = btcPayNetworkProvider;
_eventAggregator = eventAggregator;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
LanguageService = languageService; LanguageService = languageService;
} }
@@ -269,6 +277,32 @@ namespace BTCPayServer.Controllers.GreenField
return Ok(ToPaymentMethodModels(invoice)); 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<IActionResult> 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) private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity)
{ {
return entity.GetPaymentMethods().Select( return entity.GetPaymentMethods().Select(
@@ -281,6 +315,7 @@ namespace BTCPayServer.Controllers.GreenField
return new InvoicePaymentMethodDataModel() return new InvoicePaymentMethodDataModel()
{ {
Activated = details.Activated,
PaymentMethod = method.GetId().ToStringNormalized(), PaymentMethod = method.GetId().ToStringNormalized(),
Destination = details.GetPaymentDestination(), Destination = details.GetPaymentDestination(),
Rate = method.Rate, Rate = method.Rate,

View File

@@ -131,6 +131,7 @@ namespace BTCPayServer.Controllers.GreenField
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints, LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback, OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
RedirectAutomatically = storeBlob.RedirectAutomatically, RedirectAutomatically = storeBlob.RedirectAutomatically,
LazyPaymentMethods = storeBlob.LazyPaymentMethods,
ShowRecommendedFee = storeBlob.ShowRecommendedFee, ShowRecommendedFee = storeBlob.ShowRecommendedFee,
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget, RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
DefaultLang = storeBlob.DefaultLang, DefaultLang = storeBlob.DefaultLang,
@@ -167,6 +168,7 @@ namespace BTCPayServer.Controllers.GreenField
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi; blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints; blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback; blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback;
blob.LazyPaymentMethods = restModel.LazyPaymentMethods;
blob.RedirectAutomatically = restModel.RedirectAutomatically; blob.RedirectAutomatically = restModel.RedirectAutomatically;
blob.ShowRecommendedFee = restModel.ShowRecommendedFee; blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget; blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;

View File

@@ -14,6 +14,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Models.InvoicingModels;
@@ -503,9 +504,9 @@ namespace BTCPayServer.Controllers
{ {
if (!isDefaultPaymentId) if (!isDefaultPaymentId)
return null; return null;
var paymentMethodTemp = invoice.GetPaymentMethods() var paymentMethodTemp = invoice
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode) .GetPaymentMethods()
.FirstOrDefault(); .FirstOrDefault(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode);
if (paymentMethodTemp == null) if (paymentMethodTemp == null)
paymentMethodTemp = invoice.GetPaymentMethods().First(); paymentMethodTemp = invoice.GetPaymentMethods().First();
network = paymentMethodTemp.Network; network = paymentMethodTemp.Network;
@@ -514,6 +515,12 @@ namespace BTCPayServer.Controllers
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails(); 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 dto = invoice.EntityToDTO();
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
@@ -529,6 +536,7 @@ namespace BTCPayServer.Controllers
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits; var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
var model = new PaymentModel() var model = new PaymentModel()
{ {
Activated = paymentMethodDetails.Activated,
CryptoCode = network.CryptoCode, CryptoCode = network.CryptoCode,
RootPath = this.Request.PathBase.Value.WithTrailingSlash(), RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
OrderId = invoice.Metadata.OrderId, OrderId = invoice.Metadata.OrderId,

View File

@@ -44,12 +44,10 @@ namespace BTCPayServer.Controllers
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly PullPaymentHostedService _paymentHostedService; private readonly PullPaymentHostedService _paymentHostedService;
readonly IServiceProvider _ServiceProvider;
public WebhookNotificationManager WebhookNotificationManager { get; } public WebhookNotificationManager WebhookNotificationManager { get; }
public InvoiceController( public InvoiceController(
IServiceProvider serviceProvider,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable, CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
@@ -63,7 +61,6 @@ namespace BTCPayServer.Controllers
PullPaymentHostedService paymentHostedService, PullPaymentHostedService paymentHostedService,
WebhookNotificationManager webhookNotificationManager) WebhookNotificationManager webhookNotificationManager)
{ {
_ServiceProvider = serviceProvider;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
@@ -321,7 +318,16 @@ namespace BTCPayServer.Controllers
{ {
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:"; var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
var storeBlob = store.GetStoreBlob(); 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)]; var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)];
if (rate.BidAsk == null) if (rate.BidAsk == null)
{ {

View File

@@ -406,6 +406,7 @@ namespace BTCPayServer.Controllers
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically; vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee; vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee;
vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget; vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget;
@@ -477,6 +478,7 @@ namespace BTCPayServer.Controllers
}).ToList(); }).ToList();
blob.RequiresRefundEmail = model.RequiresRefundEmail; blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LazyPaymentMethods = model.LazyPaymentMethods;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints; blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback; blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;

View File

@@ -38,6 +38,7 @@ namespace BTCPayServer.Data
public bool LightningAmountInSatoshi { get; set; } public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; } public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; } public bool OnChainWithLnInvoiceFallback { get; set; }
public bool LazyPaymentMethods { get; set; }
public bool RedirectAutomatically { get; set; } public bool RedirectAutomatically { get; set; }
public bool ShowRecommendedFee { get; set; } public bool ShowRecommendedFee { get; set; }
public int RecommendedFeeBlockTarget { get; set; } public int RecommendedFeeBlockTarget { get; set; }

View File

@@ -109,7 +109,7 @@ namespace BTCPayServer.HostedServices
// We keep backward compatibility with bitpay by passing BTC info to the notification // 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) // 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) if (btcCryptoInfo != null)
{ {
#pragma warning disable CS0618 #pragma warning disable CS0618

View File

@@ -69,6 +69,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string RootPath { get; set; } public string RootPath { get; set; }
public decimal CoinSwitchAmountMarkupPercentage { get; set; } public decimal CoinSwitchAmountMarkupPercentage { get; set; }
public bool RedirectAutomatically { get; set; } public bool RedirectAutomatically { get; set; }
public bool Activated { get; set; }
public string InvoiceCurrency { get; set; } public string InvoiceCurrency { get; set; }
} }
} }

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Services; using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Rendering; 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")] [Display(Name = "Include lightning invoice fallback to on-chain BIP21 payment url")]
public bool OnChainWithLnInvoiceFallback { get; set; } 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")] [Display(Name = "Redirect invoice to redirect url automatically after paid")]
public bool RedirectAutomatically { get; set; } public bool RedirectAutomatically { get; set; }

View File

@@ -23,7 +23,7 @@ namespace BTCPayServer.Payments.Bitcoin
public decimal GetFeeRate() public decimal GetFeeRate()
{ {
return FeeRate.SatoshiPerByte; return FeeRate?.SatoshiPerByte ?? 0;
} }
public void SetPaymentDetails(IPaymentMethodDetails newPaymentMethodDetails) public void SetPaymentDetails(IPaymentMethodDetails newPaymentMethodDetails)
@@ -31,6 +31,7 @@ namespace BTCPayServer.Payments.Bitcoin
DepositAddress = newPaymentMethodDetails.GetPaymentDestination(); DepositAddress = newPaymentMethodDetails.GetPaymentDestination();
KeyPath = (newPaymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.KeyPath; KeyPath = (newPaymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.KeyPath;
} }
public bool Activated { get; set; } = true;
public NetworkFeeMode NetworkFeeMode { get; set; } public NetworkFeeMode NetworkFeeMode { get; set; }
FeeRate _NetworkFeeRate; FeeRate _NetworkFeeRate;

View File

@@ -64,16 +64,24 @@ namespace BTCPayServer.Payments.Bitcoin
model.PaymentMethodName = GetPaymentMethodName(network); model.PaymentMethodName = GetPaymentMethodName(network);
var lightningFallback = ""; var lightningFallback = "";
if (network.SupportLightning && storeBlob.OnChainWithLnInvoiceFallback) if (model.Activated && network.SupportLightning && storeBlob.OnChainWithLnInvoiceFallback)
{ {
var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a => var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a =>
a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike)); 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); lightningFallback = "&" + lightningInfo.PaymentUrls.BOLT11.Replace("lightning:", "lightning=", StringComparison.OrdinalIgnoreCase);
} }
model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21 + lightningFallback; if (model.Activated)
{
model.InvoiceBitcoinUrl = (cryptoInfo.PaymentUrls?.BIP21 ?? "") + lightningFallback;
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; 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 // 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 // Ref: https://github.com/btcpayserver/btcpayserver/pull/2060#issuecomment-723828348
@@ -145,12 +153,19 @@ namespace BTCPayServer.Payments.Bitcoin
DerivationSchemeSettings supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, DerivationSchemeSettings supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store,
BTCPayNetwork network, object preparePaymentObject) BTCPayNetwork network, object preparePaymentObject)
{ {
if (preparePaymentObject is null)
{
return new BitcoinLikeOnChainPaymentMethod()
{
Activated = false
};
}
if (!_ExplorerProvider.IsAvailable(network)) if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available"); throw new PaymentMethodUnavailableException($"Full node not available");
var prepare = (Prepare)preparePaymentObject; var prepare = (Prepare)preparePaymentObject;
var onchainMethod = new BitcoinLikeOnChainPaymentMethod(); var onchainMethod = new BitcoinLikeOnChainPaymentMethod();
var blob = store.GetStoreBlob(); var blob = store.GetStoreBlob();
onchainMethod.Activated = true;
// TODO: this needs to be refactored to move this logic into BitcoinLikeOnChainPaymentMethod // TODO: this needs to be refactored to move this logic into BitcoinLikeOnChainPaymentMethod
// This is likely a constructor code // This is likely a constructor code
onchainMethod.NetworkFeeMode = blob.NetworkFeeMode; onchainMethod.NetworkFeeMode = blob.NetworkFeeMode;

View File

@@ -352,7 +352,6 @@ namespace BTCPayServer.Payments.Bitcoin
if (strategy == null) if (strategy == null)
continue; continue;
var cryptoId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); var cryptoId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var paymentMethod = invoice.GetPaymentMethod(cryptoId).GetPaymentMethodDetails() as BitcoinLikeOnChainPaymentMethod;
if (!invoice.Support(cryptoId)) if (!invoice.Support(cryptoId))
continue; continue;
@@ -403,6 +402,7 @@ namespace BTCPayServer.Payments.Bitcoin
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike); var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike);
if (paymentMethod != null && if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc && paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
btc.Activated &&
btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.ScriptPubKey && btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.ScriptPubKey &&
paymentMethod.Calculate().Due > Money.Zero) paymentMethod.Calculate().Due > Money.Zero)
{ {

View File

@@ -16,5 +16,7 @@ namespace BTCPayServer.Payments
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
decimal GetNextNetworkFee(); decimal GetNextNetworkFee();
bool Activated {get;set;}
} }
} }

View File

@@ -54,6 +54,13 @@ namespace BTCPayServer.Payments.Lightning
LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store,
BTCPayNetwork network, object preparePaymentObject) 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 //direct casting to (BTCPayNetwork) is fixed in other pull requests with better generic interfacing for handlers
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network); var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network);
@@ -99,6 +106,7 @@ namespace BTCPayServer.Payments.Lightning
var nodeInfo = await test; var nodeInfo = await test;
return new LightningLikePaymentMethodDetails return new LightningLikePaymentMethodDetails
{ {
Activated = true,
BOLT11 = lightningInvoice.BOLT11, BOLT11 = lightningInvoice.BOLT11,
InvoiceId = lightningInvoice.Id, InvoiceId = lightningInvoice.Id,
NodeInfo = nodeInfo.ToString() NodeInfo = nodeInfo.ToString()
@@ -191,8 +199,8 @@ namespace BTCPayServer.Payments.Lightning
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var network = _networkProvider.GetNetwork<BTCPayNetwork>(model.CryptoCode); var network = _networkProvider.GetNetwork<BTCPayNetwork>(model.CryptoCode);
model.PaymentMethodName = GetPaymentMethodName(network); model.PaymentMethodName = GetPaymentMethodName(network);
model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BOLT11; model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls?.BOLT11;
model.InvoiceBitcoinUrlQR = $"lightning:{cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant().Substring("LIGHTNING:".Length)}"; model.InvoiceBitcoinUrlQR = $"lightning:{cryptoInfo.PaymentUrls?.BOLT11?.ToUpperInvariant()?.Substring("LIGHTNING:".Length)}";
model.PeerInfo = ((LightningLikePaymentMethodDetails) paymentMethod.GetPaymentMethodDetails()).NodeInfo; model.PeerInfo = ((LightningLikePaymentMethodDetails) paymentMethod.GetPaymentMethodDetails()).NodeInfo;
if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC") if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC")
@@ -238,5 +246,12 @@ namespace BTCPayServer.Payments.Lightning
{ {
return $"{network.DisplayName} (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 { };
}
} }
} }

View File

@@ -25,5 +25,6 @@ namespace BTCPayServer.Payments.Lightning
{ {
return 0.0m; return 0.0m;
} }
public bool Activated { get; set; }
} }
} }

View File

@@ -112,7 +112,7 @@ namespace BTCPayServer.Payments.Lightning
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike)) .Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
{ {
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails; var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
if (lightningMethod == null) if (lightningMethod == null || !lightningMethod.Activated)
continue; continue;
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>() var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>()
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode); .FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);

View File

@@ -262,7 +262,7 @@ namespace BTCPayServer.Payments.PayJoin
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentDetails = var paymentDetails =
paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (paymentDetails is null || !paymentDetails.PayjoinEnabled) if (paymentDetails is null || !paymentDetails.PayjoinEnabled || !paymentDetails.Activated)
continue; continue;
if (invoice.GetAllBitcoinPaymentData().Any()) if (invoice.GetAllBitcoinPaymentData().Any())
{ {

View File

@@ -70,6 +70,10 @@ namespace BTCPayServer.Payments
public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri) Money cryptoInfoDue, string serverUri)
{ {
if (!paymentMethodDetails.Activated)
{
return string.Empty;
}
var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue); var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue);
if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true && serverUri != null) if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true && serverUri != null)

View File

@@ -52,6 +52,10 @@ namespace BTCPayServer.Payments
public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri) Money cryptoInfoDue, string serverUri)
{ {
if (!paymentMethodDetails.Activated)
{
return string.Empty;
}
var lnInvoiceTrimmedOfScheme = paymentMethodDetails.GetPaymentDestination().ToLowerInvariant() var lnInvoiceTrimmedOfScheme = paymentMethodDetails.GetPaymentDestination().ToLowerInvariant()
.Replace("lightning:", "", StringComparison.InvariantCultureIgnoreCase); .Replace("lightning:", "", StringComparison.InvariantCultureIgnoreCase);

View File

@@ -34,6 +34,8 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
{ {
DepositAddress = newPaymentDestination; DepositAddress = newPaymentDestination;
} }
public bool Activated { get; set; }
public long Index { get; set; } public long Index { get; set; }
public string XPub { get; set; } public string XPub { get; set; }
public string DepositAddress { get; set; } public string DepositAddress { get; set; }

View File

@@ -35,6 +35,13 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
EthereumSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, EthereumSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod,
StoreData store, EthereumBTCPayNetwork network, object preparePaymentObject) StoreData store, EthereumBTCPayNetwork network, object preparePaymentObject)
{ {
if (preparePaymentObject is null)
{
return new EthereumLikeOnChainPaymentMethodDetails()
{
Activated = false
};
}
if (!_ethereumService.IsAvailable(network.CryptoCode, out var error)) if (!_ethereumService.IsAvailable(network.CryptoCode, out var error))
throw new PaymentMethodUnavailableException(error??$"Not configured yet"); throw new PaymentMethodUnavailableException(error??$"Not configured yet");
var invoice = paymentMethod.ParentEntity; var invoice = paymentMethod.ParentEntity;
@@ -47,7 +54,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
return new EthereumLikeOnChainPaymentMethodDetails() 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.PaymentMethodName = GetPaymentMethodName(network);
model.CryptoImage = GetCryptoImage(network); model.CryptoImage = GetCryptoImage(network);
model.InvoiceBitcoinUrl = ""; model.InvoiceBitcoinUrl = "";
model.InvoiceBitcoinUrlQR = cryptoInfo.Address; model.InvoiceBitcoinUrlQR = cryptoInfo.Address ?? "";
} }
public override string GetCryptoImage(PaymentMethodId paymentMethodId) public override string GetCryptoImage(PaymentMethodId paymentMethodId)

View File

@@ -217,7 +217,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() {InvoiceId = invoiceIds}); var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() {InvoiceId = invoiceIds});
invoices = invoices invoices = invoices
.Where(entity => PaymentMethods.Any(id => entity.GetPaymentMethod(id) != null)) .Where(entity => PaymentMethods.Any(id => entity.GetPaymentMethod(id)?.GetPaymentMethodDetails()?.Activated is true))
.ToArray(); .ToArray();
await UpdatePaymentStates(invoices, cancellationToken); await UpdatePaymentStates(invoices, cancellationToken);
@@ -245,7 +245,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Services
ExistingPayments: entity.GetPayments(network).Select(paymentEntity => (Payment: paymentEntity, ExistingPayments: entity.GetPayments(network).Select(paymentEntity => (Payment: paymentEntity,
PaymentData: (EthereumLikePaymentData)paymentEntity.GetCryptoPaymentData(), PaymentData: (EthereumLikePaymentData)paymentEntity.GetCryptoPaymentData(),
Invoice: entity)) Invoice: entity))
)).Where(tuple => tuple.PaymentMethodDetails != null).ToList(); )).Where(tuple => tuple.PaymentMethodDetails?.GetPaymentMethodDetails()?.Activated is true).ToList();
var existingPaymentData = expandedInvoices.SelectMany(tuple => var existingPaymentData = expandedInvoices.SelectMany(tuple =>
tuple.ExistingPayments.Where(valueTuple => valueTuple.Payment.Accounted)).ToList(); tuple.ExistingPayments.Where(valueTuple => valueTuple.Payment.Accounted)).ToList();

View File

@@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
{ {
return 0.0m; return 0.0m;
} }
public bool Activated { get; set; } = true;
public long AccountIndex { get; set; } public long AccountIndex { get; set; }
public long AddressIndex { get; set; } public long AddressIndex { get; set; }
public string DepositAddress { get; set; } public string DepositAddress { get; set; }

View File

@@ -35,6 +35,14 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
StoreData store, MoneroLikeSpecificBtcPayNetwork network, object preparePaymentObject) StoreData store, MoneroLikeSpecificBtcPayNetwork network, object preparePaymentObject)
{ {
if (preparePaymentObject is null)
{
return new MoneroLikeOnChainPaymentMethodDetails()
{
Activated = false
};
}
if (!_moneroRpcProvider.IsAvailable(network.CryptoCode)) if (!_moneroRpcProvider.IsAvailable(network.CryptoCode))
throw new PaymentMethodUnavailableException($"Node or wallet not available"); throw new PaymentMethodUnavailableException($"Node or wallet not available");
var invoice = paymentMethod.ParentEntity; var invoice = paymentMethod.ParentEntity;
@@ -49,7 +57,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
NextNetworkFee = MoneroMoney.Convert(feeRatePerByte * 100), NextNetworkFee = MoneroMoney.Convert(feeRatePerByte * 100),
AccountIndex = supportedPaymentMethod.AccountIndex, AccountIndex = supportedPaymentMethod.AccountIndex,
AddressIndex = address.AddressIndex, AddressIndex = address.AddressIndex,
DepositAddress = address.Address DepositAddress = address.Address,
Activated = true
}; };
} }
@@ -77,16 +86,23 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
StoreBlob storeBlob, IPaymentMethod paymentMethod) StoreBlob storeBlob, IPaymentMethod paymentMethod)
{ {
var paymentMethodId = paymentMethod.GetId(); var paymentMethodId = paymentMethod.GetId();
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var network = _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(model.CryptoCode); var network = _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(model.CryptoCode);
model.PaymentMethodName = GetPaymentMethodName(network); model.PaymentMethodName = GetPaymentMethodName(network);
model.CryptoImage = GetCryptoImage(network); model.CryptoImage = GetCryptoImage(network);
model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network, new MoneroLikeOnChainPaymentMethodDetails() if (model.Activated)
{ {
DepositAddress = cryptoInfo.Address var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
}, cryptoInfo.Due, null); model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network,
new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due,
null);
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
} }
else
{
model.InvoiceBitcoinUrl = "";
model.InvoiceBitcoinUrlQR = "";
}
}
public override string GetCryptoImage(PaymentMethodId paymentMethodId) public override string GetCryptoImage(PaymentMethodId paymentMethodId)
{ {
var network = _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(paymentMethodId.CryptoCode); var network = _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(paymentMethodId.CryptoCode);

View File

@@ -51,8 +51,9 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri)
{ {
return return paymentMethodDetails.Activated
$"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"; ? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"
: string.Empty;
} }
public override string InvoiceViewPaymentPartialName { get; } = "Monero/ViewMoneroLikePaymentData"; public override string InvoiceViewPaymentPartialName { get; } = "Monero/ViewMoneroLikePaymentData";

View File

@@ -121,6 +121,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance); var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance);
if (paymentMethod != null && if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero && paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero &&
monero.Activated &&
monero.GetPaymentDestination() == paymentData.GetDestination() && monero.GetPaymentDestination() == paymentData.GetDestination() &&
paymentMethod.Calculate().Due > Money.Zero) paymentMethod.Calculate().Due > Money.Zero)
{ {
@@ -363,7 +364,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
} }
var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() { InvoiceId = invoiceIds }); 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)}"); _logger.LogInformation($"Updating pending payments for {cryptoCode} in {string.Join(',', invoiceIds)}");
await UpdatePaymentStates(cryptoCode, invoices); await UpdatePaymentStates(cryptoCode, invoices);
} }

View File

@@ -185,7 +185,7 @@ namespace BTCPayServer.Services.Invoices
foreach (var strat in strategies.Properties()) foreach (var strat in strategies.Properties())
{ {
var paymentMethodId = PaymentMethodId.Parse(strat.Name); var paymentMethodId = PaymentMethodId.Parse(strat.Name);
var network = Networks.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode); var network = Networks.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network != null) if (network != null)
{ {
if (network == Networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike) if (network == Networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike)
@@ -374,7 +374,7 @@ namespace BTCPayServer.Services.Invoices
}).ToList(); }).ToList();
if (paymentId.PaymentType == PaymentTypes.LightningLike) if (details?.Activated is true && paymentId.PaymentType == PaymentTypes.LightningLike)
{ {
cryptoInfo.PaymentUrls = new InvoicePaymentUrls() cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
{ {
@@ -382,7 +382,7 @@ namespace BTCPayServer.Services.Invoices
ServerUrl) ServerUrl)
}; };
} }
else if (paymentId.PaymentType == PaymentTypes.BTCLike) else if (details?.Activated is true && paymentId.PaymentType == PaymentTypes.BTCLike)
{ {
var minerInfo = new MinerFeeInfo(); var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi; minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
@@ -936,10 +936,7 @@ namespace BTCPayServer.Services.Invoices
private decimal GetTxFee() private decimal GetTxFee()
{ {
var method = GetPaymentMethodDetails(); return GetPaymentMethodDetails()?.GetNextNetworkFee()?? 0m;
if (method == null)
return 0.0m;
return method.GetNextNetworkFee();
} }
} }

View File

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

View File

@@ -14,6 +14,7 @@ using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog;
using Encoders = NBitcoin.DataEncoders.Encoders; using Encoders = NBitcoin.DataEncoders.Encoders;
using InvoiceData = BTCPayServer.Data.InvoiceData; using InvoiceData = BTCPayServer.Data.InvoiceData;
@@ -180,8 +181,12 @@ namespace BTCPayServer.Services.Invoices
{ {
if (paymentMethod.Network == null) if (paymentMethod.Network == null)
throw new InvalidOperationException("CryptoCode unsupported"); 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); string address = GetDestination(paymentMethod);
await context.AddressInvoices.AddAsync(new AddressInvoiceData() await context.AddressInvoices.AddAsync(new AddressInvoiceData()
{ {
@@ -244,7 +249,13 @@ namespace BTCPayServer.Services.Invoices
if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike) if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike)
{ {
var network = (BTCPayNetwork)paymentMethod.Network; 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(); return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
@@ -294,6 +305,39 @@ namespace BTCPayServer.Services.Invoices
return true; 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) public async Task AddPendingInvoiceIfNotPresent(string invoiceId)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())

View File

@@ -149,7 +149,7 @@
</div> </div>
</div> </div>
</line-items> </line-items>
<component v-if="srvModel.uiSettings && srvModel.uiSettings.checkoutHeaderVueComponentName" <component v-if="srvModel.uiSettings && srvModel.uiSettings.checkoutHeaderVueComponentName && srvModel.activated"
v-bind:srv-model="srvModel" v-bind:srv-model="srvModel"
v-bind:is="srvModel.uiSettings.checkoutHeaderVueComponentName"> v-bind:is="srvModel.uiSettings.checkoutHeaderVueComponentName">
</component> </component>
@@ -184,7 +184,7 @@
</form> </form>
</div> </div>
<div v-if="showPaymentUI"> <div v-if="showPaymentUI">
<component v-if="srvModel.uiSettings && srvModel.uiSettings.checkoutBodyVueComponentName" <component v-if="srvModel.uiSettings && srvModel.uiSettings.checkoutBodyVueComponentName && srvModel.activated"
v-bind:srv-model="srvModel" v-bind:srv-model="srvModel"
v-bind:is="srvModel.uiSettings.checkoutBodyVueComponentName"> v-bind:is="srvModel.uiSettings.checkoutBodyVueComponentName">
</component> </component>

View File

@@ -80,6 +80,12 @@
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label> <label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
</div> </div>
</div> </div>
<div class="form-group">
<div class="form-check">
<input asp-for="LazyPaymentMethods" type="checkbox" class="form-check-input" />
<label asp-for="LazyPaymentMethods" class="form-check-label"></label>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input" /> <input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input" />

View File

@@ -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": { "components": {
@@ -729,7 +794,7 @@
"items": { "items": {
"type": "string" "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": { "expirationMinutes": {
"nullable": true, "nullable": true,
@@ -837,6 +902,10 @@
"$ref": "#/components/schemas/Payment" "$ref": "#/components/schemas/Payment"
}, },
"description": "Payments made with this payment method." "description": "Payments made with this payment method."
},
"activated": {
"type": "boolean",
"description": "If the payment method is activated (when lazy payments option is enabled"
} }
} }
}, },

View File

@@ -381,6 +381,11 @@
"default": false, "default": false,
"description": "If true, payjoin will be proposed in the checkout page if possible. ([More information](https://docs.btcpayserver.org/Payjoin/))" "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": { "defaultPaymentMethod": {
"type": "string", "type": "string",
"example": "BTC", "example": "BTC",