synor/sdk/csharp/src/Synor.Contract/ContractClient.cs
Gulshan Yadav e65ea40af2 feat: implement Privacy and Contract SDKs for all 12 languages (Phase 5)
Privacy SDK features:
- Confidential transactions with Pedersen commitments
- Bulletproof range proofs for value validation
- Ring signatures for anonymous signing with key images
- Stealth addresses for unlinkable payments
- Blinding factor generation and value operations

Contract SDK features:
- Smart contract deployment (standard and CREATE2)
- Call (view/pure) and Send (state-changing) operations
- Event log filtering, subscription, and decoding
- ABI encoding/decoding utilities
- Gas estimation and contract verification
- Multicall for batched operations
- Storage slot reading

Languages implemented:
- JavaScript/TypeScript
- Python (async with httpx)
- Go
- Rust (async with reqwest/tokio)
- Java (async with OkHttp)
- Kotlin (coroutines with Ktor)
- Swift (async/await with URLSession)
- Flutter/Dart
- C (header-only interface)
- C++ (header-only with std::future)
- C#/.NET (async with HttpClient)
- Ruby (Faraday HTTP client)

All SDKs follow consistent patterns:
- Configuration with API key, endpoint, timeout, retries
- Custom exception types with error codes
- Retry logic with exponential backoff
- Health check endpoints
- Closed state management
2026-01-28 09:03:34 +05:30

307 lines
11 KiB
C#

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
{
/// <summary>
/// Synor Contract SDK client for C#/.NET.
/// Smart contract deployment, interaction, and event handling.
/// </summary>
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<DeploymentResult> DeployAsync(DeployContractOptions options, CancellationToken ct = default)
{
return await PostAsync<DeploymentResult>("/contract/deploy", options, ct);
}
public async Task<DeploymentResult> DeployCreate2Async(DeployContractOptions options, string salt, CancellationToken ct = default)
{
var body = new Dictionary<string, object?>
{
["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<DeploymentResult>("/contract/deploy/create2", body, ct);
}
public async Task<string> PredictAddressAsync(string bytecode, string salt, string? deployer = null, CancellationToken ct = default)
{
var body = new { bytecode, salt, deployer };
var response = await PostAsync<Dictionary<string, object>>("/contract/predict-address", body, ct);
return response["address"]?.ToString() ?? throw new ContractException("Missing address");
}
#endregion
#region Contract Interaction
public async Task<JsonElement> CallAsync(CallContractOptions options, CancellationToken ct = default)
{
return await PostAsync<JsonElement>("/contract/call", options, ct);
}
public async Task<TransactionResult> SendAsync(SendContractOptions options, CancellationToken ct = default)
{
return await PostAsync<TransactionResult>("/contract/send", options, ct);
}
#endregion
#region Events
public async Task<DecodedEvent[]> GetEventsAsync(EventFilter filter, CancellationToken ct = default)
{
return await PostAsync<DecodedEvent[]>("/contract/events", filter, ct);
}
public async Task<EventLog[]> 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<EventLog[]>(path, ct);
}
public async Task<DecodedEvent[]> DecodeLogsAsync(IEnumerable<EventLog> logs, IEnumerable<AbiEntry> abi, CancellationToken ct = default)
{
var body = new { logs, abi };
return await PostAsync<DecodedEvent[]>("/contract/decode-logs", body, ct);
}
#endregion
#region ABI Utilities
public async Task<string> EncodeCallAsync(EncodeCallOptions options, CancellationToken ct = default)
{
var response = await PostAsync<Dictionary<string, object>>("/contract/encode", options, ct);
return response["data"]?.ToString() ?? throw new ContractException("Missing data");
}
public async Task<JsonElement> DecodeResultAsync(DecodeResultOptions options, CancellationToken ct = default)
{
var response = await PostAsync<Dictionary<string, JsonElement>>("/contract/decode", options, ct);
return response["result"];
}
public async Task<string> GetSelectorAsync(string signature, CancellationToken ct = default)
{
var response = await GetAsync<Dictionary<string, object>>($"/contract/selector?signature={HttpUtility.UrlEncode(signature)}", ct);
return response["selector"]?.ToString() ?? throw new ContractException("Missing selector");
}
#endregion
#region Gas Estimation
public async Task<GasEstimation> EstimateGasAsync(EstimateGasOptions options, CancellationToken ct = default)
{
return await PostAsync<GasEstimation>("/contract/estimate-gas", options, ct);
}
#endregion
#region Contract Information
public async Task<BytecodeInfo> GetBytecodeAsync(string address, CancellationToken ct = default)
{
return await GetAsync<BytecodeInfo>($"/contract/{HttpUtility.UrlEncode(address)}/bytecode", ct);
}
public async Task<VerificationResult> VerifyAsync(VerifyContractOptions options, CancellationToken ct = default)
{
return await PostAsync<VerificationResult>("/contract/verify", options, ct);
}
public async Task<VerificationResult> GetVerificationStatusAsync(string address, CancellationToken ct = default)
{
return await GetAsync<VerificationResult>($"/contract/{HttpUtility.UrlEncode(address)}/verification", ct);
}
#endregion
#region Multicall
public async Task<MulticallResult[]> MulticallAsync(IEnumerable<MulticallRequest> requests, CancellationToken ct = default)
{
var body = new { calls = requests };
return await PostAsync<MulticallResult[]>("/contract/multicall", body, ct);
}
#endregion
#region Storage
public async Task<string> 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<Dictionary<string, object>>(path, ct);
return response["value"]?.ToString() ?? throw new ContractException("Missing value");
}
#endregion
#region Lifecycle
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
{
if (_closed) return false;
try
{
var response = await GetAsync<Dictionary<string, object>>("/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<T> GetAsync<T>(string path, CancellationToken ct)
{
ThrowIfClosed();
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.GetAsync(path, ct);
return await HandleResponseAsync<T>(response, ct);
});
}
private async Task<T> PostAsync<T>(string path, object body, CancellationToken ct)
{
ThrowIfClosed();
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct);
return await HandleResponseAsync<T>(response, ct);
});
}
private async Task<T> HandleResponseAsync<T>(HttpResponseMessage response, CancellationToken ct)
{
var content = await response.Content.ReadAsStringAsync(ct);
if (response.IsSuccessStatusCode)
{
return JsonSerializer.Deserialize<T>(content, _jsonOptions)
?? throw new ContractException("Failed to deserialize response");
}
try
{
var error = JsonSerializer.Deserialize<Dictionary<string, object>>(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<T> ExecuteWithRetryAsync<T>(Func<Task<T>> 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;
}
}
}