using System.Text.Json; using Managing.Application.Abstractions.Services; using Managing.Domain.Synth.Models; using Microsoft.Extensions.Logging; namespace Managing.Application.Synth; /// /// Client for communicating with the Synth API /// public class SynthApiClient : ISynthApiClient, IDisposable { private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonOptions; // Private configuration - should come from app settings or environment variables private readonly string _apiKey; private readonly string _baseUrl; public SynthApiClient(HttpClient httpClient, ILogger logger) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); // TODO: These should come from IConfiguration or environment variables _apiKey = Environment.GetEnvironmentVariable("SYNTH_API_KEY") ?? "bfd2a078b412452af2e01ca74b2a7045d4ae411a85943342"; _baseUrl = Environment.GetEnvironmentVariable("SYNTH_BASE_URL") ?? "https://api.synthdata.co"; // Configure HttpClient once ConfigureHttpClient(); // Configure JSON options _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; } /// /// Configures the HTTP client with API settings /// private void ConfigureHttpClient() { // Validate API configuration if (string.IsNullOrEmpty(_apiKey) || string.IsNullOrEmpty(_baseUrl)) { throw new InvalidOperationException( "Synth API configuration is missing. Please set SYNTH_API_KEY and SYNTH_BASE_URL environment variables."); } // Set base address and authorization _httpClient.BaseAddress = new Uri(_baseUrl); _httpClient.DefaultRequestHeaders.Clear(); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Apikey {_apiKey}"); _httpClient.Timeout = TimeSpan.FromSeconds(30); } /// /// Fetches the current leaderboard from Synth API /// public async Task> GetLeaderboardAsync(SynthConfiguration config) { try { _logger.LogInformation("🔍 Synth API - Fetching leaderboard"); var response = await _httpClient.GetAsync("/leaderboard/latest"); if (!response.IsSuccessStatusCode) { _logger.LogWarning( $"Synth API leaderboard request failed: {response.StatusCode} - {response.ReasonPhrase}"); return new List(); } var jsonContent = await response.Content.ReadAsStringAsync(); var miners = JsonSerializer.Deserialize>(jsonContent, _jsonOptions); _logger.LogInformation($"📊 Synth API - Retrieved {miners?.Count ?? 0} miners from leaderboard"); return miners ?? new List(); } catch (HttpRequestException ex) { _logger.LogError(ex, "HTTP error while fetching Synth leaderboard"); return new List(); } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { _logger.LogError(ex, "Timeout while fetching Synth leaderboard"); return new List(); } catch (JsonException ex) { _logger.LogError(ex, "JSON deserialization error while parsing Synth leaderboard"); return new List(); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error while fetching Synth leaderboard"); return new List(); } } /// /// Fetches historical leaderboard data from Synth API for a specific time range /// public async Task> GetHistoricalLeaderboardAsync(DateTime startTime, DateTime endTime, SynthConfiguration config) { try { // Format dates to ISO 8601 format as required by the API var startTimeStr = Uri.EscapeDataString(startTime.ToString("yyyy-MM-ddTHH:mm:ssZ")); var endTimeStr = Uri.EscapeDataString(endTime.ToString("yyyy-MM-ddTHH:mm:ssZ")); var url = $"/leaderboard/historical?start_time={startTimeStr}&end_time={endTimeStr}"; _logger.LogInformation($"🔍 Synth API - Fetching historical leaderboard from {startTime:yyyy-MM-dd HH:mm} to {endTime:yyyy-MM-dd HH:mm}"); var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { _logger.LogWarning( $"Synth API historical leaderboard request failed: {response.StatusCode} - {response.ReasonPhrase}"); return new List(); } var jsonContent = await response.Content.ReadAsStringAsync(); var miners = JsonSerializer.Deserialize>(jsonContent, _jsonOptions); _logger.LogInformation($"📊 Synth API - Retrieved {miners?.Count ?? 0} miners from historical leaderboard"); return miners ?? new List(); } catch (HttpRequestException ex) { _logger.LogError(ex, "HTTP error while fetching Synth historical leaderboard"); return new List(); } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { _logger.LogError(ex, "Timeout while fetching Synth historical leaderboard"); return new List(); } catch (JsonException ex) { _logger.LogError(ex, "JSON deserialization error while parsing Synth historical leaderboard"); return new List(); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error while fetching Synth historical leaderboard"); return new List(); } } /// /// Fetches latest predictions from specified miners /// public async Task> GetMinerPredictionsAsync( List minerUids, string asset, int timeIncrement, int timeLength, SynthConfiguration config) { if (minerUids == null || !minerUids.Any()) { _logger.LogWarning("No miner UIDs provided for prediction request"); return new List(); } try { // Build URL with proper array formatting for miner parameter var queryParams = new List { $"asset={Uri.EscapeDataString(asset)}", $"time_increment={timeIncrement}", $"time_length={timeLength}" }; // Add each miner UID as a separate parameter (standard array query parameter format) foreach (var minerUid in minerUids) { queryParams.Add($"miner={minerUid}"); } var url = $"/prediction/latest?{string.Join("&", queryParams)}"; _logger.LogInformation( $"🔮 Synth API - Fetching predictions for {minerUids.Count} miners, asset: {asset}, time: {timeLength}s"); var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { _logger.LogWarning( $"Synth API predictions request failed: {response.StatusCode} - {response.ReasonPhrase}"); return new List(); } var jsonContent = await response.Content.ReadAsStringAsync(); var predictions = JsonSerializer.Deserialize>(jsonContent, _jsonOptions); var totalPaths = predictions?.Sum(p => p.NumSimulations) ?? 0; _logger.LogInformation( $"📈 Synth API - Retrieved {predictions?.Count ?? 0} predictions with {totalPaths} total simulation paths"); return predictions ?? new List(); } catch (HttpRequestException ex) { _logger.LogError(ex, $"HTTP error while fetching Synth predictions for {asset}"); return new List(); } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { _logger.LogError(ex, $"Timeout while fetching Synth predictions for {asset}"); return new List(); } catch (JsonException ex) { _logger.LogError(ex, $"JSON deserialization error while parsing Synth predictions for {asset}"); return new List(); } catch (Exception ex) { _logger.LogError(ex, $"Unexpected error while fetching Synth predictions for {asset}"); return new List(); } } /// /// Fetches historical predictions from specified miners for a specific time point /// public async Task> GetHistoricalMinerPredictionsAsync( List minerUids, string asset, DateTime startTime, int timeIncrement, int timeLength, SynthConfiguration config) { if (minerUids == null || !minerUids.Any()) { _logger.LogWarning("No miner UIDs provided for historical prediction request"); return new List(); } try { // Format start time to ISO 8601 format as required by the API var startTimeStr = Uri.EscapeDataString(startTime.ToString("yyyy-MM-ddTHH:mm:ssZ")); // Build URL with proper array formatting for miner parameter var queryParams = new List { $"asset={Uri.EscapeDataString(asset)}", $"start_time={startTimeStr}", $"time_increment={timeIncrement}", $"time_length={timeLength}" }; // Add each miner UID as a separate parameter (standard array query parameter format) foreach (var minerUid in minerUids) { queryParams.Add($"miner={minerUid}"); } var url = $"/prediction/historical?{string.Join("&", queryParams)}"; _logger.LogInformation( $"🔮 Synth API - Fetching historical predictions for {minerUids.Count} miners, asset: {asset}, time: {startTime:yyyy-MM-dd HH:mm}, duration: {timeLength}s"); var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { _logger.LogWarning( $"Synth API historical predictions request failed: {response.StatusCode} - {response.ReasonPhrase}"); return new List(); } var jsonContent = await response.Content.ReadAsStringAsync(); var predictions = JsonSerializer.Deserialize>(jsonContent, _jsonOptions); var totalPaths = predictions?.Sum(p => p.NumSimulations) ?? 0; _logger.LogInformation( $"📈 Synth API - Retrieved {predictions?.Count ?? 0} historical predictions with {totalPaths} total simulation paths"); return predictions ?? new List(); } catch (HttpRequestException ex) { _logger.LogError(ex, $"HTTP error while fetching Synth historical predictions for {asset}"); return new List(); } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { _logger.LogError(ex, $"Timeout while fetching Synth historical predictions for {asset}"); return new List(); } catch (JsonException ex) { _logger.LogError(ex, $"JSON deserialization error while parsing Synth historical predictions for {asset}"); return new List(); } catch (Exception ex) { _logger.LogError(ex, $"Unexpected error while fetching Synth historical predictions for {asset}"); return new List(); } } public void Dispose() { _httpClient?.Dispose(); } }