From e23243565ff16b841c98093e814ab1161548d045 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Wed, 28 Mar 2018 22:37:01 +0900 Subject: [PATCH] Refactor CreateInvoiceCore to better give feedback on payment method errors to the merchant, be faster, and give NodeInfo --- BTCPayServer/Controllers/InvoiceController.cs | 147 ++++++++++-------- .../Bitcoin/BitcoinLikePaymentHandler.cs | 7 +- .../Payments/IPaymentMethodHandler.cs | 20 --- .../Lightning/LightningLikePaymentHandler.cs | 63 ++++---- .../LightningLikePaymentMethodDetails.cs | 1 + .../PaymentMethodUnavailableException.cs | 19 +++ .../Services/Invoices/InvoiceRepository.cs | 13 +- 7 files changed, 141 insertions(+), 129 deletions(-) create mode 100644 BTCPayServer/Payments/PaymentMethodUnavailableException.cs diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index c5e4d716f..1c03947e6 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -79,28 +79,6 @@ namespace BTCPayServer.Controllers internal async Task> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl) { - var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider) - .Select(c => - (Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())), - SupportedPaymentMethod: c, - Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode), - IsAvailable: Task.FromResult(false))) - .Where(c => c.Network != null) - .Select(c => - { - c.IsAvailable = c.Handler.IsAvailable(c.SupportedPaymentMethod, c.Network); - return c; - }) - .ToList(); - foreach (var supportedPaymentMethod in supportedPaymentMethods.ToList()) - { - if (!await supportedPaymentMethod.IsAvailable) - { - supportedPaymentMethods.Remove(supportedPaymentMethod); - } - } - if (supportedPaymentMethods.Count == 0) - throw new BitpayHttpException(400, "No derivation strategy are available now for this store"); var entity = new InvoiceEntity { InvoiceTime = DateTimeOffset.UtcNow @@ -132,61 +110,68 @@ namespace BTCPayServer.Controllers entity.Status = "new"; entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); - var methods = supportedPaymentMethods - .Select(async o => - { - var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency); - PaymentMethod paymentMethod = new PaymentMethod(); - paymentMethod.ParentEntity = entity; - paymentMethod.Network = o.Network; - paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId); - paymentMethod.Rate = rate; - var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network); - if (storeBlob.NetworkFeeDisabled) - paymentDetails.SetNoTxFee(); - paymentMethod.SetPaymentMethodDetails(paymentDetails); -#pragma warning disable CS0618 - if (paymentMethod.GetId().IsBTCOnChain) - { - entity.TxFee = paymentMethod.TxFee; - entity.Rate = paymentMethod.Rate; - entity.DepositAddress = paymentMethod.DepositAddress; - } -#pragma warning restore CS0618 - return (SupportedPaymentMethod: o.SupportedPaymentMethod, PaymentMethod: paymentMethod); - }); - - var paymentMethods = new PaymentMethodDictionary(); + + var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider) + .Select(c => + (Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())), + SupportedPaymentMethod: c, + Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))) + .Where(c => c.Network != null) + .Select(o => + (SupportedPaymentMethod: o.SupportedPaymentMethod, + PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, storeBlob))) + .ToList(); + + List paymentMethodErrors = new List(); List supported = new List(); - foreach (var method in methods) + var paymentMethods = new PaymentMethodDictionary(); + foreach (var o in supportedPaymentMethods) { - var o = await method; - - // Check if Lightning Max value is exceeded - if(o.SupportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike && - storeBlob.LightningMaxValue != null) + try { - var lightningMaxValue = storeBlob.LightningMaxValue; - decimal rate = 0.0m; - if (lightningMaxValue.Currency == invoice.Currency) - rate = o.PaymentMethod.Rate; - else - rate = await storeBlob.ApplyRateRules(o.PaymentMethod.Network, _RateProviders.GetRateProvider(o.PaymentMethod.Network, false)).GetRateAsync(lightningMaxValue.Currency); - - var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / rate); - if (o.PaymentMethod.Calculate().Due > lightningMaxValueCrypto) + var paymentMethod = await o.PaymentMethod; + if (paymentMethod == null) + throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)"); + // Check if Lightning Max value is exceeded + if (o.SupportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike && + storeBlob.LightningMaxValue != null) { - continue; + var lightningMaxValue = storeBlob.LightningMaxValue; + decimal rate = 0.0m; + if (lightningMaxValue.Currency == invoice.Currency) + rate = paymentMethod.Rate; + else + rate = await storeBlob.ApplyRateRules(paymentMethod.Network, _RateProviders.GetRateProvider(paymentMethod.Network, false)).GetRateAsync(lightningMaxValue.Currency); + + var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / rate); + if (paymentMethod.Calculate().Due > lightningMaxValueCrypto) + { + continue; + } } + /////////////// + supported.Add(o.SupportedPaymentMethod); + paymentMethods.Add(paymentMethod); + } + catch (PaymentMethodUnavailableException ex) + { + paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})"); + } + catch (Exception ex) + { + paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})"); } - /////////////// - supported.Add(o.SupportedPaymentMethod); - paymentMethods.Add(o.PaymentMethod); } - if(supported.Count == 0) + if (supported.Count == 0) { - throw new BitpayHttpException(400, "No derivation strategy are available now for this store"); + StringBuilder errors = new StringBuilder(); + errors.AppendLine("No payment method available for this store"); + foreach(var error in paymentMethodErrors) + { + errors.AppendLine(error); + } + throw new BitpayHttpException(400, errors.ToString()); } entity.SetSupportedPaymentMethods(supported); @@ -209,12 +194,36 @@ namespace BTCPayServer.Controllers #pragma warning restore CS0618 } entity.PosData = invoice.PosData; - entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider); + entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider); + _EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created")); var resp = entity.EntityToDTO(_NetworkProvider); return new DataWrapper(resp) { Facade = "pos/invoice" }; } + private async Task CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreBlob storeBlob) + { + var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(entity.ProductInformation.Currency); + PaymentMethod paymentMethod = new PaymentMethod(); + paymentMethod.ParentEntity = entity; + paymentMethod.Network = network; + paymentMethod.SetId(supportedPaymentMethod.PaymentId); + paymentMethod.Rate = rate; + var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, network); + if (storeBlob.NetworkFeeDisabled) + paymentDetails.SetNoTxFee(); + paymentMethod.SetPaymentMethodDetails(paymentDetails); +#pragma warning disable CS0618 + if (paymentMethod.GetId().IsBTCOnChain) + { + entity.TxFee = paymentMethod.TxFee; + entity.Rate = paymentMethod.Rate; + entity.DepositAddress = paymentMethod.DepositAddress; + } +#pragma warning restore CS0618 + return paymentMethod; + } + #pragma warning disable CS0618 private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate) { diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index 64c68532a..27f5bd219 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -29,6 +29,8 @@ namespace BTCPayServer.Payments.Bitcoin public override async Task CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network) { + if (!_ExplorerProvider.IsAvailable(network)) + throw new PaymentMethodUnavailableException($"Full node not available"); var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(); var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase); Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod(); @@ -37,10 +39,5 @@ namespace BTCPayServer.Payments.Bitcoin onchainMethod.DepositAddress = (await getAddress).ToString(); return onchainMethod; } - - public override Task IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network) - { - return Task.FromResult(_ExplorerProvider.IsAvailable(network)); - } } } diff --git a/BTCPayServer/Payments/IPaymentMethodHandler.cs b/BTCPayServer/Payments/IPaymentMethodHandler.cs index db9884e2d..f2e138cf6 100644 --- a/BTCPayServer/Payments/IPaymentMethodHandler.cs +++ b/BTCPayServer/Payments/IPaymentMethodHandler.cs @@ -11,14 +11,6 @@ namespace BTCPayServer.Payments /// public interface IPaymentMethodHandler { - /// - /// Returns true if the dependencies for a specific payment method are satisfied. - /// - /// - /// - /// true if this payment method is available - Task IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network); - /// /// Create needed to track payments of this invoice /// @@ -31,7 +23,6 @@ namespace BTCPayServer.Payments public interface IPaymentMethodHandler : IPaymentMethodHandler where T : ISupportedPaymentMethod { - Task IsAvailable(T supportedPaymentMethod, BTCPayNetwork network); Task CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network); } @@ -47,16 +38,5 @@ namespace BTCPayServer.Payments } throw new NotSupportedException("Invalid supportedPaymentMethod"); } - - public abstract Task IsAvailable(T supportedPaymentMethod, BTCPayNetwork network); - - Task IPaymentMethodHandler.IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network) - { - if(supportedPaymentMethod is T method) - { - return IsAvailable(method, network); - } - return Task.FromResult(false); - } } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 041975fca..a853dd3b6 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -25,30 +25,32 @@ namespace BTCPayServer.Payments.Lightning } public override async Task CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network) { + var test = Test(supportedPaymentMethod, network); var invoice = paymentMethod.ParentEntity; var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8); var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network); var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow; if (expiry < TimeSpan.Zero) expiry = TimeSpan.FromSeconds(1); - var lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry); + + LightningInvoice lightningInvoice = null; + try + { + lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry); + } + catch(Exception ex) + { + throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex); + } + var nodeInfo = await test; return new LightningLikePaymentMethodDetails() { BOLT11 = lightningInvoice.BOLT11, - InvoiceId = lightningInvoice.Id + InvoiceId = lightningInvoice.Id, + NodeInfo = nodeInfo.ToString() }; } - public async override Task IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network) - { - try - { - await Test(supportedPaymentMethod, network); - return true; - } - catch { return false; } - } - /// /// Used for testing /// @@ -57,8 +59,8 @@ namespace BTCPayServer.Payments.Lightning public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network) { if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) - throw new Exception($"Full node not available"); - + throw new PaymentMethodUnavailableException($"Full node not available"); + var cts = new CancellationTokenSource(5000); var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network); LightningNodeInformation info = null; @@ -68,37 +70,39 @@ namespace BTCPayServer.Payments.Lightning } catch (OperationCanceledException) when (cts.IsCancellationRequested) { - throw new Exception($"The lightning node did not replied in a timely maner"); + throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner"); } catch (Exception ex) { - throw new Exception($"Error while connecting to the API ({ex.Message})"); + throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})"); } - if(info.Address == null) + if (info.Address == null) { - throw new Exception($"No lightning node public address has been configured"); + throw new PaymentMethodUnavailableException($"No lightning node public address has been configured"); } var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight); if (blocksGap > 10) { - throw new Exception($"The lightning is not synched ({blocksGap} blocks)"); + throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)"); } try { - if(!SkipP2PTest) + if (!SkipP2PTest) + { await TestConnection(info.Address, info.P2PPort, cts.Token); + } } catch (Exception ex) { - throw new Exception($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})"); + throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})"); } return new NodeInfo(info.NodeId, info.Address, info.P2PPort); } - private async Task TestConnection(string addressStr, int port, CancellationToken cancellation) + private async Task TestConnection(string addressStr, int port, CancellationToken cancellation) { IPAddress address = null; try @@ -107,25 +111,16 @@ namespace BTCPayServer.Payments.Lightning } catch { - try - { - address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault(); - } - catch { } + address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault(); } if (address == null) - throw new Exception($"DNS did not resolved {addressStr}"); + throw new PaymentMethodUnavailableException($"DNS did not resolved {addressStr}"); using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)) { - try - { - await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation); - } - catch { return false; } + await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation); } - return true; } static Task WithTimeout(Task task, CancellationToken token) diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs index ea56c6fa8..60ba19b13 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs @@ -9,6 +9,7 @@ namespace BTCPayServer.Payments.Lightning { public string BOLT11 { get; set; } public string InvoiceId { get; set; } + public string NodeInfo { get; set; } public string GetPaymentDestination() { diff --git a/BTCPayServer/Payments/PaymentMethodUnavailableException.cs b/BTCPayServer/Payments/PaymentMethodUnavailableException.cs new file mode 100644 index 000000000..be609c940 --- /dev/null +++ b/BTCPayServer/Payments/PaymentMethodUnavailableException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Payments +{ + public class PaymentMethodUnavailableException : Exception + { + public PaymentMethodUnavailableException(string message) : base(message) + { + + } + public PaymentMethodUnavailableException(string message, Exception inner) : base(message, inner) + { + + } + } +} diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 4dc28f7c6..ea2231f2b 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -101,7 +101,7 @@ namespace BTCPayServer.Services.Invoices } } - public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider) + public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice, IEnumerable creationLogs, BTCPayNetworkProvider networkProvider) { List textSearch = new List(); invoice = Clone(invoice, null); @@ -146,6 +146,17 @@ namespace BTCPayServer.Services.Invoices textSearch.Add(paymentMethod.Calculate().TotalDue.ToString()); } context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id }); + + foreach(var log in creationLogs) + { + context.InvoiceEvents.Add(new InvoiceEventData() + { + InvoiceDataId = invoice.Id, + Message = log, + Timestamp = invoice.InvoiceTime, + UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10)) + }); + } await context.SaveChangesAsync().ConfigureAwait(false); }