324 lines
13 KiB
C#
324 lines
13 KiB
C#
using System.Text.Json;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Domain.Synth.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Managing.Application.Synth;
|
|
|
|
/// <summary>
|
|
/// Client for communicating with the Synth API
|
|
/// </summary>
|
|
public class SynthApiClient : ISynthApiClient, IDisposable
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly ILogger<SynthApiClient> _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<SynthApiClient> 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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures the HTTP client with API settings
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches the current leaderboard from Synth API
|
|
/// </summary>
|
|
public async Task<List<MinerInfo>> 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<MinerInfo>();
|
|
}
|
|
|
|
var jsonContent = await response.Content.ReadAsStringAsync();
|
|
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(jsonContent, _jsonOptions);
|
|
|
|
_logger.LogInformation($"📊 Synth API - Retrieved {miners?.Count ?? 0} miners from leaderboard");
|
|
|
|
return miners ?? new List<MinerInfo>();
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "HTTP error while fetching Synth leaderboard");
|
|
return new List<MinerInfo>();
|
|
}
|
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
|
{
|
|
_logger.LogError(ex, "Timeout while fetching Synth leaderboard");
|
|
return new List<MinerInfo>();
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
_logger.LogError(ex, "JSON deserialization error while parsing Synth leaderboard");
|
|
return new List<MinerInfo>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Unexpected error while fetching Synth leaderboard");
|
|
return new List<MinerInfo>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches historical leaderboard data from Synth API for a specific time range
|
|
/// </summary>
|
|
public async Task<List<MinerInfo>> 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<MinerInfo>();
|
|
}
|
|
|
|
var jsonContent = await response.Content.ReadAsStringAsync();
|
|
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(jsonContent, _jsonOptions);
|
|
|
|
_logger.LogInformation($"📊 Synth API - Retrieved {miners?.Count ?? 0} miners from historical leaderboard");
|
|
|
|
return miners ?? new List<MinerInfo>();
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "HTTP error while fetching Synth historical leaderboard");
|
|
return new List<MinerInfo>();
|
|
}
|
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
|
{
|
|
_logger.LogError(ex, "Timeout while fetching Synth historical leaderboard");
|
|
return new List<MinerInfo>();
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
_logger.LogError(ex, "JSON deserialization error while parsing Synth historical leaderboard");
|
|
return new List<MinerInfo>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Unexpected error while fetching Synth historical leaderboard");
|
|
return new List<MinerInfo>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches latest predictions from specified miners
|
|
/// </summary>
|
|
public async Task<List<MinerPrediction>> GetMinerPredictionsAsync(
|
|
List<int> 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<MinerPrediction>();
|
|
}
|
|
|
|
try
|
|
{
|
|
// Build URL with proper array formatting for miner parameter
|
|
var queryParams = new List<string>
|
|
{
|
|
$"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<MinerPrediction>();
|
|
}
|
|
|
|
var jsonContent = await response.Content.ReadAsStringAsync();
|
|
var predictions = JsonSerializer.Deserialize<List<MinerPrediction>>(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<MinerPrediction>();
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, $"HTTP error while fetching Synth predictions for {asset}");
|
|
return new List<MinerPrediction>();
|
|
}
|
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
|
{
|
|
_logger.LogError(ex, $"Timeout while fetching Synth predictions for {asset}");
|
|
return new List<MinerPrediction>();
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
_logger.LogError(ex, $"JSON deserialization error while parsing Synth predictions for {asset}");
|
|
return new List<MinerPrediction>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, $"Unexpected error while fetching Synth predictions for {asset}");
|
|
return new List<MinerPrediction>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches historical predictions from specified miners for a specific time point
|
|
/// </summary>
|
|
public async Task<List<MinerPrediction>> GetHistoricalMinerPredictionsAsync(
|
|
List<int> 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<MinerPrediction>();
|
|
}
|
|
|
|
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<string>
|
|
{
|
|
$"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<MinerPrediction>();
|
|
}
|
|
|
|
var jsonContent = await response.Content.ReadAsStringAsync();
|
|
var predictions = JsonSerializer.Deserialize<List<MinerPrediction>>(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<MinerPrediction>();
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, $"HTTP error while fetching Synth historical predictions for {asset}");
|
|
return new List<MinerPrediction>();
|
|
}
|
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
|
{
|
|
_logger.LogError(ex, $"Timeout while fetching Synth historical predictions for {asset}");
|
|
return new List<MinerPrediction>();
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
_logger.LogError(ex, $"JSON deserialization error while parsing Synth historical predictions for {asset}");
|
|
return new List<MinerPrediction>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, $"Unexpected error while fetching Synth historical predictions for {asset}");
|
|
return new List<MinerPrediction>();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_httpClient?.Dispose();
|
|
}
|
|
} |