Recommended exchange to be resolved during Invoice Creation (#5976)

* Recommended Exchange Rate Selection during Invoice Creation

* Make Recommended exchanges pluginifiable
This commit is contained in:
Nicolas Dorier
2024-05-13 22:29:42 +09:00
committed by GitHub
parent b0da802abe
commit d96b066658
26 changed files with 255 additions and 166 deletions

File diff suppressed because one or more lines are too long

View File

@@ -203,7 +203,7 @@ namespace BTCPayServer.Rating
return (ExpressionSyntax)CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script)).GetRoot().ChildNodes().First().ChildNodes().First().ChildNodes().First(); return (ExpressionSyntax)CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script)).GetRoot().ChildNodes().First().ChildNodes().First().ChildNodes().First();
} }
public static RateRules Combine(RateRules[] rateRules) public static RateRules Combine(IEnumerable<RateRules> rateRules)
{ {
var str = string.Join(Environment.NewLine, rateRules.Select(r => r.ToString())); var str = string.Join(Environment.NewLine, rateRules.Select(r => r.ToString()));
return Parse(str); return Parse(str);

View File

@@ -4284,7 +4284,7 @@ namespace BTCPayServer.Tests
config = await clientBasic.GetStoreRateConfiguration(user.StoreId); config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript); Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
await AssertValidationError(new[] { "EffectiveScript", "PreferredSource" }, () => await AssertValidationError(new[] { "EffectiveScript" }, () =>
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, EffectiveScript = "BTC_XYZ = 1;" })); clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, EffectiveScript = "BTC_XYZ = 1;" }));
await AssertValidationError(new[] { "EffectiveScript" }, () => await AssertValidationError(new[] { "EffectiveScript" }, () =>

View File

@@ -196,7 +196,7 @@ retry:
TestLogs.LogInformation($"Created store {name}"); TestLogs.LogInformation($"Created store {name}");
Driver.WaitForElement(By.Id("Name")).SendKeys(name); Driver.WaitForElement(By.Id("Name")).SendKeys(name);
var rateSource = new SelectElement(Driver.FindElement(By.Id("PreferredExchange"))); var rateSource = new SelectElement(Driver.FindElement(By.Id("PreferredExchange")));
Assert.Equal("Kraken (Recommended)", rateSource.SelectedOption.Text); Assert.Equal("Recommendation (Kraken)", rateSource.SelectedOption.Text);
rateSource.SelectByText("CoinGecko"); rateSource.SelectByText("CoinGecko");
Driver.WaitForElement(By.Id("Create")).Click(); Driver.WaitForElement(By.Id("Create")).Click();
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click(); Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();

View File

@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Hosting;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Rating; using BTCPayServer.Rating;
@@ -357,12 +358,13 @@ retry:
var factory = FastTests.CreateBTCPayRateFactory(); var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory); var fetcher = new RateFetcher(factory);
var provider = CreateDefaultRates(ChainName.Mainnet); var provider = CreateDefaultRates(ChainName.Mainnet);
var defaultRules = new DefaultRulesCollection(provider.Select(p => p.DefaultRates));
var b = new StoreBlob(); var b = new StoreBlob();
string[] temporarilyBroken = Array.Empty<string>(); string[] temporarilyBroken = Array.Empty<string>();
foreach (var k in StoreBlob.RecommendedExchanges) foreach (var k in defaultRules.RecommendedExchanges)
{ {
b.DefaultCurrency = k.Key; b.DefaultCurrency = k.Key;
var rules = b.GetDefaultRateRules(provider.Select(p => p.DefaultRates)); var rules = b.GetDefaultRateRules(defaultRules);
var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet(); var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet();
var result = fetcher.FetchRates(pairs, rules, null, default); var result = fetcher.FetchRates(pairs, rules, null, default);
foreach ((CurrencyPair key, Task<RateResult> value) in result) foreach ((CurrencyPair key, Task<RateResult> value) in result)
@@ -389,11 +391,13 @@ retry:
public async Task CanGetRateCryptoCurrenciesByDefault() public async Task CanGetRateCryptoCurrenciesByDefault()
{ {
using var cts = new CancellationTokenSource(60_000); using var cts = new CancellationTokenSource(60_000);
var provider = CreateDefaultRates(ChainName.Mainnet); var provider = CreateDefaultRates(ChainName.Mainnet, exchangeRecommendation: true);
var defaultRules = new DefaultRulesCollection(provider.Select(p => p.DefaultRates));
var factory = FastTests.CreateBTCPayRateFactory(); var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory); var fetcher = new RateFetcher(factory);
var pairs = var pairs =
provider provider
.Where(c => c.CryptoCode is not null)
.Select(c => new CurrencyPair(c.CryptoCode, "USD")) .Select(c => new CurrencyPair(c.CryptoCode, "USD"))
.ToHashSet(); .ToHashSet();
@@ -408,7 +412,7 @@ retry:
} }
} }
var rules = new StoreBlob().GetDefaultRateRules(provider.Select(p => p.DefaultRates)); var rules = new StoreBlob().GetDefaultRateRules(defaultRules);
var result = fetcher.FetchRates(pairs, rules, null, cts.Token); var result = fetcher.FetchRates(pairs, rules, null, cts.Token);
foreach ((CurrencyPair key, Task<RateResult> value) in result) foreach ((CurrencyPair key, Task<RateResult> value) in result)
{ {
@@ -418,13 +422,22 @@ retry:
} }
} }
private IEnumerable<(string CryptoCode, DefaultRates DefaultRates)> CreateDefaultRates(ChainName chainName) private IEnumerable<(string CryptoCode, DefaultRules DefaultRates)> CreateDefaultRates(ChainName chainName, bool exchangeRecommendation = false)
{ {
var results = new List<(string CryptoCode, DefaultRates DefaultRates)>(); var results = new List<(string CryptoCode, DefaultRules DefaultRates)>();
var prov = CreateNetworkProvider(chainName); var prov = CreateNetworkProvider(chainName);
foreach (var network in prov.GetAll()) foreach (var network in prov.GetAll())
{ {
results.Add((network.CryptoCode, new DefaultRates(network.DefaultRateRules))); results.Add((network.CryptoCode, new DefaultRules(network.DefaultRateRules)));
}
if (exchangeRecommendation)
{
ServiceCollection services = new ServiceCollection();
BTCPayServerServices.RegisterExchangeRecommendations(services);
foreach (var rule in services.BuildServiceProvider().GetRequiredService<IEnumerable<DefaultRules>>())
{
results.Add((null, rule));
}
} }
return results; return results;
} }

