using System.Net.Http.Json; using System.Text.Json; namespace Synor.Sdk.Mining; /// /// Synor Mining SDK client for C#. /// Pool connections, block templates, hashrate stats, and GPU management. /// public class SynorMining : IDisposable { private readonly MiningConfig _config; private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonOptions; private bool _disposed; private StratumConnection? _activeConnection; public SynorMining(MiningConfig config) { _config = config; _httpClient = new HttpClient { BaseAddress = new Uri(config.Endpoint), Timeout = config.Timeout }; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0"); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; } // ==================== Pool Operations ==================== public async Task ConnectAsync(PoolConfig pool, CancellationToken ct = default) { var body = new Dictionary { ["url"] = pool.Url, ["user"] = pool.User }; if (pool.Password != null) body["password"] = pool.Password; if (pool.Algorithm != null) body["algorithm"] = pool.Algorithm; if (pool.Difficulty != null) body["difficulty"] = pool.Difficulty; _activeConnection = await PostAsync("/pool/connect", body, ct); return _activeConnection; } public async Task DisconnectAsync(CancellationToken ct = default) { if (_activeConnection == null) return; await PostAsync("/pool/disconnect", new { }, ct); _activeConnection = null; } public async Task GetConnectionAsync(CancellationToken ct = default) { _activeConnection = await GetAsync("/pool/connection", ct); return _activeConnection; } public async Task GetPoolStatsAsync(CancellationToken ct = default) => await GetAsync("/pool/stats", ct); public bool IsConnected => _activeConnection?.Status == ConnectionStatus.Connected; // ==================== Mining Operations ==================== public async Task GetBlockTemplateAsync(CancellationToken ct = default) => await GetAsync("/mining/template", ct); public async Task SubmitWorkAsync(MinedWork work, CancellationToken ct = default) => await PostAsync("/mining/submit", work, ct); public async Task StartMiningAsync(string? algorithm = null, CancellationToken ct = default) { var body = algorithm != null ? new { algorithm } : (object)new { }; await PostAsync("/mining/start", body, ct); } public async Task StopMiningAsync(CancellationToken ct = default) => await PostAsync("/mining/stop", new { }, ct); public async Task IsMiningAsync(CancellationToken ct = default) { var r = await GetAsync("/mining/status", ct); return r.Mining; } // ==================== Stats Operations ==================== public async Task GetHashrateAsync(CancellationToken ct = default) => await GetAsync("/stats/hashrate", ct); public async Task GetStatsAsync(CancellationToken ct = default) => await GetAsync("/stats", ct); public async Task GetEarningsAsync(TimePeriod? period = null, CancellationToken ct = default) { var path = period.HasValue ? $"/stats/earnings?period={period.Value.ToString().ToLower()}" : "/stats/earnings"; return await GetAsync(path, ct); } public async Task GetShareStatsAsync(CancellationToken ct = default) => await GetAsync("/stats/shares", ct); // ==================== Device Operations ==================== public async Task> ListDevicesAsync(CancellationToken ct = default) { var r = await GetAsync("/devices", ct); return r.Devices ?? new List(); } public async Task GetDeviceAsync(string deviceId, CancellationToken ct = default) => await GetAsync($"/devices/{Uri.EscapeDataString(deviceId)}", ct); public async Task SetDeviceConfigAsync(string deviceId, DeviceConfig config, CancellationToken ct = default) => await PutAsync($"/devices/{Uri.EscapeDataString(deviceId)}/config", config, ct); public async Task EnableDeviceAsync(string deviceId, CancellationToken ct = default) => await SetDeviceConfigAsync(deviceId, new DeviceConfig(true), ct); public async Task DisableDeviceAsync(string deviceId, CancellationToken ct = default) => await SetDeviceConfigAsync(deviceId, new DeviceConfig(false), ct); // ==================== Worker Operations ==================== public async Task> ListWorkersAsync(CancellationToken ct = default) { var r = await GetAsync("/workers", ct); return r.Workers ?? new List(); } public async Task GetWorkerAsync(string workerId, CancellationToken ct = default) => await GetAsync($"/workers/{Uri.EscapeDataString(workerId)}", ct); public async Task RemoveWorkerAsync(string workerId, CancellationToken ct = default) => await DeleteAsync($"/workers/{Uri.EscapeDataString(workerId)}", ct); // ==================== Algorithm Operations ==================== public async Task> ListAlgorithmsAsync(CancellationToken ct = default) { var r = await GetAsync("/algorithms", ct); return r.Algorithms ?? new List(); } public async Task GetAlgorithmAsync(string name, CancellationToken ct = default) => await GetAsync($"/algorithms/{Uri.EscapeDataString(name)}", ct); public async Task SwitchAlgorithmAsync(string algorithm, CancellationToken ct = default) => await PostAsync("/algorithms/switch", new { algorithm }, ct); public async Task GetMostProfitableAsync(CancellationToken ct = default) => await GetAsync("/algorithms/profitable", ct); // ==================== Lifecycle ==================== public async Task HealthCheckAsync(CancellationToken ct = default) { try { var r = await GetAsync("/health", ct); return r.Status == "healthy"; } catch { return false; } } public bool IsClosed => _disposed; public void Dispose() { if (!_disposed) { _httpClient.Dispose(); _activeConnection = null; _disposed = true; } GC.SuppressFinalize(this); } // ==================== Private HTTP Methods ==================== private async Task GetAsync(string path, CancellationToken ct) => await ExecuteAsync(async () => { var r = await _httpClient.GetAsync(path, ct); await EnsureSuccessAsync(r); return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; }); private async Task PostAsync(string path, object body, CancellationToken ct) => await ExecuteAsync(async () => { var r = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); await EnsureSuccessAsync(r); return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; }); private async Task PutAsync(string path, object body, CancellationToken ct) => await ExecuteAsync(async () => { var r = await _httpClient.PutAsJsonAsync(path, body, _jsonOptions, ct); await EnsureSuccessAsync(r); return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; }); private async Task DeleteAsync(string path, CancellationToken ct) => await ExecuteAsync(async () => { var r = await _httpClient.DeleteAsync(path, ct); await EnsureSuccessAsync(r); return true; }); private async Task ExecuteAsync(Func> op) { Exception? err = null; for (int i = 0; i < _config.Retries; i++) { try { return await op(); } catch (Exception ex) { err = ex; if (i < _config.Retries - 1) await Task.Delay(1000 << i); } } throw err!; } private async Task EnsureSuccessAsync(HttpResponseMessage r) { if (!r.IsSuccessStatusCode) { var c = await r.Content.ReadAsStringAsync(); var e = JsonSerializer.Deserialize>(c, _jsonOptions); throw new MiningException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); } } }