using System.Net.Http.Json; using System.Text.Json; namespace Synor.Sdk.Governance; /// /// Synor Governance SDK client for C#. /// Proposals, voting, DAOs, and vesting operations. /// public class SynorGovernance : IDisposable { private readonly GovernanceConfig _config; private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonOptions; private bool _disposed; private static readonly HashSet FinalStatuses = new() { ProposalStatus.Passed, ProposalStatus.Rejected, ProposalStatus.Executed, ProposalStatus.Cancelled }; public SynorGovernance(GovernanceConfig 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 }; } // ==================== Proposal Operations ==================== public async Task CreateProposalAsync(ProposalDraft draft, CancellationToken ct = default) => await PostAsync("/proposals", draft, ct); public async Task GetProposalAsync(string proposalId, CancellationToken ct = default) => await GetAsync($"/proposals/{Uri.EscapeDataString(proposalId)}", ct); public async Task> ListProposalsAsync(ProposalFilter? filter = null, CancellationToken ct = default) { var q = new List(); if (filter?.Status != null) q.Add($"status={filter.Status.ToString()!.ToLower()}"); if (filter?.Proposer != null) q.Add($"proposer={Uri.EscapeDataString(filter.Proposer)}"); if (filter?.DaoId != null) q.Add($"daoId={Uri.EscapeDataString(filter.DaoId)}"); if (filter?.Limit != null) q.Add($"limit={filter.Limit}"); if (filter?.Offset != null) q.Add($"offset={filter.Offset}"); var path = q.Count > 0 ? $"/proposals?{string.Join("&", q)}" : "/proposals"; var r = await GetAsync(path, ct); return r.Proposals ?? new List(); } public async Task CancelProposalAsync(string proposalId, CancellationToken ct = default) => await PostAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/cancel", new { }, ct); public async Task ExecuteProposalAsync(string proposalId, CancellationToken ct = default) => await PostAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/execute", new { }, ct); public async Task WaitForProposalAsync(string proposalId, TimeSpan? pollInterval = null, TimeSpan? maxWait = null, CancellationToken ct = default) { var interval = pollInterval ?? TimeSpan.FromMinutes(1); var deadline = DateTime.UtcNow + (maxWait ?? TimeSpan.FromDays(7)); while (DateTime.UtcNow < deadline) { var p = await GetProposalAsync(proposalId, ct); if (FinalStatuses.Contains(p.Status)) return p; await Task.Delay(interval, ct); } throw new GovernanceException("Timeout waiting for proposal completion"); } // ==================== Voting Operations ==================== public async Task VoteAsync(string proposalId, Vote vote, string? weight = null, CancellationToken ct = default) { var body = new Dictionary { ["choice"] = vote.Choice.ToString().ToLower() }; if (vote.Reason != null) body["reason"] = vote.Reason; if (weight != null) body["weight"] = weight; return await PostAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/vote", body, ct); } public async Task> GetVotesAsync(string proposalId, CancellationToken ct = default) { var r = await GetAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/votes", ct); return r.Votes ?? new List(); } public async Task GetMyVoteAsync(string proposalId, CancellationToken ct = default) => await GetAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/votes/me", ct); public async Task DelegateAsync(string delegatee, string? amount = null, CancellationToken ct = default) { var body = new Dictionary { ["delegatee"] = delegatee }; if (amount != null) body["amount"] = amount; return await PostAsync("/voting/delegate", body, ct); } public async Task UndelegateAsync(string delegatee, CancellationToken ct = default) => await PostAsync("/voting/undelegate", new { delegatee }, ct); public async Task GetVotingPowerAsync(string address, CancellationToken ct = default) => await GetAsync($"/voting/power/{Uri.EscapeDataString(address)}", ct); public async Task> GetDelegationsAsync(string address, CancellationToken ct = default) { var r = await GetAsync($"/voting/delegations/{Uri.EscapeDataString(address)}", ct); return r.Delegations ?? new List(); } // ==================== DAO Operations ==================== public async Task CreateDaoAsync(DaoConfig config, CancellationToken ct = default) => await PostAsync("/daos", config, ct); public async Task GetDaoAsync(string daoId, CancellationToken ct = default) => await GetAsync($"/daos/{Uri.EscapeDataString(daoId)}", ct); public async Task> ListDaosAsync(int? limit = null, int? offset = null, CancellationToken ct = default) { var q = new List(); if (limit != null) q.Add($"limit={limit}"); if (offset != null) q.Add($"offset={offset}"); var path = q.Count > 0 ? $"/daos?{string.Join("&", q)}" : "/daos"; var r = await GetAsync(path, ct); return r.Daos ?? new List(); } public async Task GetDaoTreasuryAsync(string daoId, CancellationToken ct = default) => await GetAsync($"/daos/{Uri.EscapeDataString(daoId)}/treasury", ct); public async Task> GetDaoMembersAsync(string daoId, CancellationToken ct = default) { var r = await GetAsync($"/daos/{Uri.EscapeDataString(daoId)}/members", ct); return r.Members ?? new List(); } // ==================== Vesting Operations ==================== public async Task CreateVestingScheduleAsync(VestingSchedule schedule, CancellationToken ct = default) => await PostAsync("/vesting", schedule, ct); public async Task GetVestingContractAsync(string contractId, CancellationToken ct = default) => await GetAsync($"/vesting/{Uri.EscapeDataString(contractId)}", ct); public async Task> ListVestingContractsAsync(string? beneficiary = null, CancellationToken ct = default) { var path = beneficiary != null ? $"/vesting?beneficiary={Uri.EscapeDataString(beneficiary)}" : "/vesting"; var r = await GetAsync(path, ct); return r.Contracts ?? new List(); } public async Task ClaimVestedAsync(string contractId, CancellationToken ct = default) => await PostAsync($"/vesting/{Uri.EscapeDataString(contractId)}/claim", new { }, ct); public async Task RevokeVestingAsync(string contractId, CancellationToken ct = default) => await PostAsync($"/vesting/{Uri.EscapeDataString(contractId)}/revoke", new { }, ct); public async Task GetReleasableAmountAsync(string contractId, CancellationToken ct = default) { var r = await GetAsync($"/vesting/{Uri.EscapeDataString(contractId)}/releasable", ct); return r.Amount; } // ==================== 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(); _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 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 GovernanceException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); } } }