View File

@@ -31,7 +31,7 @@ namespace BTCPayServer.Components.WalletNav
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly UIWalletsController _walletsController; private readonly UIWalletsController _walletsController;
private readonly CurrencyNameTable _currencies; private readonly CurrencyNameTable _currencies;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
private readonly RateFetcher _rateFetcher; private readonly RateFetcher _rateFetcher;
public WalletNav( public WalletNav(
@@ -39,14 +39,14 @@ namespace BTCPayServer.Components.WalletNav
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary handlers,
UIWalletsController walletsController, UIWalletsController walletsController,
CurrencyNameTable currencies, CurrencyNameTable currencies,
IEnumerable<DefaultRates> defaultRates, DefaultRulesCollection defaultRules,
RateFetcher rateFetcher) RateFetcher rateFetcher)
{ {
_walletProvider = walletProvider; _walletProvider = walletProvider;
_handlers = handlers; _handlers = handlers;
_walletsController = walletsController; _walletsController = walletsController;
_currencies = currencies; _currencies = currencies;
_defaultRates = defaultRates; _defaultRules = defaultRules;
_rateFetcher = rateFetcher; _rateFetcher = rateFetcher;
} }
@@ -76,7 +76,7 @@ namespace BTCPayServer.Components.WalletNav
if (defaultCurrency != network.CryptoCode) if (defaultCurrency != network.CryptoCode)
{ {
var rule = store.GetStoreBlob().GetRateRules(_defaultRates)?.GetRuleFor(new Rating.CurrencyPair(network.CryptoCode, defaultCurrency)); var rule = store.GetStoreBlob().GetRateRules(_defaultRules)?.GetRuleFor(new Rating.CurrencyPair(network.CryptoCode, defaultCurrency));
var bid = rule is null ? null : (await _rateFetcher.FetchRate(rule, new StoreIdRateContext(walletId.StoreId), HttpContext.RequestAborted)).BidAsk?.Bid; var bid = rule is null ? null : (await _rateFetcher.FetchRate(rule, new StoreIdRateContext(walletId.StoreId), HttpContext.RequestAborted)).BidAsk?.Bid;
if (bid is decimal b) if (bid is decimal b)
{ {

View File

@@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers
readonly RateFetcher _rateProviderFactory; readonly RateFetcher _rateProviderFactory;
readonly CurrencyNameTable _currencyNameTable; readonly CurrencyNameTable _currencyNameTable;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
readonly StoreRepository _storeRepo; readonly StoreRepository _storeRepo;
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
@@ -43,14 +43,14 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepo, StoreRepository storeRepo,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable, CurrencyNameTable currencyNameTable,
IEnumerable<DefaultRates> defaultRates, DefaultRulesCollection defaultRules,
PaymentMethodHandlerDictionary handlers) PaymentMethodHandlerDictionary handlers)
{ {
_rateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory)); _rateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_storeRepo = storeRepo; _storeRepo = storeRepo;
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_defaultRates = defaultRates; _defaultRules = defaultRules;
_handlers = handlers; _handlers = handlers;
} }
@@ -124,7 +124,7 @@ namespace BTCPayServer.Controllers
} }
} }
var rules = store.GetStoreBlob().GetRateRules(_defaultRates); var rules = store.GetStoreBlob().GetRateRules(_defaultRules);
var pairs = new HashSet<CurrencyPair>(); var pairs = new HashSet<CurrencyPair>();
foreach (var currency in currencyPairs.Split(',')) foreach (var currency in currencyPairs.Split(','))
{ {

View File

@@ -48,7 +48,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions; private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions;
private readonly PayoutMethodHandlerDictionary _payoutHandlers; private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
public LanguageService LanguageService { get; } public LanguageService LanguageService { get; }
@@ -62,7 +62,7 @@ namespace BTCPayServer.Controllers.Greenfield
Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions, Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions,
PayoutMethodHandlerDictionary payoutHandlers, PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary handlers,
IEnumerable<DefaultRates> defaultRates) DefaultRulesCollection defaultRules)
{ {
_invoiceController = invoiceController; _invoiceController = invoiceController;
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
@@ -76,7 +76,7 @@ namespace BTCPayServer.Controllers.Greenfield
_paymentLinkExtensions = paymentLinkExtensions; _paymentLinkExtensions = paymentLinkExtensions;
_payoutHandlers = payoutHandlers; _payoutHandlers = payoutHandlers;
_handlers = handlers; _handlers = handlers;
_defaultRates = defaultRates; _defaultRules = defaultRules;
LanguageService = languageService; LanguageService = languageService;
} }
@@ -430,7 +430,7 @@ namespace BTCPayServer.Controllers.Greenfield
var paidCurrency = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility); var paidCurrency = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate( var rateResult = await _rateProvider.FetchRate(
new CurrencyPair(paymentPrompt.Currency, invoice.Currency), new CurrencyPair(paymentPrompt.Currency, invoice.Currency),
store.GetStoreBlob().GetRateRules(_defaultRates), new StoreIdRateContext(storeId), store.GetStoreBlob().GetRateRules(_defaultRules), new StoreIdRateContext(storeId),
cancellationToken cancellationToken
); );

View File

