using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Rating; using static BTCPayServer.Services.Rates.RateProviderFactory; namespace BTCPayServer.Services.Rates { public class ExchangeException { public Exception Exception { get; set; } public string ExchangeName { get; set; } } public class RateResult { public List ExchangeExceptions { get; set; } = new List(); public string Rule { get; set; } public string EvaluatedRule { get; set; } public HashSet Errors { get; set; } public BidAsk BidAsk { get; set; } public TimeSpan Latency { get; internal set; } } #nullable enable public class RateFetcher { private readonly RateProviderFactory _rateProviderFactory; public RateFetcher(RateProviderFactory rateProviderFactory) { _rateProviderFactory = rateProviderFactory; } public RateProviderFactory RateProviderFactory => _rateProviderFactory; public Task FetchRate(CurrencyPair pair, RateRules rules, IRateContext? context, CancellationToken cancellationToken) => FetchRate(pair, new RateRulesCollection(rules, null), context, cancellationToken); public async Task FetchRate(CurrencyPair pair, RateRulesCollection rules, IRateContext? context, CancellationToken cancellationToken) { return await FetchRates(new HashSet(new[] { pair }), rules, context, cancellationToken).First().Value; } public Dictionary> FetchRates(HashSet pairs, RateRules rules, IRateContext? context, CancellationToken cancellationToken) => FetchRates(pairs, new RateRulesCollection(rules, null), context, cancellationToken); record FetchContext( Dictionary Query, Dictionary> FetchingRates, Dictionary> FetchingExchanges, IRateContext? RateContext, CancellationToken CancellationToken) { public FetchContext(IRateContext? context, CancellationToken cancellationToken) :this(new(), new(), new(), context, cancellationToken) { } } public Dictionary> FetchRates(HashSet pairs, RateRulesCollection rules, IRateContext? context, CancellationToken cancellationToken) { var ctx = new FetchContext(context, cancellationToken); void SetQuery(RateRules rateRules) { ctx.Query.Clear(); foreach (var p in pairs.Select(p => (Pair: p, RateRule: rateRules.GetRuleFor(p)))) ctx.Query.Add(p.Pair, p.RateRule); } SetQuery(rules.Primary); FetchRates(ctx); if (rules.Fallback is not null) { SetQuery(rules.Fallback); FetchRates(ctx); } return ctx.FetchingRates; } private void FetchRates(FetchContext ctx) { foreach (var i in ctx.Query) { var dependentQueries = new List>(); foreach (var requiredExchange in i.Value.ExchangeRates) { if (!ctx.FetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching)) { fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange, ctx.RateContext, ctx.CancellationToken); ctx.FetchingExchanges.Add(requiredExchange.Exchange, fetching); } dependentQueries.Add(fetching); } if (ctx.FetchingRates.TryGetValue(i.Key, out var primaryFetch)) { ctx.FetchingRates[i.Key] = FallbackGetRuleValue(dependentQueries, i.Value, primaryFetch); } else ctx.FetchingRates.Add(i.Key, GetRuleValue(dependentQueries, i.Value)); } } private async Task FallbackGetRuleValue(List> dependentQueries, RateRule fallbackRateRule, Task primaryFetch) { var primaryResult = await primaryFetch; if (primaryResult.BidAsk != null) return primaryResult; return await GetRuleValue(dependentQueries, fallbackRateRule); } public Task FetchRate(RateRule rateRule, IRateContext? context, CancellationToken cancellationToken) => FetchRate(new RateRuleCollection(rateRule, null), context, cancellationToken); public Task FetchRate(RateRuleCollection rateRule, IRateContext? context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(rateRule); var ctx = new FetchContext(context, cancellationToken); void SetQuery(RateRule r) { ctx.Query.Clear(); ctx.Query.Add(new("AAA","AAA"), r); } SetQuery(rateRule.Primary); FetchRates(ctx); if (rateRule.Fallback is not null) { SetQuery(rateRule.Fallback); FetchRates(ctx); } return ctx.FetchingRates.First().Value; } private async Task GetRuleValue(List> dependentQueries, RateRule rateRule) { var result = new RateResult(); foreach (var queryAsync in dependentQueries) { var query = await queryAsync; result.Latency = query.Latency; if (query.Exception != null) result.ExchangeExceptions.Add(query.Exception); foreach (var rule in query.PairRates) { rateRule.ExchangeRates.SetRate(query.Exchange, rule.CurrencyPair, rule.BidAsk); } } rateRule.Reevaluate(); result.BidAsk = rateRule.BidAsk; result.Errors = rateRule.Errors; result.EvaluatedRule = rateRule.ToString(true); result.Rule = rateRule.ToString(false); return result; } } }