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();
}
}