Refactor CreateInvoiceCore to better give feedback on payment method errors to the merchant, be faster, and give NodeInfo

This commit is contained in:
nicolas.dorier
2018-03-28 22:37:01 +09:00
parent d3420532ae
commit e23243565f
7 changed files with 141 additions and 129 deletions

View File

@@ -79,28 +79,6 @@ namespace BTCPayServer.Controllers
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl) internal async Task<DataWrapper<InvoiceResponse>> 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 var entity = new InvoiceEntity
{ {
InvoiceTime = DateTimeOffset.UtcNow InvoiceTime = DateTimeOffset.UtcNow
@@ -132,61 +110,68 @@ namespace BTCPayServer.Controllers
entity.Status = "new"; entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); 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<string> paymentMethodErrors = new List<string>();
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>(); List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
foreach (var method in methods) var paymentMethods = new PaymentMethodDictionary();
foreach (var o in supportedPaymentMethods)
{ {
var o = await method; try
// Check if Lightning Max value is exceeded
if(o.SupportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
storeBlob.LightningMaxValue != null)
{ {
var lightningMaxValue = storeBlob.LightningMaxValue; var paymentMethod = await o.PaymentMethod;
decimal rate = 0.0m; if (paymentMethod == null)
if (lightningMaxValue.Currency == invoice.Currency) throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
rate = o.PaymentMethod.Rate; // Check if Lightning Max value is exceeded
else if (o.SupportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
rate = await storeBlob.ApplyRateRules(o.PaymentMethod.Network, _RateProviders.GetRateProvider(o.PaymentMethod.Network, false)).GetRateAsync(lightningMaxValue.Currency); storeBlob.LightningMaxValue != null)
var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / rate);
if (o.PaymentMethod.Calculate().Due > lightningMaxValueCrypto)
{ {
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); entity.SetSupportedPaymentMethods(supported);
@@ -209,12 +194,36 @@ namespace BTCPayServer.Controllers
#pragma warning restore CS0618 #pragma warning restore CS0618
} }
entity.PosData = invoice.PosData; 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")); _EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
var resp = entity.EntityToDTO(_NetworkProvider); var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" }; return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
} }
private async Task<PaymentMethod> 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 #pragma warning disable CS0618
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate) private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
{ {

View File

@@ -29,6 +29,8 @@ namespace BTCPayServer.Payments.Bitcoin
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network) public override async Task<IPaymentMethodDetails> 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 getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase); var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod(); Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
@@ -37,10 +39,5 @@ namespace BTCPayServer.Payments.Bitcoin
onchainMethod.DepositAddress = (await getAddress).ToString(); onchainMethod.DepositAddress = (await getAddress).ToString();
return onchainMethod; return onchainMethod;
} }
public override Task<bool> IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network)
{
return Task.FromResult(_ExplorerProvider.IsAvailable(network));
}
} }
} }

View File

@@ -11,14 +11,6 @@ namespace BTCPayServer.Payments
/// </summary> /// </summary>
public interface IPaymentMethodHandler public interface IPaymentMethodHandler
{ {
/// <summary>
/// Returns true if the dependencies for a specific payment method are satisfied.
/// </summary>
/// <param name="supportedPaymentMethod"></param>
/// <param name="network"></param>
/// <returns>true if this payment method is available</returns>
Task<bool> IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network);
/// <summary> /// <summary>
/// Create needed to track payments of this invoice /// Create needed to track payments of this invoice
/// </summary> /// </summary>
@@ -31,7 +23,6 @@ namespace BTCPayServer.Payments
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{ {
Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network); Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
} }
@@ -47,16 +38,5 @@ namespace BTCPayServer.Payments
} }
throw new NotSupportedException("Invalid supportedPaymentMethod"); throw new NotSupportedException("Invalid supportedPaymentMethod");
} }
public abstract Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
Task<bool> IPaymentMethodHandler.IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if(supportedPaymentMethod is T method)
{
return IsAvailable(method, network);
}
return Task.FromResult(false);
}
} }
} }

View File

@@ -25,30 +25,32 @@ namespace BTCPayServer.Payments.Lightning
} }
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network) public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
{ {
var test = Test(supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity; var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8); var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network); var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow; var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (expiry < TimeSpan.Zero) if (expiry < TimeSpan.Zero)
expiry = TimeSpan.FromSeconds(1); 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() return new LightningLikePaymentMethodDetails()
{ {
BOLT11 = lightningInvoice.BOLT11, BOLT11 = lightningInvoice.BOLT11,
InvoiceId = lightningInvoice.Id InvoiceId = lightningInvoice.Id,
NodeInfo = nodeInfo.ToString()
}; };
} }
public async override Task<bool> IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
try
{
await Test(supportedPaymentMethod, network);
return true;
}
catch { return false; }
}
/// <summary> /// <summary>
/// Used for testing /// Used for testing
/// </summary> /// </summary>
@@ -57,7 +59,7 @@ namespace BTCPayServer.Payments.Lightning
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network) public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{ {
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) 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 cts = new CancellationTokenSource(5000);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network); var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
@@ -68,37 +70,39 @@ namespace BTCPayServer.Payments.Lightning
} }
catch (OperationCanceledException) when (cts.IsCancellationRequested) 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) 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); var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10) if (blocksGap > 10)
{ {
throw new Exception($"The lightning is not synched ({blocksGap} blocks)"); throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
} }
try try
{ {
if(!SkipP2PTest) if (!SkipP2PTest)
{
await TestConnection(info.Address, info.P2PPort, cts.Token); await TestConnection(info.Address, info.P2PPort, cts.Token);
}
} }
catch (Exception ex) 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); return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
} }
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation) private async Task TestConnection(string addressStr, int port, CancellationToken cancellation)
{ {
IPAddress address = null; IPAddress address = null;
try try
@@ -107,25 +111,16 @@ namespace BTCPayServer.Payments.Lightning
} }
catch catch
{ {
try address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
{
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
}
catch { }
} }
if (address == null) 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)) using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{ {
try await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
}
catch { return false; }
} }
return true;
} }
static Task WithTimeout(Task task, CancellationToken token) static Task WithTimeout(Task task, CancellationToken token)

View File

@@ -9,6 +9,7 @@ namespace BTCPayServer.Payments.Lightning
{ {
public string BOLT11 { get; set; } public string BOLT11 { get; set; }
public string InvoiceId { get; set; } public string InvoiceId { get; set; }
public string NodeInfo { get; set; }
public string GetPaymentDestination() public string GetPaymentDestination()
{ {

View File

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

View File

@@ -101,7 +101,7 @@ namespace BTCPayServer.Services.Invoices
} }
} }
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider) public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, IEnumerable<string> creationLogs, BTCPayNetworkProvider networkProvider)
{ {
List<string> textSearch = new List<string>(); List<string> textSearch = new List<string>();
invoice = Clone(invoice, null); invoice = Clone(invoice, null);
@@ -146,6 +146,17 @@ namespace BTCPayServer.Services.Invoices
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString()); textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
} }
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id }); 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); await context.SaveChangesAsync().ConfigureAwait(false);
} }