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).
212 lines
10 KiB
C#
212 lines
10 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
|
|
namespace Synor.Sdk.Governance;
|
|
|
|
/// <summary>
|
|
/// Synor Governance SDK client for C#.
|
|
/// Proposals, voting, DAOs, and vesting operations.
|
|
/// </summary>
|
|
public class SynorGovernance : IDisposable
|
|
{
|
|
private readonly GovernanceConfig _config;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
private bool _disposed;
|
|
private static readonly HashSet<ProposalStatus> 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<Proposal> CreateProposalAsync(ProposalDraft draft, CancellationToken ct = default)
|
|
=> await PostAsync<Proposal>("/proposals", draft, ct);
|
|
|
|
public async Task<Proposal> GetProposalAsync(string proposalId, CancellationToken ct = default)
|
|
=> await GetAsync<Proposal>($"/proposals/{Uri.EscapeDataString(proposalId)}", ct);
|
|
|
|
public async Task<List<Proposal>> ListProposalsAsync(ProposalFilter? filter = null, CancellationToken ct = default)
|
|
{
|
|
var q = new List<string>();
|
|
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<ProposalsResponse>(path, ct);
|
|
return r.Proposals ?? new List<Proposal>();
|
|
}
|
|
|
|
public async Task<Proposal> CancelProposalAsync(string proposalId, CancellationToken ct = default)
|
|
=> await PostAsync<Proposal>($"/proposals/{Uri.EscapeDataString(proposalId)}/cancel", new { }, ct);
|
|
|
|
public async Task<Proposal> ExecuteProposalAsync(string proposalId, CancellationToken ct = default)
|
|
=> await PostAsync<Proposal>($"/proposals/{Uri.EscapeDataString(proposalId)}/execute", new { }, ct);
|
|
|
|
public async Task<Proposal> 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<VoteReceipt> VoteAsync(string proposalId, Vote vote, string? weight = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["choice"] = vote.Choice.ToString().ToLower() };
|
|
if (vote.Reason != null) body["reason"] = vote.Reason;
|
|
if (weight != null) body["weight"] = weight;
|
|
return await PostAsync<VoteReceipt>($"/proposals/{Uri.EscapeDataString(proposalId)}/vote", body, ct);
|
|
}
|
|
|
|
public async Task<List<VoteReceipt>> GetVotesAsync(string proposalId, CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<VotesResponse>($"/proposals/{Uri.EscapeDataString(proposalId)}/votes", ct);
|
|
return r.Votes ?? new List<VoteReceipt>();
|
|
}
|
|
|
|
public async Task<VoteReceipt> GetMyVoteAsync(string proposalId, CancellationToken ct = default)
|
|
=> await GetAsync<VoteReceipt>($"/proposals/{Uri.EscapeDataString(proposalId)}/votes/me", ct);
|
|
|
|
public async Task<DelegationReceipt> DelegateAsync(string delegatee, string? amount = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["delegatee"] = delegatee };
|
|
if (amount != null) body["amount"] = amount;
|
|
return await PostAsync<DelegationReceipt>("/voting/delegate", body, ct);
|
|
}
|
|
|
|
public async Task<DelegationReceipt> UndelegateAsync(string delegatee, CancellationToken ct = default)
|
|
=> await PostAsync<DelegationReceipt>("/voting/undelegate", new { delegatee }, ct);
|
|
|
|
public async Task<VotingPower> GetVotingPowerAsync(string address, CancellationToken ct = default)
|
|
=> await GetAsync<VotingPower>($"/voting/power/{Uri.EscapeDataString(address)}", ct);
|
|
|
|
public async Task<List<DelegationReceipt>> GetDelegationsAsync(string address, CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<DelegationsResponse>($"/voting/delegations/{Uri.EscapeDataString(address)}", ct);
|
|
return r.Delegations ?? new List<DelegationReceipt>();
|
|
}
|
|
|
|
// ==================== DAO Operations ====================
|
|
|
|
public async Task<Dao> CreateDaoAsync(DaoConfig config, CancellationToken ct = default)
|
|
=> await PostAsync<Dao>("/daos", config, ct);
|
|
|
|
public async Task<Dao> GetDaoAsync(string daoId, CancellationToken ct = default)
|
|
=> await GetAsync<Dao>($"/daos/{Uri.EscapeDataString(daoId)}", ct);
|
|
|
|
public async Task<List<Dao>> ListDaosAsync(int? limit = null, int? offset = null, CancellationToken ct = default)
|
|
{
|
|
var q = new List<string>();
|
|
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<DaosResponse>(path, ct);
|
|
return r.Daos ?? new List<Dao>();
|
|
}
|
|
|
|
public async Task<DaoTreasury> GetDaoTreasuryAsync(string daoId, CancellationToken ct = default)
|
|
=> await GetAsync<DaoTreasury>($"/daos/{Uri.EscapeDataString(daoId)}/treasury", ct);
|
|
|
|
public async Task<List<string>> GetDaoMembersAsync(string daoId, CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<MembersResponse>($"/daos/{Uri.EscapeDataString(daoId)}/members", ct);
|
|
return r.Members ?? new List<string>();
|
|
}
|
|
|
|
// ==================== Vesting Operations ====================
|
|
|
|
public async Task<VestingContract> CreateVestingScheduleAsync(VestingSchedule schedule, CancellationToken ct = default)
|
|
=> await PostAsync<VestingContract>("/vesting", schedule, ct);
|
|
|
|
public async Task<VestingContract> GetVestingContractAsync(string contractId, CancellationToken ct = default)
|
|
=> await GetAsync<VestingContract>($"/vesting/{Uri.EscapeDataString(contractId)}", ct);
|
|
|
|
public async Task<List<VestingContract>> ListVestingContractsAsync(string? beneficiary = null, CancellationToken ct = default)
|
|
{
|
|
var path = beneficiary != null ? $"/vesting?beneficiary={Uri.EscapeDataString(beneficiary)}" : "/vesting";
|
|
var r = await GetAsync<VestingContractsResponse>(path, ct);
|
|
return r.Contracts ?? new List<VestingContract>();
|
|
}
|
|
|
|
public async Task<ClaimReceipt> ClaimVestedAsync(string contractId, CancellationToken ct = default)
|
|
=> await PostAsync<ClaimReceipt>($"/vesting/{Uri.EscapeDataString(contractId)}/claim", new { }, ct);
|
|
|
|
public async Task<VestingContract> RevokeVestingAsync(string contractId, CancellationToken ct = default)
|
|
=> await PostAsync<VestingContract>($"/vesting/{Uri.EscapeDataString(contractId)}/revoke", new { }, ct);
|
|
|
|
public async Task<string> GetReleasableAmountAsync(string contractId, CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<ReleasableResponse>($"/vesting/{Uri.EscapeDataString(contractId)}/releasable", ct);
|
|
return r.Amount;
|
|
}
|
|
|
|
// ==================== Lifecycle ====================
|
|
|
|
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
|
{
|
|
try { var r = await GetAsync<GovernanceHealthResponse>("/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<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> 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 GovernanceException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
|
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
|
}
|
|
}
|
|
}
|