@@ -27,16 +27,16 @@ namespace BTCPayServer.Controllers.GreenField
public class GreenfieldStoreRateConfigurationController : ControllerBase public class GreenfieldStoreRateConfigurationController : ControllerBase
{ {
private readonly RateFetcher _rateProviderFactory; private readonly RateFetcher _rateProviderFactory;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
public GreenfieldStoreRateConfigurationController( public GreenfieldStoreRateConfigurationController(
RateFetcher rateProviderFactory, RateFetcher rateProviderFactory,
IEnumerable<DefaultRates> defaultRates, DefaultRulesCollection defaultRules,
StoreRepository storeRepository) StoreRepository storeRepository)
{ {
_rateProviderFactory = rateProviderFactory; _rateProviderFactory = rateProviderFactory;
_defaultRates = defaultRates; _defaultRules = defaultRules;
_storeRepository = storeRepository; _storeRepository = storeRepository;
} }
@@ -49,10 +49,10 @@ namespace BTCPayServer.Controllers.GreenField
return Ok(new StoreRateConfiguration() return Ok(new StoreRateConfiguration()
{ {
EffectiveScript = blob.GetRateRules(_defaultRates, out var preferredExchange).ToString(), EffectiveScript = blob.GetRateRules(_defaultRules, out var preferredExchange).ToString(),
Spread = blob.Spread * 100.0m, Spread = blob.Spread * 100.0m,
IsCustomScript = blob.RateScripting, IsCustomScript = blob.RateScripting,
PreferredSource = preferredExchange ? blob.PreferredExchange : null PreferredSource = preferredExchange ? blob.GetPreferredExchange(_defaultRules) : null
}); });
} }
@@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers.GreenField
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
PopulateBlob(configuration, blob); PopulateBlob(configuration, blob);
var rules = blob.GetRateRules(_defaultRates); var rules = blob.GetRateRules(_defaultRules);
var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, new StoreIdRateContext(data.Id), CancellationToken.None); var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, new StoreIdRateContext(data.Id), CancellationToken.None);
@@ -156,7 +156,7 @@ namespace BTCPayServer.Controllers.GreenField
{ {
if (string.IsNullOrEmpty(configuration.EffectiveScript)) if (string.IsNullOrEmpty(configuration.EffectiveScript))
{ {
configuration.EffectiveScript = storeBlob.GetDefaultRateRules(_defaultRates).ToString(); configuration.EffectiveScript = storeBlob.GetDefaultRateRules(_defaultRules).ToString();
} }
if (!RateRules.TryParse(configuration.EffectiveScript, out var r)) if (!RateRules.TryParse(configuration.EffectiveScript, out var r))
@@ -182,12 +182,8 @@ $"You can't set the preferredSource if you are using custom scripts");
ModelState.AddModelError(nameof(configuration.EffectiveScript), ModelState.AddModelError(nameof(configuration.EffectiveScript),
$"You can't set the effectiveScript if you aren't using custom scripts"); $"You can't set the effectiveScript if you aren't using custom scripts");
} }
if (string.IsNullOrEmpty(configuration.PreferredSource)) if (!string.IsNullOrEmpty(configuration.PreferredSource))
{ {
ModelState.AddModelError(nameof(configuration.PreferredSource),
$"The preferredSource is required if you aren't using custom scripts");
}
configuration.PreferredSource = _rateProviderFactory configuration.PreferredSource = _rateProviderFactory
.RateProviderFactory .RateProviderFactory
.AvailableRateProviders .AvailableRateProviders
@@ -202,6 +198,7 @@ $"The preferredSource is required if you aren't using custom scripts");
} }
} }
} }
}
private static void PopulateBlob(StoreRateConfiguration configuration, StoreBlob storeBlob) private static void PopulateBlob(StoreRateConfiguration configuration, StoreBlob storeBlob)
{ {

View File

@@ -24,14 +24,14 @@ namespace BTCPayServer.Controllers.GreenField
public class GreenfieldStoreRatesController : ControllerBase public class GreenfieldStoreRatesController : ControllerBase
{ {
private readonly RateFetcher _rateProviderFactory; private readonly RateFetcher _rateProviderFactory;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
public GreenfieldStoreRatesController( public GreenfieldStoreRatesController(
RateFetcher rateProviderFactory, RateFetcher rateProviderFactory,
IEnumerable<DefaultRates> defaultRates) DefaultRulesCollection defaultRules)
{ {
_rateProviderFactory = rateProviderFactory; _rateProviderFactory = rateProviderFactory;
_defaultRates = defaultRates; _defaultRules = defaultRules;
} }
[HttpGet("")] [HttpGet("")]
@@ -62,7 +62,7 @@ namespace BTCPayServer.Controllers.GreenField
} }
var rules = blob.GetRateRules(_defaultRates); var rules = blob.GetRateRules(_defaultRules);
var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, new StoreIdRateContext(data.Id), CancellationToken.None); var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, new StoreIdRateContext(data.Id), CancellationToken.None);

View File

@@ -382,7 +382,7 @@ namespace BTCPayServer.Controllers
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility); var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethod.Divisibility); model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethod.Divisibility);
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodCurrency); model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodCurrency);
rules = store.GetStoreBlob().GetRateRules(_defaultRates); rules = store.GetStoreBlob().GetRateRules(_defaultRules);
rateResult = await _RateProvider.FetchRate( rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodCurrency, invoice.Currency), rules, new StoreIdRateContext(store.Id), new CurrencyPair(paymentMethodCurrency, invoice.Currency), rules, new StoreIdRateContext(store.Id),
cancellationToken); cancellationToken);
@@ -491,7 +491,7 @@ namespace BTCPayServer.Controllers
return View("_RefundModal", model); return View("_RefundModal", model);
} }
rules = store.GetStoreBlob().GetRateRules(_defaultRates); rules = store.GetStoreBlob().GetRateRules(_defaultRules);
rateResult = await _RateProvider.FetchRate( rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodCurrency, model.CustomCurrency), rules, new StoreIdRateContext(store.Id), new CurrencyPair(paymentMethodCurrency, model.CustomCurrency), rules, new StoreIdRateContext(store.Id),
cancellationToken); cancellationToken);

View File

