Add Economics, Governance, and Mining SDKs for: - Java: Full SDK with CompletableFuture async operations - Kotlin: Coroutine-based SDK with suspend functions - Swift: Modern Swift SDK with async/await - Flutter/Dart: Complete Dart SDK with Future-based API - C: Header files and implementations with opaque handles - C++: Modern C++17 with std::future and PIMPL pattern - C#: Records, async/await Tasks, and IDisposable - Ruby: Struct-based types with Faraday HTTP client Also includes minor Dart lint fixes (const exceptions).
215 lines
9 KiB
C#
215 lines
9 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
|
|
namespace Synor.Sdk.Mining;
|
|
|
|
/// <summary>
|
|
/// Synor Mining SDK client for C#.
|
|
/// Pool connections, block templates, hashrate stats, and GPU management.
|
|
/// </summary>
|
|
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<StratumConnection> ConnectAsync(PoolConfig pool, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["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<StratumConnection>("/pool/connect", body, ct);
|
|
return _activeConnection;
|
|
}
|
|
|
|
public async Task DisconnectAsync(CancellationToken ct = default)
|
|
{
|
|
if (_activeConnection == null) return;
|
|
await PostAsync<object>("/pool/disconnect", new { }, ct);
|
|
_activeConnection = null;
|
|
}
|
|
|
|
public async Task<StratumConnection> GetConnectionAsync(CancellationToken ct = default)
|
|
{
|
|
_activeConnection = await GetAsync<StratumConnection>("/pool/connection", ct);
|
|
return _activeConnection;
|
|
}
|
|
|
|
public async Task<PoolStats> GetPoolStatsAsync(CancellationToken ct = default)
|
|
=> await GetAsync<PoolStats>("/pool/stats", ct);
|
|
|
|
public bool IsConnected => _activeConnection?.Status == ConnectionStatus.Connected;
|
|
|
|
// ==================== Mining Operations ====================
|
|
|
|
public async Task<BlockTemplate> GetBlockTemplateAsync(CancellationToken ct = default)
|
|
=> await GetAsync<BlockTemplate>("/mining/template", ct);
|
|
|
|
public async Task<SubmitResult> SubmitWorkAsync(MinedWork work, CancellationToken ct = default)
|
|
=> await PostAsync<SubmitResult>("/mining/submit", work, ct);
|
|
|
|
public async Task StartMiningAsync(string? algorithm = null, CancellationToken ct = default)
|
|
{
|
|
var body = algorithm != null ? new { algorithm } : (object)new { };
|
|
await PostAsync<object>("/mining/start", body, ct);
|
|
}
|
|
|
|
public async Task StopMiningAsync(CancellationToken ct = default)
|
|
=> await PostAsync<object>("/mining/stop", new { }, ct);
|
|
|
|
public async Task<bool> IsMiningAsync(CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<MiningStatusResponse>("/mining/status", ct);
|
|
return r.Mining;
|
|
}
|
|
|
|
// ==================== Stats Operations ====================
|
|
|
|
public async Task<Hashrate> GetHashrateAsync(CancellationToken ct = default)
|
|
=> await GetAsync<Hashrate>("/stats/hashrate", ct);
|
|
|
|
public async Task<MiningStats> GetStatsAsync(CancellationToken ct = default)
|
|
=> await GetAsync<MiningStats>("/stats", ct);
|
|
|
|
public async Task<Earnings> GetEarningsAsync(TimePeriod? period = null, CancellationToken ct = default)
|
|
{
|
|
var path = period.HasValue ? $"/stats/earnings?period={period.Value.ToString().ToLower()}" : "/stats/earnings";
|
|
return await GetAsync<Earnings>(path, ct);
|
|
}
|
|
|
|
public async Task<ShareStats> GetShareStatsAsync(CancellationToken ct = default)
|
|
=> await GetAsync<ShareStats>("/stats/shares", ct);
|
|
|
|
// ==================== Device Operations ====================
|
|
|
|
public async Task<List<MiningDevice>> ListDevicesAsync(CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<DevicesResponse>("/devices", ct);
|
|
return r.Devices ?? new List<MiningDevice>();
|
|
}
|
|
|
|
public async Task<MiningDevice> GetDeviceAsync(string deviceId, CancellationToken ct = default)
|
|
=> await GetAsync<MiningDevice>($"/devices/{Uri.EscapeDataString(deviceId)}", ct);
|
|
|
|
public async Task<MiningDevice> SetDeviceConfigAsync(string deviceId, DeviceConfig config, CancellationToken ct = default)
|
|
=> await PutAsync<MiningDevice>($"/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<List<WorkerInfo>> ListWorkersAsync(CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<WorkersResponse>("/workers", ct);
|
|
return r.Workers ?? new List<WorkerInfo>();
|
|
}
|
|
|
|
public async Task<WorkerInfo> GetWorkerAsync(string workerId, CancellationToken ct = default)
|
|
=> await GetAsync<WorkerInfo>($"/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<List<MiningAlgorithm>> ListAlgorithmsAsync(CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<AlgorithmsResponse>("/algorithms", ct);
|
|
return r.Algorithms ?? new List<MiningAlgorithm>();
|
|
}
|
|
|
|
public async Task<MiningAlgorithm> GetAlgorithmAsync(string name, CancellationToken ct = default)
|
|
=> await GetAsync<MiningAlgorithm>($"/algorithms/{Uri.EscapeDataString(name)}", ct);
|
|
|
|
public async Task SwitchAlgorithmAsync(string algorithm, CancellationToken ct = default)
|
|
=> await PostAsync<object>("/algorithms/switch", new { algorithm }, ct);
|
|
|
|
public async Task<MiningAlgorithm> GetMostProfitableAsync(CancellationToken ct = default)
|
|
=> await GetAsync<MiningAlgorithm>("/algorithms/profitable", ct);
|
|
|
|
// ==================== Lifecycle ====================
|
|
|
|
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
|
{
|
|
try { var r = await GetAsync<MiningHealthResponse>("/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<T> GetAsync<T>(string path, CancellationToken ct)
|
|
=> await ExecuteAsync(async () => {
|
|
var r = await _httpClient.GetAsync(path, ct);
|
|
await EnsureSuccessAsync(r);
|
|
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
|
});
|
|
|
|
private async Task<T> PostAsync<T>(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<T>(_jsonOptions, ct)!;
|
|
});
|
|
|
|
private async Task<T> PutAsync<T>(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<T>(_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<T> ExecuteAsync<T>(Func<Task<T>> 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<Dictionary<string, object>>(c, _jsonOptions);
|
|
throw new MiningException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
|
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
|
}
|
|
}
|
|
}
|