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);
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 bool Activated { get; set; }
public string Destination { get; set; }
public string PaymentLink { get; set; }

View File

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

View File

@@ -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<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.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

View File

@@ -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;
}
@@ -269,6 +277,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<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)
{
return entity.GetPaymentMethods().Select(
@@ -281,6 +315,7 @@ namespace BTCPayServer.Controllers.GreenField
return new InvoicePaymentMethodDataModel()
{
Activated = details.Activated,
PaymentMethod = method.GetId().ToStringNormalized(),
Destination = details.GetPaymentDestination(),
Rate = method.Rate,

View File

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

View File

@@ -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,

View File

@@ -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<ApplicationUser> 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)
{

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
{

View File

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

View File

@@ -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<BTCPayNetwork>(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 { };
}
}
}

View File

@@ -25,5 +25,6 @@ namespace BTCPayServer.Payments.Lightning
{
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))
{
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
if (lightningMethod == null)
if (lightningMethod == null || !lightningMethod.Activated)
continue;
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>()
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);

View File

@@ -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())
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,14 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
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");
var invoice = paymentMethod.ParentEntity;
@@ -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<MoneroLikeSpecificBtcPayNetwork>(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)
{

View File

@@ -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";

View File

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

View File

@@ -185,7 +185,7 @@ namespace BTCPayServer.Services.Invoices
foreach (var strat in strategies.Properties())
{
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 == 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;
}
}

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 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())

View File

@@ -149,7 +149,7 @@
</div>
</div>
</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:is="srvModel.uiSettings.checkoutHeaderVueComponentName">
</component>
@@ -184,7 +184,7 @@
</form>
</div>
<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:is="srvModel.uiSettings.checkoutBodyVueComponentName">
</component>

View File

@@ -80,6 +80,12 @@
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
</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-check">
<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": {
@@ -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"
}
}
},

View File

@@ -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",