@@ -52,7 +52,7 @@ namespace BTCPayServer.Controllers
readonly BTCPayNetworkProvider _NetworkProvider; readonly BTCPayNetworkProvider _NetworkProvider;
private readonly PayoutMethodHandlerDictionary _payoutHandlers; private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly PullPaymentHostedService _paymentHostedService; private readonly PullPaymentHostedService _paymentHostedService;
private readonly LanguageService _languageService; private readonly LanguageService _languageService;
@@ -94,7 +94,7 @@ namespace BTCPayServer.Controllers
AppService appService, AppService appService,
IFileService fileService, IFileService fileService,
UriResolver uriResolver, UriResolver uriResolver,
IEnumerable<DefaultRates> defaultRates, DefaultRulesCollection defaultRules,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
TransactionLinkProviders transactionLinkProviders, TransactionLinkProviders transactionLinkProviders,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions, Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
@@ -125,7 +125,7 @@ namespace BTCPayServer.Controllers
_viewProvider = viewProvider; _viewProvider = viewProvider;
_fileService = fileService; _fileService = fileService;
_uriResolver = uriResolver; _uriResolver = uriResolver;
_defaultRates = defaultRates; _defaultRules = defaultRules;
_appService = appService; _appService = appService;
} }
@@ -297,7 +297,7 @@ namespace BTCPayServer.Controllers
private async Task FetchRates(InvoiceCreationContext context, CancellationToken cancellationToken) private async Task FetchRates(InvoiceCreationContext context, CancellationToken cancellationToken)
{ {
var rateRules = context.StoreBlob.GetRateRules(_defaultRates); var rateRules = context.StoreBlob.GetRateRules(_defaultRules);
await context.FetchingRates(_RateProvider, rateRules, cancellationToken); await context.FetchingRates(_RateProvider, rateRules, cancellationToken);
} }
} }

View File

@@ -14,6 +14,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static BTCPayServer.Lightning.Eclair.Models.ChannelResponse;
namespace BTCPayServer.Controllers; namespace BTCPayServer.Controllers;
@@ -22,17 +23,9 @@ public partial class UIStoresController
[HttpGet("{storeId}/rates")] [HttpGet("{storeId}/rates")]
public IActionResult Rates() public IActionResult Rates()
{ {
var exchanges = GetSupportedExchanges().ToList();
var storeBlob = CurrentStore.GetStoreBlob(); var storeBlob = CurrentStore.GetStoreBlob();
var vm = new RatesViewModel(); var vm = new RatesViewModel();
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? storeBlob.GetRecommendedExchange()); FillFromStore(vm, storeBlob);
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.StoreId = CurrentStore.Id;
vm.Script = storeBlob.GetRateRules(_defaultRates).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_defaultRates).ToString();
vm.AvailableExchanges = exchanges;
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
vm.ShowScripting = storeBlob.RateScripting;
return View(vm); return View(vm);
} }
@@ -48,9 +41,6 @@ public partial class UIStoresController
{ {
return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId }); return RedirectToAction(nameof(ShowRateRules), new { scripting = false, storeId = model.StoreId });
} }
var exchanges = GetSupportedExchanges().ToList();
model.SetExchangeRates(exchanges, model.PreferredExchange ?? HttpContext.GetStoreData().GetStoreBlob().GetRecommendedExchange());
model.StoreId = storeId ?? model.StoreId; model.StoreId = storeId ?? model.StoreId;
CurrencyPair[]? currencyPairs = null; CurrencyPair[]? currencyPairs = null;
try try
@@ -70,22 +60,10 @@ public partial class UIStoresController
} }
if (model.PreferredExchange != null) if (model.PreferredExchange != null)
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
if (string.IsNullOrEmpty(model.PreferredExchange))
model.PreferredExchange = null;
var blob = CurrentStore.GetStoreBlob(); var blob = CurrentStore.GetStoreBlob();
model.DefaultScript = blob.GetDefaultRateRules(_defaultRates).ToString();
model.AvailableExchanges = exchanges;
blob.PreferredExchange = model.PreferredExchange;
blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs;
if (!model.ShowScripting)
{
if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase)))
{
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model);
}
}
RateRules? rules; RateRules? rules;
if (model.ShowScripting) if (model.ShowScripting)
{ {
@@ -94,17 +72,20 @@ public partial class UIStoresController
errors ??= []; errors ??= [];
var errorString = string.Join(", ", errors.ToArray()); var errorString = string.Join(", ", errors.ToArray());
ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})"); ModelState.AddModelError(nameof(model.Script), $"Parsing error ({errorString})");
FillFromStore(model, blob);
return View(model); return View(model);
} }
else else
{ {
blob.RateScript = rules.ToString(); blob.RateScript = rules.ToString();
ModelState.Remove(nameof(model.Script)); ModelState.Remove(nameof(model.Script));
model.Script = blob.RateScript;
} }
} }
rules = blob.GetRateRules(_defaultRates); blob.PreferredExchange = model.PreferredExchange;
blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs;
FillFromStore(model, blob);
rules = blob.GetRateRules(_defaultRules);
if (command == "Test") if (command == "Test")
{ {
if (string.IsNullOrWhiteSpace(model.ScriptTest)) if (string.IsNullOrWhiteSpace(model.ScriptTest))
@@ -142,6 +123,12 @@ public partial class UIStoresController
return View(model); return View(model);
} }
if (model.PreferredExchange is not null && !model.AvailableExchanges.Any(a => a.Id == model.PreferredExchange))
{
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange");
return View(model);
}
// command == Save // command == Save
if (CurrentStore.SetStoreBlob(blob)) if (CurrentStore.SetStoreBlob(blob))
{ {
@@ -175,16 +162,29 @@ public partial class UIStoresController
{ {
var blob = CurrentStore.GetStoreBlob(); var blob = CurrentStore.GetStoreBlob();
blob.RateScripting = scripting; blob.RateScripting = scripting;
blob.RateScript = blob.GetDefaultRateRules(_defaultRates).ToString(); blob.RateScript = blob.GetDefaultRateRules(_defaultRules).ToString();
CurrentStore.SetStoreBlob(blob); CurrentStore.SetStoreBlob(blob);
await _storeRepo.UpdateStore(CurrentStore); await _storeRepo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated"); TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated");
return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id }); return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id });
} }
private IEnumerable<RateSourceInfo> GetSupportedExchanges() private void FillFromStore(RatesViewModel vm, StoreBlob storeBlob)
{ {
return _rateFactory.RateProviderFactory.AvailableRateProviders var sources = _rateFactory.RateProviderFactory.AvailableRateProviders
.OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase); .OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase);
vm.AvailableExchanges = sources;
var exchange = storeBlob.GetPreferredExchange(_defaultRules);
var chosenSource = sources.First(r => r.Id == exchange);
vm.Exchanges = UIUserStoresController.GetExchangesSelectList(_rateFactory, _defaultRules, storeBlob);
vm.PreferredExchange = vm.Exchanges.SelectedValue as string;
vm.PreferredResolvedExchange = chosenSource.Id;
vm.RateSource = chosenSource.Url;
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.StoreId = CurrentStore.Id;
vm.Script = storeBlob.GetRateRules(_defaultRules).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_defaultRules).ToString();
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
vm.ShowScripting = storeBlob.RateScripting;
} }
} }

