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); } var paymentMethod = new PaymentMethod { ParentEntity = entity, Network = network, Rate = rate.BidAsk.Bid, PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) }; paymentMethod.SetId(supportedPaymentMethod.PaymentId); 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})", InvoiceEventData.EventSeverity.Error); } return(null); }
internal async Task <InvoiceEntity> CreateInvoiceCoreRaw(InvoiceEntity entity, StoreData store, IPaymentFilter invoicePaymentMethodFilter, CancellationToken cancellationToken = default) { InvoiceLogs logs = new InvoiceLogs(); logs.Write("Creation of invoice starting", InvoiceEventData.EventSeverity.Info); var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id); var storeBlob = store.GetStoreBlob(); if (entity.Metadata.BuyerEmail != null) { if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail)) { throw new BitpayHttpException(400, "Invalid email"); } entity.RefundMail = entity.Metadata.BuyerEmail; } entity.Status = InvoiceStatusLegacy.New; HashSet <CurrencyPair> currencyPairsToFetch = new HashSet <CurrencyPair>(); var rules = storeBlob.GetRateRules(_NetworkProvider); var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any() if (invoicePaymentMethodFilter != null) { excludeFilter = PaymentFilter.Or(excludeFilter, invoicePaymentMethodFilter); } foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider) .Where(s => !excludeFilter.Match(s.PaymentId)) .Select(c => _NetworkProvider.GetNetwork <BTCPayNetworkBase>(c.PaymentId.CryptoCode)) .Where(c => c != null)) { currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, entity.Currency)); foreach (var paymentMethodCriteria in store.GetPaymentMethodCriteria(_NetworkProvider, storeBlob)) { if (paymentMethodCriteria.Value != null) { currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, paymentMethodCriteria.Value.Currency)); } } } var rateRules = storeBlob.GetRateRules(_NetworkProvider); var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair); List <ISupportedPaymentMethod> supported = new List <ISupportedPaymentMethod>(); var paymentMethods = new PaymentMethodDictionary(); // This loop ends with .ToList so we are querying all payment methods at once // instead of sequentially to improve response time foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider) .Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId)) .Select(c => (Handler: _paymentMethodHandlerDictionary[c.PaymentId], SupportedPaymentMethod: c, Network: _NetworkProvider.GetNetwork <BTCPayNetworkBase>(c.PaymentId.CryptoCode))) .Where(c => c.Network != null) .Select(o => (SupportedPaymentMethod: o.SupportedPaymentMethod, PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs))) .ToList()) { var paymentMethod = await o.PaymentMethod; if (paymentMethod == null) { continue; } supported.Add(o.SupportedPaymentMethod); paymentMethods.Add(paymentMethod); } if (supported.Count == 0) { StringBuilder errors = new StringBuilder(); if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any()) { errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)"); } foreach (var error in logs.ToList()) { errors.AppendLine(error.ToString()); } throw new BitpayHttpException(400, errors.ToString()); } entity.SetSupportedPaymentMethods(supported); entity.SetPaymentMethods(paymentMethods); foreach (var app in await getAppsTaggingStore) { entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id)); } using (logs.Measure("Saving invoice")) { entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); } _ = Task.Run(async() => { try { await fetchingAll; } catch (AggregateException ex) { ex.Handle(e => { logs.Write($"Error while fetching rates {ex}", InvoiceEventData.EventSeverity.Error); return(true); }); } await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs); }); _EventAggregator.Publish(new Events.InvoiceEvent(entity, InvoiceEvent.Created)); return(entity); }