Implements Database, Hosting, and Bridge SDKs for remaining languages: Swift SDKs: - SynorDatabase with KV, Document, Vector, TimeSeries stores - SynorHosting with domain, DNS, deployment, SSL operations - SynorBridge with lock-mint and burn-unlock cross-chain flows C SDKs: - database.h/c - multi-model database client - hosting.h/c - hosting and domain management - bridge.h/c - cross-chain asset transfers C++ SDKs: - database.hpp - modern C++17 with std::future async - hosting.hpp - domain and deployment operations - bridge.hpp - cross-chain bridge with wait operations C# SDKs: - SynorDatabase.cs - async/await with inner store classes - SynorHosting.cs - domain management and analytics - SynorBridge.cs - cross-chain with BridgeException handling Ruby SDKs: - synor_database - Struct-based types with Faraday HTTP - synor_hosting - domain, DNS, SSL, analytics - synor_bridge - lock-mint/burn-unlock with retry logic Phase 3 complete: Database/Hosting/Bridge now available in all 12 languages.
237 lines
12 KiB
C#
237 lines
12 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
|
|
namespace Synor.Sdk.Bridge;
|
|
|
|
/// <summary>
|
|
/// Synor Bridge SDK client for C#.
|
|
/// Cross-chain asset transfers with lock-mint and burn-unlock patterns.
|
|
/// </summary>
|
|
public class SynorBridge : IDisposable
|
|
{
|
|
private readonly BridgeConfig _config;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
private bool _disposed;
|
|
private static readonly HashSet<TransferStatus> FinalStatuses = new() { TransferStatus.Completed, TransferStatus.Failed, TransferStatus.Refunded };
|
|
|
|
public SynorBridge(BridgeConfig 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.SnakeCaseLower, PropertyNameCaseInsensitive = true };
|
|
}
|
|
|
|
// Chain Operations
|
|
public async Task<List<Chain>> GetSupportedChainsAsync(CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<ChainsResponse>("/chains", ct);
|
|
return r.Chains ?? new List<Chain>();
|
|
}
|
|
|
|
public async Task<Chain> GetChainAsync(ChainId chainId, CancellationToken ct = default)
|
|
=> await GetAsync<Chain>($"/chains/{chainId.ToString().ToLower()}", ct);
|
|
|
|
public async Task<bool> IsChainSupportedAsync(ChainId chainId, CancellationToken ct = default)
|
|
{
|
|
try { var c = await GetChainAsync(chainId, ct); return c.Supported; }
|
|
catch { return false; }
|
|
}
|
|
|
|
// Asset Operations
|
|
public async Task<List<Asset>> GetSupportedAssetsAsync(ChainId chainId, CancellationToken ct = default)
|
|
{
|
|
var r = await GetAsync<AssetsResponse>($"/chains/{chainId.ToString().ToLower()}/assets", ct);
|
|
return r.Assets ?? new List<Asset>();
|
|
}
|
|
|
|
public async Task<Asset> GetAssetAsync(string assetId, CancellationToken ct = default)
|
|
=> await GetAsync<Asset>($"/assets/{Uri.EscapeDataString(assetId)}", ct);
|
|
|
|
public async Task<WrappedAsset> GetWrappedAssetAsync(string originalAssetId, ChainId targetChain, CancellationToken ct = default)
|
|
=> await GetAsync<WrappedAsset>($"/assets/{Uri.EscapeDataString(originalAssetId)}/wrapped/{targetChain.ToString().ToLower()}", ct);
|
|
|
|
// Fee Operations
|
|
public async Task<FeeEstimate> EstimateFeeAsync(string asset, string amount, ChainId sourceChain, ChainId targetChain, CancellationToken ct = default)
|
|
=> await PostAsync<FeeEstimate>("/fees/estimate", new { asset, amount, sourceChain = sourceChain.ToString().ToLower(), targetChain = targetChain.ToString().ToLower() }, ct);
|
|
|
|
public async Task<ExchangeRate> GetExchangeRateAsync(string fromAsset, string toAsset, CancellationToken ct = default)
|
|
=> await GetAsync<ExchangeRate>($"/rates/{Uri.EscapeDataString(fromAsset)}/{Uri.EscapeDataString(toAsset)}", ct);
|
|
|
|
// Lock-Mint Flow
|
|
public async Task<LockReceipt> LockAsync(string asset, string amount, ChainId targetChain, LockOptions? options = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["asset"] = asset, ["amount"] = amount, ["targetChain"] = targetChain.ToString().ToLower() };
|
|
if (options?.Recipient != null) body["recipient"] = options.Recipient;
|
|
if (options?.Deadline != null) body["deadline"] = options.Deadline;
|
|
if (options?.Slippage != null) body["slippage"] = options.Slippage;
|
|
return await PostAsync<LockReceipt>("/transfers/lock", body, ct);
|
|
}
|
|
|
|
public async Task<LockProof> GetLockProofAsync(string lockReceiptId, CancellationToken ct = default)
|
|
=> await GetAsync<LockProof>($"/transfers/lock/{Uri.EscapeDataString(lockReceiptId)}/proof", ct);
|
|
|
|
public async Task<LockProof> WaitForLockProofAsync(string lockReceiptId, TimeSpan? pollInterval = null, TimeSpan? maxWait = null, CancellationToken ct = default)
|
|
{
|
|
var interval = pollInterval ?? TimeSpan.FromSeconds(5);
|
|
var deadline = DateTime.UtcNow + (maxWait ?? TimeSpan.FromMinutes(10));
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
try { return await GetLockProofAsync(lockReceiptId, ct); }
|
|
catch (BridgeException ex) when (ex.IsConfirmationsPending) { await Task.Delay(interval, ct); }
|
|
}
|
|
throw new BridgeException("Timeout waiting for lock proof", "CONFIRMATIONS_PENDING");
|
|
}
|
|
|
|
public async Task<SignedTransaction> MintAsync(LockProof proof, string targetAddress, MintOptions? options = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["proof"] = proof, ["targetAddress"] = targetAddress };
|
|
if (options?.GasLimit != null) body["gasLimit"] = options.GasLimit;
|
|
if (options?.MaxFeePerGas != null) body["maxFeePerGas"] = options.MaxFeePerGas;
|
|
if (options?.MaxPriorityFeePerGas != null) body["maxPriorityFeePerGas"] = options.MaxPriorityFeePerGas;
|
|
return await PostAsync<SignedTransaction>("/transfers/mint", body, ct);
|
|
}
|
|
|
|
// Burn-Unlock Flow
|
|
public async Task<BurnReceipt> BurnAsync(string wrappedAsset, string amount, BurnOptions? options = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["wrappedAsset"] = wrappedAsset, ["amount"] = amount };
|
|
if (options?.Recipient != null) body["recipient"] = options.Recipient;
|
|
if (options?.Deadline != null) body["deadline"] = options.Deadline;
|
|
return await PostAsync<BurnReceipt>("/transfers/burn", body, ct);
|
|
}
|
|
|
|
public async Task<BurnProof> GetBurnProofAsync(string burnReceiptId, CancellationToken ct = default)
|
|
=> await GetAsync<BurnProof>($"/transfers/burn/{Uri.EscapeDataString(burnReceiptId)}/proof", ct);
|
|
|
|
public async Task<BurnProof> WaitForBurnProofAsync(string burnReceiptId, TimeSpan? pollInterval = null, TimeSpan? maxWait = null, CancellationToken ct = default)
|
|
{
|
|
var interval = pollInterval ?? TimeSpan.FromSeconds(5);
|
|
var deadline = DateTime.UtcNow + (maxWait ?? TimeSpan.FromMinutes(10));
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
try { return await GetBurnProofAsync(burnReceiptId, ct); }
|
|
catch (BridgeException ex) when (ex.IsConfirmationsPending) { await Task.Delay(interval, ct); }
|
|
}
|
|
throw new BridgeException("Timeout waiting for burn proof", "CONFIRMATIONS_PENDING");
|
|
}
|
|
|
|
public async Task<SignedTransaction> UnlockAsync(BurnProof proof, UnlockOptions? options = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["proof"] = proof };
|
|
if (options?.GasLimit != null) body["gasLimit"] = options.GasLimit;
|
|
if (options?.GasPrice != null) body["gasPrice"] = options.GasPrice;
|
|
return await PostAsync<SignedTransaction>("/transfers/unlock", body, ct);
|
|
}
|
|
|
|
// Transfer Management
|
|
public async Task<Transfer> GetTransferAsync(string transferId, CancellationToken ct = default)
|
|
=> await GetAsync<Transfer>($"/transfers/{Uri.EscapeDataString(transferId)}", ct);
|
|
|
|
public async Task<TransferStatus> GetTransferStatusAsync(string transferId, CancellationToken ct = default)
|
|
{
|
|
var t = await GetTransferAsync(transferId, ct);
|
|
return t.Status;
|
|
}
|
|
|
|
public async Task<List<Transfer>> ListTransfersAsync(TransferFilter? filter = null, CancellationToken ct = default)
|
|
{
|
|
var q = new List<string>();
|
|
if (filter?.Status != null) q.Add($"status={filter.Status.ToString()!.ToLower()}");
|
|
if (filter?.SourceChain != null) q.Add($"sourceChain={filter.SourceChain.ToString()!.ToLower()}");
|
|
if (filter?.TargetChain != null) q.Add($"targetChain={filter.TargetChain.ToString()!.ToLower()}");
|
|
if (filter?.Limit != null) q.Add($"limit={filter.Limit}");
|
|
if (filter?.Offset != null) q.Add($"offset={filter.Offset}");
|
|
var path = q.Count > 0 ? $"/transfers?{string.Join("&", q)}" : "/transfers";
|
|
var r = await GetAsync<TransfersResponse>(path, ct);
|
|
return r.Transfers ?? new List<Transfer>();
|
|
}
|
|
|
|
public async Task<Transfer> WaitForTransferAsync(string transferId, TimeSpan? pollInterval = null, TimeSpan? maxWait = null, CancellationToken ct = default)
|
|
{
|
|
var interval = pollInterval ?? TimeSpan.FromSeconds(10);
|
|
var deadline = DateTime.UtcNow + (maxWait ?? TimeSpan.FromMinutes(30));
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
var t = await GetTransferAsync(transferId, ct);
|
|
if (FinalStatuses.Contains(t.Status)) return t;
|
|
await Task.Delay(interval, ct);
|
|
}
|
|
throw new BridgeException("Timeout waiting for transfer completion");
|
|
}
|
|
|
|
// Convenience Methods
|
|
public async Task<Transfer> BridgeToAsync(string asset, string amount, ChainId targetChain, string targetAddress, LockOptions? lockOptions = null, MintOptions? mintOptions = null, CancellationToken ct = default)
|
|
{
|
|
var receipt = await LockAsync(asset, amount, targetChain, lockOptions, ct);
|
|
if (_config.Debug) Console.WriteLine($"Locked: {receipt.Id}");
|
|
var proof = await WaitForLockProofAsync(receipt.Id, ct: ct);
|
|
if (_config.Debug) Console.WriteLine($"Proof ready, minting on {targetChain}");
|
|
await MintAsync(proof, targetAddress, mintOptions, ct);
|
|
return await WaitForTransferAsync(receipt.Id, ct: ct);
|
|
}
|
|
|
|
public async Task<Transfer> BridgeBackAsync(string wrappedAsset, string amount, BurnOptions? burnOptions = null, UnlockOptions? unlockOptions = null, CancellationToken ct = default)
|
|
{
|
|
var receipt = await BurnAsync(wrappedAsset, amount, burnOptions, ct);
|
|
if (_config.Debug) Console.WriteLine($"Burned: {receipt.Id}");
|
|
var proof = await WaitForBurnProofAsync(receipt.Id, ct: ct);
|
|
if (_config.Debug) Console.WriteLine($"Proof ready, unlocking on {receipt.TargetChain}");
|
|
await UnlockAsync(proof, unlockOptions, ct);
|
|
return await WaitForTransferAsync(receipt.Id, ct: ct);
|
|
}
|
|
|
|
// Lifecycle
|
|
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
|
{
|
|
try { var r = await GetAsync<BridgeHealthResponse>("/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 BridgeException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
|
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
|
}
|
|
}
|
|
}
|