View File

@@ -55,7 +55,7 @@ public partial class UIStoresController : Controller
IOptions<LightningNetworkOptions> lightningNetworkOptions, IOptions<LightningNetworkOptions> lightningNetworkOptions,
IOptions<ExternalServicesOptions> externalServiceOptions, IOptions<ExternalServicesOptions> externalServiceOptions,
IHtmlHelper html, IHtmlHelper html,
IEnumerable<DefaultRates> defaultRates, DefaultRulesCollection defaultRules,
EmailSenderFactory emailSenderFactory, EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers, WalletFileParsers onChainWalletParsers,
UriResolver uriResolver, UriResolver uriResolver,
@@ -85,7 +85,7 @@ public partial class UIStoresController : Controller
_settingsRepository = settingsRepository; _settingsRepository = settingsRepository;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_html = html; _html = html;
_defaultRates = defaultRates; _defaultRules = defaultRules;
_dataProtector = dataProtector.CreateProtector("ConfigProtector"); _dataProtector = dataProtector.CreateProtector("ConfigProtector");
_webhookNotificationManager = webhookNotificationManager; _webhookNotificationManager = webhookNotificationManager;
_lightningNetworkOptions = lightningNetworkOptions.Value; _lightningNetworkOptions = lightningNetworkOptions.Value;
@@ -104,7 +104,7 @@ public partial class UIStoresController : Controller
private readonly ExplorerClientProvider _explorerProvider; private readonly ExplorerClientProvider _explorerProvider;
private readonly LanguageService _langService; private readonly LanguageService _langService;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
private readonly PoliciesSettings _policiesSettings; private readonly PoliciesSettings _policiesSettings;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly AppService _appService; private readonly AppService _appService;

View File

@@ -23,17 +23,20 @@ namespace BTCPayServer.Controllers
private readonly StoreRepository _repo; private readonly StoreRepository _repo;
private readonly SettingsRepository _settingsRepository; private readonly SettingsRepository _settingsRepository;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly DefaultRulesCollection _defaultRules;
private readonly RateFetcher _rateFactory; private readonly RateFetcher _rateFactory;
public string CreatedStoreId { get; set; } public string CreatedStoreId { get; set; }
public UIUserStoresController( public UIUserStoresController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
DefaultRulesCollection defaultRules,
StoreRepository storeRepository, StoreRepository storeRepository,
RateFetcher rateFactory, RateFetcher rateFactory,
SettingsRepository settingsRepository) SettingsRepository settingsRepository)
{ {
_repo = storeRepository; _repo = storeRepository;
_userManager = userManager; _userManager = userManager;
_defaultRules = defaultRules;
_rateFactory = rateFactory; _rateFactory = rateFactory;
_settingsRepository = settingsRepository; _settingsRepository = settingsRepository;
} }
@@ -81,7 +84,7 @@ namespace BTCPayServer.Controllers
{ {
var stores = await _repo.GetStoresByUserId(GetUserId()); var stores = await _repo.GetStoresByUserId(GetUserId());
vm.IsFirstStore = !stores.Any(); vm.IsFirstStore = !stores.Any();
vm.Exchanges = GetExchangesSelectList(vm.PreferredExchange); vm.Exchanges = GetExchangesSelectList(null);
return View(vm); return View(vm);
} }
@@ -124,14 +127,19 @@ namespace BTCPayServer.Controllers
private string GetUserId() => _userManager.GetUserId(User); private string GetUserId() => _userManager.GetUserId(User);
private SelectList GetExchangesSelectList(string selected) private SelectList GetExchangesSelectList(StoreBlob storeBlob) => GetExchangesSelectList(_rateFactory, _defaultRules, storeBlob);
internal static SelectList GetExchangesSelectList(RateFetcher rateFetcher, DefaultRulesCollection defaultRules, StoreBlob storeBlob)
{ {
var exchanges = _rateFactory.RateProviderFactory if (storeBlob is null)
storeBlob = new StoreBlob();
var defaultExchange = defaultRules.GetRecommendedExchange(storeBlob.DefaultCurrency);
var exchanges = rateFetcher.RateProviderFactory
.AvailableRateProviders .AvailableRateProviders
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase) .OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
exchanges.Insert(0, new (null, "Recommended", "")); var exchange = exchanges.First(e => e.Id == defaultExchange);
var chosen = exchanges.FirstOrDefault(f => f.Id == selected) ?? exchanges.First(); exchanges.Insert(0, new(null, $"Recommendation ({exchange.DisplayName})", ""));
var chosen = exchanges.FirstOrDefault(f => f.Id == storeBlob.PreferredExchange) ?? exchanges.First();
return new SelectList(exchanges, nameof(chosen.Id), nameof(chosen.DisplayName), chosen.Id); return new SelectList(exchanges, nameof(chosen.Id), nameof(chosen.DisplayName), chosen.Id);
} }
} }

View File

