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
307 lines
11 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|