private async Task <PaymentMethod> CreatePaymentMethodAsync(Dictionary <CurrencyPair, Task <RateResult> > fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, StoreData store, InvoiceLogs logs) { try { var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:"; var storeBlob = store.GetStoreBlob(); var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network); var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)]; if (rate.BidAsk == null) { return(null); } PaymentMethod paymentMethod = new PaymentMethod(); paymentMethod.ParentEntity = entity; paymentMethod.Network = network; paymentMethod.SetId(supportedPaymentMethod.PaymentId); paymentMethod.Rate = rate.BidAsk.Bid; paymentMethod.PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); using (logs.Measure($"{logPrefix} Payment method details creation")) { var paymentDetails = await handler.CreatePaymentMethodDetails(logs, supportedPaymentMethod, paymentMethod, store, network, preparePayment); paymentMethod.SetPaymentMethodDetails(paymentDetails); } var errorMessage = await handler .IsPaymentMethodAllowedBasedOnInvoiceAmount(storeBlob, fetchingByCurrencyPair, paymentMethod.Calculate().Due, supportedPaymentMethod.PaymentId); if (!string.IsNullOrEmpty(errorMessage)) { logs.Write($"{logPrefix} {errorMessage}"); return(null); } #pragma warning disable CS0618 if (paymentMethod.GetId().IsBTCOnChain) { entity.TxFee = paymentMethod.NextNetworkFee; entity.Rate = paymentMethod.Rate; entity.DepositAddress = paymentMethod.DepositAddress; } #pragma warning restore CS0618 return(paymentMethod); } catch (PaymentMethodUnavailableException ex) { logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})"); } catch (Exception ex) { logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})"); } return(null); }
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); // Check if Lightning Max value is exceeded if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike && storeBlob.LightningMaxValue != null) { var lightningMaxValue = storeBlob.LightningMaxValue; var lightningMaxValueRate = 0.0m; if (lightningMaxValue.Currency == entity.ProductInformation.Currency) { lightningMaxValueRate = paymentMethod.Rate; } else { lightningMaxValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(lightningMaxValue.Currency); } var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / lightningMaxValueRate); if (paymentMethod.Calculate().Due > lightningMaxValueCrypto) { throw new PaymentMethodUnavailableException("Lightning max value exceeded"); } } /////////////// #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); }
private async Task <PaymentMethod> CreatePaymentMethodAsync(Dictionary <CurrencyPair, Task <RateResult> > fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store, InvoiceLogs logs) { try { var logPrefix = $"{handler.ToPrettyString(supportedPaymentMethod.PaymentId)}:"; var storeBlob = store.GetStoreBlob(); var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network); var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)]; if (rate.BidAsk == null) { return(null); } PaymentMethod paymentMethod = new PaymentMethod(); paymentMethod.ParentEntity = entity; paymentMethod.Network = network; paymentMethod.SetId(supportedPaymentMethod.PaymentId); paymentMethod.Rate = rate.BidAsk.Bid; paymentMethod.PreferOnion = this.Request.IsOnion(); using (logs.Measure($"{logPrefix} Payment method details creation")) { var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment); paymentMethod.SetPaymentMethodDetails(paymentDetails); } Func <Money, Money, bool> compare = null; CurrencyValue limitValue = null; string errorMessage = null; if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike && storeBlob.LightningMaxValue != null) { compare = (a, b) => a > b; limitValue = storeBlob.LightningMaxValue; errorMessage = "The amount of the invoice is too high to be paid with lightning"; } else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike && storeBlob.OnChainMinValue != null) { compare = (a, b) => a < b; limitValue = storeBlob.OnChainMinValue; errorMessage = "The amount of the invoice is too low to be paid on chain"; } if (compare != null) { var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)]; if (limitValueRate.BidAsk != null) { var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid); if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) { logs.Write($"{logPrefix} {errorMessage}"); return(null); } } } /////////////// #pragma warning disable CS0618 if (paymentMethod.GetId().IsBTCOnChain) { entity.TxFee = paymentMethod.NextNetworkFee; entity.Rate = paymentMethod.Rate; entity.DepositAddress = paymentMethod.DepositAddress; } #pragma warning restore CS0618 return(paymentMethod); } catch (PaymentMethodUnavailableException ex) { logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})"); } catch (Exception ex) { logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})"); } return(null); }
public override async Task <IPaymentMethodDetails> CreatePaymentMethodDetails( InvoiceLogs logs, LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, Data.StoreData store, BTCPayNetwork network, object preparePaymentObject) { if (paymentMethod.ParentEntity.Type == InvoiceType.TopUp) { throw new PaymentMethodUnavailableException("Lightning Network payment method is not available for top-up invoices"); } 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(supportedPaymentMethod, network, paymentMethod.PreferOnion); var invoice = paymentMethod.ParentEntity; decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility); try { due = paymentMethod.Calculate().Due.ToDecimal(MoneyUnit.BTC); } catch (Exception) { // ignored } var client = supportedPaymentMethod.CreateLightningClient(network, Options.Value, _lightningClientFactory); var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow; if (expiry < TimeSpan.Zero) { expiry = TimeSpan.FromSeconds(1); } LightningInvoice?lightningInvoice = null; string description = storeBlob.LightningDescriptionTemplate; description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{ItemDescription}", invoice.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{OrderId}", invoice.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT)) { try { var request = new CreateInvoiceParams(new LightMoney(due, LightMoneyUnit.BTC), description, expiry); request.PrivateRouteHints = storeBlob.LightningPrivateRouteHints; lightningInvoice = await client.CreateInvoice(request, cts.Token); } catch (OperationCanceledException) when(cts.IsCancellationRequested) { throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner"); } catch (Exception ex) { throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex); } } var nodeInfo = await test; return(new LightningLikePaymentMethodDetails { Activated = true, BOLT11 = lightningInvoice.BOLT11, PaymentHash = BOLT11PaymentRequest.Parse(lightningInvoice.BOLT11, network.NBitcoinNetwork).PaymentHash, InvoiceId = lightningInvoice.Id, NodeInfo = nodeInfo.First().ToString() }); }
private async Task <PaymentMethod> CreatePaymentMethodAsync(Dictionary <CurrencyPair, Task <RateResult> > fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) { var storeBlob = store.GetStoreBlob(); var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)]; if (rate.Value == null) { return(null); } PaymentMethod paymentMethod = new PaymentMethod(); paymentMethod.ParentEntity = entity; paymentMethod.Network = network; paymentMethod.SetId(supportedPaymentMethod.PaymentId); paymentMethod.Rate = rate.Value.Value; var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network); if (storeBlob.NetworkFeeDisabled) { paymentDetails.SetNoTxFee(); } paymentMethod.SetPaymentMethodDetails(paymentDetails); Func <Money, Money, bool> compare = null; CurrencyValue limitValue = null; string errorMessage = null; if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike && storeBlob.LightningMaxValue != null) { compare = (a, b) => a > b; limitValue = storeBlob.LightningMaxValue; errorMessage = "The amount of the invoice is too high to be paid with lightning"; } else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike && storeBlob.OnChainMinValue != null) { compare = (a, b) => a < b; limitValue = storeBlob.OnChainMinValue; errorMessage = "The amount of the invoice is too low to be paid on chain"; } if (compare != null) { var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)]; if (limitValueRate.Value.HasValue) { var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value); if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) { throw new PaymentMethodUnavailableException(errorMessage); } } } /////////////// #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); }
private async Task <PaymentMethod> CreatePaymentMethodAsync(Dictionary <CurrencyPair, Task <RateResult> > fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, StoreData store, InvoiceLogs logs) { try { var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:"; var storeBlob = store.GetStoreBlob(); var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network); var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)]; if (rate.BidAsk == null) { return(null); } PaymentMethod paymentMethod = new PaymentMethod(); paymentMethod.ParentEntity = entity; paymentMethod.Network = network; paymentMethod.SetId(supportedPaymentMethod.PaymentId); paymentMethod.Rate = rate.BidAsk.Bid; paymentMethod.PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); using (logs.Measure($"{logPrefix} Payment method details creation")) { var paymentDetails = await handler.CreatePaymentMethodDetails(logs, supportedPaymentMethod, paymentMethod, store, network, preparePayment); paymentMethod.SetPaymentMethodDetails(paymentDetails); } var criteria = store.GetPaymentMethodCriteria(_NetworkProvider, storeBlob)?.Find(methodCriteria => methodCriteria.PaymentMethod == supportedPaymentMethod.PaymentId); if (criteria?.Value != null) { var currentRateToCrypto = await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)]; if (currentRateToCrypto?.BidAsk != null) { var amount = paymentMethod.Calculate().Due.GetValue(network as BTCPayNetwork); var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid; if (amount < limitValueCrypto && criteria.Above) { logs.Write($"{logPrefix} invoice amount below accepted value for payment method", InvoiceEventData.EventSeverity.Error); return(null); } if (amount > limitValueCrypto && !criteria.Above) { logs.Write($"{logPrefix} invoice amount above accepted value for payment method", InvoiceEventData.EventSeverity.Error); return(null); } } } #pragma warning disable CS0618 if (paymentMethod.GetId().IsBTCOnChain) { entity.TxFee = paymentMethod.NextNetworkFee; entity.Rate = paymentMethod.Rate; entity.DepositAddress = paymentMethod.DepositAddress; } #pragma warning restore CS0618 return(paymentMethod); } catch (PaymentMethodUnavailableException ex) { logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})", InvoiceEventData.EventSeverity.Error); } catch (Exception ex) { logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})", InvoiceEventData.EventSeverity.Error); } return(null); }
public override async Task <IPaymentMethodDetails> CreatePaymentMethodDetails( InvoiceLogs logs, 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; onchainMethod.FeeRate = await prepare.GetFeeRate; switch (onchainMethod.NetworkFeeMode) { case NetworkFeeMode.Always: onchainMethod.NetworkFeeRate = (await prepare.GetNetworkFeeRate); onchainMethod.NextNetworkFee = onchainMethod.NetworkFeeRate.GetFee(100); // assume price for 100 bytes break; case NetworkFeeMode.Never: onchainMethod.NetworkFeeRate = FeeRate.Zero; onchainMethod.NextNetworkFee = Money.Zero; break; case NetworkFeeMode.MultiplePaymentsOnly: onchainMethod.NetworkFeeRate = (await prepare.GetNetworkFeeRate); onchainMethod.NextNetworkFee = Money.Zero; break; } var reserved = await prepare.ReserveAddress; if (paymentMethod.ParentEntity.Type != InvoiceType.TopUp) { var txOut = network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut(); txOut.ScriptPubKey = reserved.Address.ScriptPubKey; var dust = txOut.GetDustThreshold(); var amount = paymentMethod.Calculate().Due; if (amount < dust) { throw new PaymentMethodUnavailableException("Amount below the dust threshold. For amounts of this size, it is recommended to enable an off-chain (Lightning) payment method"); } } onchainMethod.DepositAddress = reserved.Address.ToString(); onchainMethod.KeyPath = reserved.KeyPath; onchainMethod.PayjoinEnabled = blob.PayJoinEnabled && supportedPaymentMethod .AccountDerivation.ScriptPubKeyType() != ScriptPubKeyType.Legacy && network.SupportPayJoin; if (onchainMethod.PayjoinEnabled) { var prefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:"; var nodeSupport = _dashboard?.Get(network.CryptoCode)?.Status?.BitcoinStatus?.Capabilities ?.CanSupportTransactionCheck is true; onchainMethod.PayjoinEnabled &= supportedPaymentMethod.IsHotWallet && nodeSupport; if (!supportedPaymentMethod.IsHotWallet) { logs.Write($"{prefix} Payjoin should have been enabled, but your store is not a hotwallet", InvoiceEventData.EventSeverity.Warning); } if (!nodeSupport) { logs.Write($"{prefix} Payjoin should have been enabled, but your version of NBXplorer or full node does not support it.", InvoiceEventData.EventSeverity.Warning); } if (onchainMethod.PayjoinEnabled) { logs.Write($"{prefix} Payjoin is enabled for this invoice.", InvoiceEventData.EventSeverity.Info); } } return(onchainMethod); }