@@ -73,7 +73,7 @@ namespace BTCPayServer.Controllers
private readonly PayjoinClient _payjoinClient; private readonly PayjoinClient _payjoinClient;
private readonly LabelService _labelService; private readonly LabelService _labelService;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions; private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly TransactionLinkProviders _transactionLinkProviders; private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly PullPaymentHostedService _pullPaymentHostedService;
@@ -100,14 +100,14 @@ namespace BTCPayServer.Controllers
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
LabelService labelService, LabelService labelService,
IEnumerable<DefaultRates> defaultRates, DefaultRulesCollection defaultRules,
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary handlers,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions, Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
TransactionLinkProviders transactionLinkProviders) TransactionLinkProviders transactionLinkProviders)
{ {
_currencyTable = currencyTable; _currencyTable = currencyTable;
_labelService = labelService; _labelService = labelService;
_defaultRates = defaultRates; _defaultRules = defaultRules;
_handlers = handlers; _handlers = handlers;
_paymentModelExtensions = paymentModelExtensions; _paymentModelExtensions = paymentModelExtensions;
_transactionLinkProviders = transactionLinkProviders; _transactionLinkProviders = transactionLinkProviders;
@@ -459,7 +459,7 @@ namespace BTCPayServer.Controllers
if (network == null || network.ReadonlyWallet) if (network == null || network.ReadonlyWallet)
return NotFound(); return NotFound();
var storeData = store.GetStoreBlob(); var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(_defaultRates); var rateRules = store.GetStoreBlob().GetRateRules(_defaultRules);
rateRules.Spread = 0.0m; rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(walletId.CryptoCode, storeData.DefaultCurrency); var currencyPair = new Rating.CurrencyPair(walletId.CryptoCode, storeData.DefaultCurrency);
double.TryParse(defaultAmount, out var amount); double.TryParse(defaultAmount, out var amount);

View File

@@ -102,7 +102,19 @@ namespace BTCPayServer.Data
public decimal Spread { get; set; } = 0.0m; public decimal Spread { get; set; } = 0.0m;
/// <summary>
/// This may be null. Use <see cref="GetPreferredExchange(DefaultRulesCollection)"/> instead if you want to return a valid exchange
/// </summary>
public string PreferredExchange { get; set; } public string PreferredExchange { get; set; }
/// <summary>
/// Use the preferred exchange of the store, or the recommended exchange from the default currency
/// </summary>
/// <param name="defaultRules"></param>
/// <returns></returns>
public string GetPreferredExchange(DefaultRulesCollection defaultRules)
{
return string.IsNullOrEmpty(PreferredExchange) ? defaultRules.GetRecommendedExchange(DefaultCurrency) : PreferredExchange;
}
public List<PaymentMethodCriteria> PaymentMethodCriteria { get; set; } public List<PaymentMethodCriteria> PaymentMethodCriteria { get; set; }
public string HtmlTitle { get; set; } public string HtmlTitle { get; set; }
@@ -135,18 +147,18 @@ namespace BTCPayServer.Data
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public double PaymentTolerance { get; set; } public double PaymentTolerance { get; set; }
public BTCPayServer.Rating.RateRules GetRateRules(IEnumerable<DefaultRates> defaultRates) public BTCPayServer.Rating.RateRules GetRateRules(DefaultRulesCollection defaultRules)
{ {
return GetRateRules(defaultRates, out _); return GetRateRules(defaultRules, out _);
} }
public BTCPayServer.Rating.RateRules GetRateRules(IEnumerable<DefaultRates> defaultRates, out bool preferredSource) public BTCPayServer.Rating.RateRules GetRateRules(DefaultRulesCollection defaultRules, out bool preferredSource)
{ {
if (!RateScripting || if (!RateScripting ||
string.IsNullOrEmpty(RateScript) || string.IsNullOrEmpty(RateScript) ||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules)) !BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
{ {
preferredSource = true; preferredSource = true;
return GetDefaultRateRules(defaultRates); return GetDefaultRateRules(defaultRules);
} }
else else
{ {
@@ -156,34 +168,13 @@ namespace BTCPayServer.Data
} }
} }
public RateRules GetDefaultRateRules(IEnumerable<DefaultRates> defaultRates) public RateRules GetDefaultRateRules(DefaultRulesCollection defaultRules)
{ {
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? GetRecommendedExchange() : PreferredExchange; var rules = defaultRules.WithPreferredExchange(PreferredExchange);
var preferredExchangeRule = RateRules.Parse($"X_X = {preferredExchange}(X_X);");
var rules = RateRules.Combine(defaultRates.Select(r => r.Rules).Concat([preferredExchangeRule]).ToArray());
rules.Spread = Spread; rules.Spread = Spread;
return rules; return rules;
} }
public static JObject RecommendedExchanges = new()
{
{ "EUR", "kraken" },
{ "USD", "kraken" },
{ "GBP", "kraken" },
{ "CHF", "kraken" },
{ "GTQ", "bitpay" },
{ "COP", "yadio" },
{ "ARS", "yadio" },
{ "JPY", "bitbank" },
{ "TRY", "btcturk" },
{ "UGX", "yadio"},
{ "RSD", "bitpay"},
{ "NGN", "bitnob"}
};
public string GetRecommendedExchange() =>
RecommendedExchanges.Property(DefaultCurrency)?.Value.ToString() ?? "coingecko";
[Obsolete("Use GetExcludedPaymentMethods instead")] [Obsolete("Use GetExcludedPaymentMethods instead")]
public string[] ExcludedPaymentMethods { get; set; } public string[] ExcludedPaymentMethods { get; set; }

View File

@@ -53,8 +53,6 @@ namespace BTCPayServer.Data
{ {
ArgumentNullException.ThrowIfNull(storeData); ArgumentNullException.ThrowIfNull(storeData);
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(storeData.StoreBlob); var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(storeData.StoreBlob);
if (result.PreferredExchange == null)
result.PreferredExchange = result.GetRecommendedExchange();
if (result.PaymentMethodCriteria is null) if (result.PaymentMethodCriteria is null)
result.PaymentMethodCriteria = new List<PaymentMethodCriteria>(); result.PaymentMethodCriteria = new List<PaymentMethodCriteria>();
result.PaymentMethodCriteria.RemoveAll(criteria => criteria?.PaymentMethod is null); result.PaymentMethodCriteria.RemoveAll(criteria => criteria?.PaymentMethod is null);

View File

@@ -1,11 +0,0 @@
using System;
using System.Linq;
using BTCPayServer.Rating;
namespace BTCPayServer;
public record DefaultRates(RateRules Rules)
{
public DefaultRates(string[] Rules) : this(RateRules.Combine(Rules.Select(r => RateRules.Parse(r)).ToArray()))
{
}
}

View File

@@ -0,0 +1,72 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Data;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer;
public record DefaultRules(RateRules Rules)
{
public record Recommendation : DefaultRules
{
public Recommendation(string currency, string exchange) : base($"X_{currency.ToUpperInvariant()} = {exchange.ToLowerInvariant()}(X_{currency.ToUpperInvariant()});")
{
Currency = currency.ToUpperInvariant();
Exchange = exchange.ToLowerInvariant();
}
public string Currency { get; }
public string Exchange { get; }
}
public const int HardcodedRecommendedExchangeOrder = 10;
public DefaultRules(string Rules) : this(RateRules.Parse(Rules))
{
}
public DefaultRules(string[] Rules) : this(RateRules.Combine(Rules.Select(r => RateRules.Parse(r)).ToArray()))
{
}
/// <summary>
/// Rules are applied in order, the lower the order, the higher the priority. Default is 0.
/// </summary>
public int Order { get; set; }
}
public class DefaultRulesCollection
{
public DefaultRulesCollection(IEnumerable<DefaultRules> defaultRules)
{
defaultRules = defaultRules.OrderBy(o => o.Order).ToList();
Consolidated = RateRules.Combine(defaultRules.Select(r => r.Rules));
ConsolidatedWithoutRecommendation = RateRules.Combine(defaultRules.Where(r => r is not DefaultRules.Recommendation).Select(r => r.Rules));
var rules = Consolidated.ToString();
foreach (var recommendation in defaultRules.OfType<DefaultRules.Recommendation>())
{
RecommendedExchanges.TryAdd(recommendation.Currency, recommendation.Exchange);
}
}
public RateRules Consolidated { get; private set; }
public RateRules ConsolidatedWithoutRecommendation { get; private set; }
public RateRules WithPreferredExchange(string? preferredExchange)
{
if (string.IsNullOrEmpty(preferredExchange))
{
return Consolidated;
}
else
{
var catchAll = RateRules.Parse($"X_X = {preferredExchange}(X_X);");
return RateRules.Combine([catchAll, ConsolidatedWithoutRecommendation]);
}
}
public Dictionary<string, string> RecommendedExchanges { get; } = new Dictionary<string, string>();
public string GetRecommendedExchange(string currency) =>
RecommendedExchanges.TryGetValue(currency, out var ex) ? ex : "coingecko";
public override string ToString() => Consolidated.ToString();
}

View File

@@ -279,7 +279,7 @@ namespace BTCPayServer.HostedServices
EventAggregator eventAggregator, EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
PayoutMethodHandlerDictionary handlers, PayoutMethodHandlerDictionary handlers,
IEnumerable<DefaultRates> defaultRates, DefaultRulesCollection defaultRules,
NotificationSender notificationSender, NotificationSender notificationSender,
RateFetcher rateFetcher, RateFetcher rateFetcher,
ILogger<PullPaymentHostedService> logger, ILogger<PullPaymentHostedService> logger,
@@ -292,7 +292,7 @@ namespace BTCPayServer.HostedServices
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_networkProvider = networkProvider; _networkProvider = networkProvider;
_handlers = handlers; _handlers = handlers;
_defaultRates = defaultRates; _defaultRules = defaultRules;
_notificationSender = notificationSender; _notificationSender = notificationSender;
_rateFetcher = rateFetcher; _rateFetcher = rateFetcher;
_logger = logger; _logger = logger;
@@ -306,7 +306,7 @@ namespace BTCPayServer.HostedServices
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly PayoutMethodHandlerDictionary _handlers; private readonly PayoutMethodHandlerDictionary _handlers;
private readonly IEnumerable<DefaultRates> _defaultRates; private readonly DefaultRulesCollection _defaultRules;
private readonly NotificationSender _notificationSender; private readonly NotificationSender _notificationSender;
private readonly RateFetcher _rateFetcher; private readonly RateFetcher _rateFetcher;
private readonly ILogger<PullPaymentHostedService> _logger; private readonly ILogger<PullPaymentHostedService> _logger;
@@ -392,7 +392,7 @@ namespace BTCPayServer.HostedServices
if (explicitRateRule is null) if (explicitRateRule is null)
{ {
var storeBlob = payout.StoreData.GetStoreBlob(); var storeBlob = payout.StoreData.GetStoreBlob();
var rules = storeBlob.GetRateRules(_defaultRates); var rules = storeBlob.GetRateRules(_defaultRules);
rules.Spread = 0.0m; rules.Spread = 0.0m;
rule = rules.GetRuleFor(currencyPair); rule = rules.GetRuleFor(currencyPair);
} }

View File

@@ -403,6 +403,8 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<NotificationManager>(); services.AddSingleton<NotificationManager>();
services.AddScoped<NotificationSender>(); services.AddScoped<NotificationSender>();
RegisterExchangeRecommendations(services);
services.AddSingleton<DefaultRulesCollection>();
services.AddSingleton<IHostedService, NBXplorerWaiters>(); services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceEventSaverService>(); services.AddSingleton<IHostedService, InvoiceEventSaverService>();
services.AddSingleton<IHostedService, BitpayIPNSender>(); services.AddSingleton<IHostedService, BitpayIPNSender>();
@@ -504,6 +506,30 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
return services; return services;
} }
public static void RegisterExchangeRecommendations(IServiceCollection services)
{
foreach (var rule in new Dictionary<string, string>()
{
{ "EUR", "kraken" },
{ "USD", "kraken" },
{ "GBP", "kraken" },
{ "CHF", "kraken" },
{ "GTQ", "bitpay" },
{ "COP", "yadio" },
{ "ARS", "yadio" },
{ "JPY", "bitbank" },
{ "TRY", "btcturk" },
{ "UGX", "yadio"},
{ "RSD", "bitpay"},
{ "NGN", "bitnob"}
})
{
var r = new DefaultRules.Recommendation(rule.Key, rule.Value);
r.Order = DefaultRules.HardcodedRecommendedExchangeOrder;
services.AddSingleton<DefaultRules>(r);
}
}
public static void AddOnchainWalletParsers(IServiceCollection services) public static void AddOnchainWalletParsers(IServiceCollection services)
{ {
services.AddSingleton<WalletFileParsers>(); services.AddSingleton<WalletFileParsers>();
@@ -563,13 +589,13 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
} }
public static IServiceCollection AddBTCPayNetwork(this IServiceCollection services, BTCPayNetworkBase network) public static IServiceCollection AddBTCPayNetwork(this IServiceCollection services, BTCPayNetworkBase network)
{ {
services.AddSingleton(new DefaultRates(network.DefaultRateRules)); services.AddSingleton(new DefaultRules(network.DefaultRateRules));
services.AddSingleton<BTCPayNetworkBase>(network); services.AddSingleton<BTCPayNetworkBase>(network);
return services; return services;
} }
public static IServiceCollection AddBTCPayNetwork(this IServiceCollection services, BTCPayNetwork network) public static IServiceCollection AddBTCPayNetwork(this IServiceCollection services, BTCPayNetwork network)
{ {
services.AddSingleton(new DefaultRates(network.DefaultRateRules)); services.AddSingleton(new DefaultRules(network.DefaultRateRules));
// BTC // BTC
{ {
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);

View File

@@ -17,15 +17,6 @@ namespace BTCPayServer.Models.StoreViewModels
public bool Error { get; set; } public bool Error { get; set; }
} }
public void SetExchangeRates(IEnumerable<RateSourceInfo> supportedList, string preferredExchange)
{
supportedList = supportedList.ToArray();
var chosen = supportedList.FirstOrDefault(f => f.Id == preferredExchange) ?? supportedList.FirstOrDefault();
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.DisplayName), chosen);
PreferredExchange = chosen?.Id;
RateSource = chosen?.Url;
}
public List<TestResultViewModel> TestRateRules { get; set; } public List<TestResultViewModel> TestRateRules { get; set; }
public SelectList Exchanges { get; set; } public SelectList Exchanges { get; set; }
@@ -47,6 +38,7 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Preferred Price Source")] [Display(Name = "Preferred Price Source")]
public string PreferredExchange { get; set; } public string PreferredExchange { get; set; }
public string PreferredResolvedExchange { get; set; }
public string RateSource { get; set; } public string RateSource { get; set; }
} }

