using System.Net.Http.Json; using System.Text.Json; namespace Synor.Sdk.Bridge; /// /// Synor Bridge SDK client for C#. /// Cross-chain asset transfers with lock-mint and burn-unlock patterns. /// public class SynorBridge : IDisposable { private readonly BridgeConfig _config; private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonOptions; private bool _disposed; private static readonly HashSet 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> GetSupportedChainsAsync(CancellationToken ct = default) { var r = await GetAsync("/chains", ct); return r.Chains ?? new List(); } public async Task GetChainAsync(ChainId chainId, CancellationToken ct = default) => await GetAsync($"/chains/{chainId.ToString().ToLower()}", ct); public async Task IsChainSupportedAsync(ChainId chainId, CancellationToken ct = default) { try { var c = await GetChainAsync(chainId, ct); return c.Supported; } catch { return false; } } // Asset Operations public async Task> GetSupportedAssetsAsync(ChainId chainId, CancellationToken ct = default) { var r = await GetAsync($"/chains/{chainId.ToString().ToLower()}/assets", ct); return r.Assets ?? new List(); } public async Task GetAssetAsync(string assetId, CancellationToken ct = default) => await GetAsync($"/assets/{Uri.EscapeDataString(assetId)}", ct); public async Task GetWrappedAssetAsync(string originalAssetId, ChainId targetChain, CancellationToken ct = default) => await GetAsync($"/assets/{Uri.EscapeDataString(originalAssetId)}/wrapped/{targetChain.ToString().ToLower()}", ct); // Fee Operations public async Task EstimateFeeAsync(string asset, string amount, ChainId sourceChain, ChainId targetChain, CancellationToken ct = default) => await PostAsync("/fees/estimate", new { asset, amount, sourceChain = sourceChain.ToString().ToLower(), targetChain = targetChain.ToString().ToLower() }, ct); public async Task GetExchangeRateAsync(string fromAsset, string toAsset, CancellationToken ct = default) => await GetAsync($"/rates/{Uri.EscapeDataString(fromAsset)}/{Uri.EscapeDataString(toAsset)}", ct); // Lock-Mint Flow public async Task LockAsync(string asset, string amount, ChainId targetChain, LockOptions? options = null, CancellationToken ct = default) { var body = new Dictionary { ["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("/transfers/lock", body, ct); } public async Task GetLockProofAsync(string lockReceiptId, CancellationToken ct = default) => await GetAsync($"/transfers/lock/{Uri.EscapeDataString(lockReceiptId)}/proof", ct); public async Task 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 MintAsync(LockProof proof, string targetAddress, MintOptions? options = null, CancellationToken ct = default) { var body = new Dictionary { ["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("/transfers/mint", body, ct); } // Burn-Unlock Flow public async Task BurnAsync(string wrappedAsset, string amount, BurnOptions? options = null, CancellationToken ct = default) { var body = new Dictionary { ["wrappedAsset"] = wrappedAsset, ["amount"] = amount }; if (options?.Recipient != null) body["recipient"] = options.Recipient; if (options?.Deadline != null) body["deadline"] = options.Deadline; return await PostAsync("/transfers/burn", body, ct); } public async Task GetBurnProofAsync(string burnReceiptId, CancellationToken ct = default) => await GetAsync($"/transfers/burn/{Uri.EscapeDataString(burnReceiptId)}/proof", ct); public async Task 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 UnlockAsync(BurnProof proof, UnlockOptions? options = null, CancellationToken ct = default) { var body = new Dictionary { ["proof"] = proof }; if (options?.GasLimit != null) body["gasLimit"] = options.GasLimit; if (options?.GasPrice != null) body["gasPrice"] = options.GasPrice; return await PostAsync("/transfers/unlock", body, ct); } // Transfer Management public async Task GetTransferAsync(string transferId, CancellationToken ct = default) => await GetAsync($"/transfers/{Uri.EscapeDataString(transferId)}", ct); public async Task GetTransferStatusAsync(string transferId, CancellationToken ct = default) { var t = await GetTransferAsync(transferId, ct); return t.Status; } public async Task> ListTransfersAsync(TransferFilter? filter = null, CancellationToken ct = default) { var q = new List(); 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(path, ct); return r.Transfers ?? new List(); } public async Task 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 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 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 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 BridgeException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); } } }