using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Web; namespace Synor.Contract { /// /// Synor Contract SDK client for C#/.NET. /// Smart contract deployment, interaction, and event handling. /// public sealed class ContractClient : IDisposable { public const string Version = "0.1.0"; private readonly ContractConfig _config; private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonOptions; private bool _closed; public ContractClient(ContractConfig config) { _config = config ?? throw new ArgumentNullException(nameof(config)); _httpClient = new HttpClient { BaseAddress = new Uri(config.Endpoint), Timeout = TimeSpan.FromMilliseconds(config.TimeoutMs) }; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", $"csharp/{Version}"); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; } #region Contract Deployment public async Task DeployAsync(DeployContractOptions options, CancellationToken ct = default) { return await PostAsync("/contract/deploy", options, ct); } public async Task DeployCreate2Async(DeployContractOptions options, string salt, CancellationToken ct = default) { var body = new Dictionary { ["bytecode"] = options.Bytecode, ["salt"] = salt, ["abi"] = options.Abi, ["constructor_args"] = options.ConstructorArgs, ["value"] = options.Value, ["gas_limit"] = options.GasLimit, ["gas_price"] = options.GasPrice }; return await PostAsync("/contract/deploy/create2", body, ct); } public async Task PredictAddressAsync(string bytecode, string salt, string? deployer = null, CancellationToken ct = default) { var body = new { bytecode, salt, deployer }; var response = await PostAsync>("/contract/predict-address", body, ct); return response["address"]?.ToString() ?? throw new ContractException("Missing address"); } #endregion #region Contract Interaction public async Task CallAsync(CallContractOptions options, CancellationToken ct = default) { return await PostAsync("/contract/call", options, ct); } public async Task SendAsync(SendContractOptions options, CancellationToken ct = default) { return await PostAsync("/contract/send", options, ct); } #endregion #region Events public async Task GetEventsAsync(EventFilter filter, CancellationToken ct = default) { return await PostAsync("/contract/events", filter, ct); } public async Task GetLogsAsync(string contract, long? fromBlock = null, long? toBlock = null, CancellationToken ct = default) { var path = $"/contract/logs?contract={HttpUtility.UrlEncode(contract)}"; if (fromBlock.HasValue) path += $"&from_block={fromBlock}"; if (toBlock.HasValue) path += $"&to_block={toBlock}"; return await GetAsync(path, ct); } public async Task DecodeLogsAsync(IEnumerable logs, IEnumerable abi, CancellationToken ct = default) { var body = new { logs, abi }; return await PostAsync("/contract/decode-logs", body, ct); } #endregion #region ABI Utilities public async Task EncodeCallAsync(EncodeCallOptions options, CancellationToken ct = default) { var response = await PostAsync>("/contract/encode", options, ct); return response["data"]?.ToString() ?? throw new ContractException("Missing data"); } public async Task DecodeResultAsync(DecodeResultOptions options, CancellationToken ct = default) { var response = await PostAsync>("/contract/decode", options, ct); return response["result"]; } public async Task GetSelectorAsync(string signature, CancellationToken ct = default) { var response = await GetAsync>($"/contract/selector?signature={HttpUtility.UrlEncode(signature)}", ct); return response["selector"]?.ToString() ?? throw new ContractException("Missing selector"); } #endregion #region Gas Estimation public async Task EstimateGasAsync(EstimateGasOptions options, CancellationToken ct = default) { return await PostAsync("/contract/estimate-gas", options, ct); } #endregion #region Contract Information public async Task GetBytecodeAsync(string address, CancellationToken ct = default) { return await GetAsync($"/contract/{HttpUtility.UrlEncode(address)}/bytecode", ct); } public async Task VerifyAsync(VerifyContractOptions options, CancellationToken ct = default) { return await PostAsync("/contract/verify", options, ct); } public async Task GetVerificationStatusAsync(string address, CancellationToken ct = default) { return await GetAsync($"/contract/{HttpUtility.UrlEncode(address)}/verification", ct); } #endregion #region Multicall public async Task MulticallAsync(IEnumerable requests, CancellationToken ct = default) { var body = new { calls = requests }; return await PostAsync("/contract/multicall", body, ct); } #endregion #region Storage public async Task ReadStorageAsync(ReadStorageOptions options, CancellationToken ct = default) { var path = $"/contract/storage?contract={HttpUtility.UrlEncode(options.Contract)}&slot={HttpUtility.UrlEncode(options.Slot)}"; if (options.BlockNumber.HasValue) path += $"&block={options.BlockNumber}"; var response = await GetAsync>(path, ct); return response["value"]?.ToString() ?? throw new ContractException("Missing value"); } #endregion #region Lifecycle public async Task HealthCheckAsync(CancellationToken ct = default) { if (_closed) return false; try { var response = await GetAsync>("/health", ct); return response.TryGetValue("status", out var status) && status is JsonElement elem && elem.GetString() == "healthy"; } catch { return false; } } public void Dispose() { _closed = true; _httpClient.Dispose(); } public bool IsClosed => _closed; #endregion #region HTTP Helpers private async Task GetAsync(string path, CancellationToken ct) { ThrowIfClosed(); return await ExecuteWithRetryAsync(async () => { var response = await _httpClient.GetAsync(path, ct); return await HandleResponseAsync(response, ct); }); } private async Task PostAsync(string path, object body, CancellationToken ct) { ThrowIfClosed(); return await ExecuteWithRetryAsync(async () => { var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); return await HandleResponseAsync(response, ct); }); } private async Task HandleResponseAsync(HttpResponseMessage response, CancellationToken ct) { var content = await response.Content.ReadAsStringAsync(ct); if (response.IsSuccessStatusCode) { return JsonSerializer.Deserialize(content, _jsonOptions) ?? throw new ContractException("Failed to deserialize response"); } try { var error = JsonSerializer.Deserialize>(content, _jsonOptions); var message = error?.GetValueOrDefault("message")?.ToString() ?? error?.GetValueOrDefault("error")?.ToString() ?? "Unknown error"; var code = error?.GetValueOrDefault("code")?.ToString(); throw new ContractException(message, (int)response.StatusCode, code); } catch (JsonException) { throw new ContractException(content, (int)response.StatusCode); } } private async Task ExecuteWithRetryAsync(Func> action) { Exception? lastException = null; for (var attempt = 0; attempt < _config.Retries; attempt++) { try { return await action(); } catch (Exception ex) { lastException = ex; if (attempt < _config.Retries - 1) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); } } } throw lastException ?? new ContractException("Request failed"); } private void ThrowIfClosed() { if (_closed) throw new ContractException("Client has been closed"); } #endregion } public class ContractConfig { public required string ApiKey { get; init; } public string Endpoint { get; init; } = "https://contract.synor.io"; public int TimeoutMs { get; init; } = 30000; public int Retries { get; init; } = 3; } public class ContractException : Exception { public int? StatusCode { get; } public string? Code { get; } public ContractException(string message, int? statusCode = null, string? code = null) : base(message) { StatusCode = statusCode; Code = code; } } }