View File

@@ -187,7 +187,7 @@
<h4 class="mt-5">Customization</h4> <h4 class="mt-5">Customization</h4>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label asp-for="DefaultCurrency" class="form-label"></label> <label asp-for="DefaultCurrency" class="form-label"></label>
<input asp-for="DefaultCurrency" placeholder="Default Store Currency" class="form-control" currency-selection /> <input asp-for="DefaultCurrency" placeholder="@StoreBlob.StandardDefaultCurrency" class="form-control" currency-selection />
</div> </div>
<div class="form-group mb-5"> <div class="form-group mb-5">
<label asp-for="RootAppId" class="form-label"></label> <label asp-for="RootAppId" class="form-label"></label>

View File

@@ -138,7 +138,7 @@ X_X = kraken(X_X);</code></pre>
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-select"></select> <select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-select"></select>
<span asp-validation-for="PreferredExchange" class="text-danger"></span> <span asp-validation-for="PreferredExchange" class="text-danger"></span>
<div id="PreferredExchangeHelpBlock" class="form-text"> <div id="PreferredExchangeHelpBlock" class="form-text">
Current Rates source is <a href="@Model.RateSource" target="_blank" rel="noreferrer noopener">@Model.PreferredExchange</a>. Current Rates source is <a href="@Model.RateSource" target="_blank" rel="noreferrer noopener">@Model.PreferredResolvedExchange</a>.
</div> </div>
</div> </div>
} }

