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)
{
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<string> paymentMethodErrors = new List<string>();
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
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<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
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)
{
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<bool> IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network)
{
return Task.FromResult(_ExplorerProvider.IsAvailable(network));
}
}
}

View File

@ -11,14 +11,6 @@ namespace BTCPayServer.Payments
/// </summary>
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>
/// Create needed to track payments of this invoice
/// </summary>
@ -31,7 +23,6 @@ namespace BTCPayServer.Payments
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{
Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
}
@ -47,16 +38,5 @@ namespace BTCPayServer.Payments
}
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)
{
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<bool> IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
try
{
await Test(supportedPaymentMethod, network);
return true;
}
catch { return false; }
}
/// <summary>
/// Used for testing
/// </summary>
@ -57,8 +59,8 @@ namespace BTCPayServer.Payments.Lightning
public async Task<NodeInfo> 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<bool> 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)

View File

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

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