View File

@@ -1,4 +1,5 @@
@model BTCPayServer.Models.StoreViewModels.CreateStoreViewModel @model BTCPayServer.Models.StoreViewModels.CreateStoreViewModel
@inject DefaultRulesCollection DefaultRules
@{ @{
Layout = Model.IsFirstStore ? "_LayoutWizard" : "_Layout"; Layout = Model.IsFirstStore ? "_LayoutWizard" : "_Layout";
ViewData.SetActivePage(StoreNavPages.Create, Model.IsFirstStore ? "Create your first store" : "Create a new store"); ViewData.SetActivePage(StoreNavPages.Create, Model.IsFirstStore ? "Create your first store" : "Create a new store");
@@ -7,12 +8,12 @@
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />
<script> <script>
const exchanges = @Safe.Json(StoreBlob.RecommendedExchanges); const exchanges = @Safe.Json(DefaultRules.RecommendedExchanges);
const recommended = document.querySelector("#PreferredExchange option[value='']") const recommended = document.querySelector("#PreferredExchange option[value='']")
const updateRecommended = currency => { const updateRecommended = currency => {
const source = exchanges[currency] || 'coingecko' const source = exchanges[currency] || 'coingecko'
const name = source.charAt(0).toUpperCase() + source.slice(1) const name = source.charAt(0).toUpperCase() + source.slice(1)
recommended.innerText = `${name} (Recommended)` recommended.innerText = `Recommendation (${name})`
} }
updateRecommended(@Safe.Json(Model.DefaultCurrency)) updateRecommended(@Safe.Json(Model.DefaultCurrency))
delegate('change', '#DefaultCurrency', e => updateRecommended(e.target.value)) delegate('change', '#DefaultCurrency', e => updateRecommended(e.target.value))