synor/sdk/csharp/Synor.Sdk/Rpc/SynorRpc.cs
Gulshan Yadav 74b82d2bb2 Add Synor Storage and Wallet SDKs for Swift
- Implement SynorStorage class for decentralized storage operations including upload, download, pinning, and CAR file management.
- Create supporting types and models for storage operations such as UploadOptions, Pin, and StorageConfig.
- Implement SynorWallet class for wallet operations including wallet creation, address generation, transaction signing, and balance queries.
- Create supporting types and models for wallet operations such as Wallet, Address, and Transaction.
- Introduce error handling for both storage and wallet operations.
2026-01-27 01:56:45 +05:30

477 lines
15 KiB
C#

using System.Net.Http.Json;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace Synor.Sdk.Rpc;
/// <summary>
/// Synor RPC SDK client for C#.
///
/// Provides blockchain data queries, transaction submission,
/// and real-time subscriptions via WebSocket.
/// </summary>
/// <example>
/// <code>
/// var rpc = new SynorRpc(new RpcConfig { ApiKey = "your-api-key" });
///
/// // Get latest block
/// var block = await rpc.GetLatestBlockAsync();
/// Console.WriteLine($"Block height: {block.Height}");
///
/// // Subscribe to new blocks
/// var subscription = await rpc.SubscribeBlocksAsync(block => {
/// Console.WriteLine($"New block: {block.Height}");
/// });
///
/// // Later: cancel subscription
/// subscription.Cancel();
/// rpc.Dispose();
/// </code>
/// </example>
public class SynorRpc : IDisposable
{
private readonly RpcConfig _config;
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
private ClientWebSocket? _webSocket;
private CancellationTokenSource? _wsCancellation;
private readonly Dictionary<string, Action<JsonElement>> _subscriptionCallbacks = new();
private bool _disposed;
public SynorRpc(RpcConfig config)
{
_config = config;
_httpClient = new HttpClient
{
BaseAddress = new Uri(config.Endpoint),
Timeout = config.Timeout
};
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
}
/// <summary>
/// Get the latest block.
/// </summary>
public async Task<Block> GetLatestBlockAsync(CancellationToken cancellationToken = default)
{
return await GetAsync<Block>("/blocks/latest", cancellationToken);
}
/// <summary>
/// Get a block by hash or height.
/// </summary>
public async Task<Block> GetBlockAsync(
string hashOrHeight,
CancellationToken cancellationToken = default)
{
return await GetAsync<Block>($"/blocks/{hashOrHeight}", cancellationToken);
}
/// <summary>
/// Get a block header by hash or height.
/// </summary>
public async Task<BlockHeader> GetBlockHeaderAsync(
string hashOrHeight,
CancellationToken cancellationToken = default)
{
return await GetAsync<BlockHeader>($"/blocks/{hashOrHeight}/header", cancellationToken);
}
/// <summary>
/// Get a range of blocks.
/// </summary>
public async Task<List<Block>> GetBlocksAsync(
long startHeight,
long endHeight,
CancellationToken cancellationToken = default)
{
return await GetAsync<List<Block>>(
$"/blocks?start={startHeight}&end={endHeight}",
cancellationToken);
}
/// <summary>
/// Get a transaction by ID.
/// </summary>
public async Task<RpcTransaction> GetTransactionAsync(
string txid,
CancellationToken cancellationToken = default)
{
return await GetAsync<RpcTransaction>($"/transactions/{txid}", cancellationToken);
}
/// <summary>
/// Get raw transaction hex.
/// </summary>
public async Task<string> GetRawTransactionAsync(
string txid,
CancellationToken cancellationToken = default)
{
var response = await GetAsync<JsonElement>($"/transactions/{txid}/raw", cancellationToken);
return response.GetProperty("hex").GetString() ?? throw new RpcException("Invalid response");
}
/// <summary>
/// Send a raw transaction.
/// </summary>
public async Task<SubmitResult> SendRawTransactionAsync(
string hex,
CancellationToken cancellationToken = default)
{
var request = new { hex };
return await PostAsync<SubmitResult>("/transactions/send", request, cancellationToken);
}
/// <summary>
/// Decode a raw transaction.
/// </summary>
public async Task<RpcTransaction> DecodeRawTransactionAsync(
string hex,
CancellationToken cancellationToken = default)
{
var request = new { hex };
return await PostAsync<RpcTransaction>("/transactions/decode", request, cancellationToken);
}
/// <summary>
/// Get transactions for an address.
/// </summary>
public async Task<List<RpcTransaction>> GetAddressTransactionsAsync(
string address,
int limit = 20,
int offset = 0,
CancellationToken cancellationToken = default)
{
return await GetAsync<List<RpcTransaction>>(
$"/addresses/{address}/transactions?limit={limit}&offset={offset}",
cancellationToken);
}
/// <summary>
/// Estimate transaction fee.
/// </summary>
public async Task<FeeEstimate> EstimateFeeAsync(
RpcPriority priority = RpcPriority.Medium,
CancellationToken cancellationToken = default)
{
return await GetAsync<FeeEstimate>(
$"/fees/estimate?priority={priority.ToString().ToLower()}",
cancellationToken);
}
/// <summary>
/// Get all fee estimates.
/// </summary>
public async Task<Dictionary<RpcPriority, FeeEstimate>> GetAllFeeEstimatesAsync(
CancellationToken cancellationToken = default)
{
var estimates = await GetAsync<List<FeeEstimate>>("/fees/estimates", cancellationToken);
return estimates.ToDictionary(e => e.Priority, e => e);
}
/// <summary>
/// Get chain information.
/// </summary>
public async Task<ChainInfo> GetChainInfoAsync(CancellationToken cancellationToken = default)
{
return await GetAsync<ChainInfo>("/chain/info", cancellationToken);
}
/// <summary>
/// Get mempool information.
/// </summary>
public async Task<MempoolInfo> GetMempoolInfoAsync(CancellationToken cancellationToken = default)
{
return await GetAsync<MempoolInfo>("/mempool/info", cancellationToken);
}
/// <summary>
/// Get mempool transaction IDs.
/// </summary>
public async Task<List<string>> GetMempoolTransactionsAsync(
int limit = 100,
CancellationToken cancellationToken = default)
{
return await GetAsync<List<string>>($"/mempool/transactions?limit={limit}", cancellationToken);
}
/// <summary>
/// Subscribe to new blocks.
/// </summary>
public async Task<Subscription> SubscribeBlocksAsync(
Action<Block> callback,
CancellationToken cancellationToken = default)
{
await EnsureWebSocketConnectedAsync(cancellationToken);
var subscriptionId = Guid.NewGuid().ToString();
_subscriptionCallbacks[subscriptionId] = element =>
{
var block = element.Deserialize<Block>(_jsonOptions);
if (block != null) callback(block);
};
var subscribeMessage = JsonSerializer.Serialize(new
{
type = "subscribe",
channel = "blocks",
subscription_id = subscriptionId
});
await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);
return new Subscription(subscriptionId, "blocks", () =>
{
_subscriptionCallbacks.Remove(subscriptionId);
var unsubscribeMessage = JsonSerializer.Serialize(new
{
type = "unsubscribe",
subscription_id = subscriptionId
});
_ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
});
}
/// <summary>
/// Subscribe to address transactions.
/// </summary>
public async Task<Subscription> SubscribeAddressAsync(
string address,
Action<RpcTransaction> callback,
CancellationToken cancellationToken = default)
{
await EnsureWebSocketConnectedAsync(cancellationToken);
var subscriptionId = Guid.NewGuid().ToString();
_subscriptionCallbacks[subscriptionId] = element =>
{
var tx = element.Deserialize<RpcTransaction>(_jsonOptions);
if (tx != null) callback(tx);
};
var subscribeMessage = JsonSerializer.Serialize(new
{
type = "subscribe",
channel = "address",
address,
subscription_id = subscriptionId
});
await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);
return new Subscription(subscriptionId, $"address:{address}", () =>
{
_subscriptionCallbacks.Remove(subscriptionId);
var unsubscribeMessage = JsonSerializer.Serialize(new
{
type = "unsubscribe",
subscription_id = subscriptionId
});
_ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
});
}
/// <summary>
/// Subscribe to mempool transactions.
/// </summary>
public async Task<Subscription> SubscribeMempoolAsync(
Action<RpcTransaction> callback,
CancellationToken cancellationToken = default)
{
await EnsureWebSocketConnectedAsync(cancellationToken);
var subscriptionId = Guid.NewGuid().ToString();
_subscriptionCallbacks[subscriptionId] = element =>
{
var tx = element.Deserialize<RpcTransaction>(_jsonOptions);
if (tx != null) callback(tx);
};
var subscribeMessage = JsonSerializer.Serialize(new
{
type = "subscribe",
channel = "mempool",
subscription_id = subscriptionId
});
await SendWebSocketMessageAsync(subscribeMessage, cancellationToken);
return new Subscription(subscriptionId, "mempool", () =>
{
_subscriptionCallbacks.Remove(subscriptionId);
var unsubscribeMessage = JsonSerializer.Serialize(new
{
type = "unsubscribe",
subscription_id = subscriptionId
});
_ = SendWebSocketMessageAsync(unsubscribeMessage, CancellationToken.None);
});
}
private async Task EnsureWebSocketConnectedAsync(CancellationToken cancellationToken)
{
if (_webSocket?.State == WebSocketState.Open)
return;
_webSocket?.Dispose();
_wsCancellation?.Cancel();
_wsCancellation?.Dispose();
_webSocket = new ClientWebSocket();
_webSocket.Options.SetRequestHeader("Authorization", $"Bearer {_config.ApiKey}");
_wsCancellation = new CancellationTokenSource();
await _webSocket.ConnectAsync(new Uri(_config.WsEndpoint), cancellationToken);
_ = ReceiveWebSocketMessagesAsync(_wsCancellation.Token);
}
private async Task ReceiveWebSocketMessagesAsync(CancellationToken cancellationToken)
{
var buffer = new byte[8192];
try
{
while (_webSocket?.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
var result = await _webSocket.ReceiveAsync(buffer, cancellationToken);
if (result.MessageType == WebSocketMessageType.Close)
break;
if (result.MessageType == WebSocketMessageType.Text)
{
var json = Encoding.UTF8.GetString(buffer, 0, result.Count);
ProcessWebSocketMessage(json);
}
}
}
catch (OperationCanceledException)
{
// Expected when cancelling
}
catch (Exception ex)
{
if (_config.Debug)
{
Console.WriteLine($"WebSocket error: {ex.Message}");
}
}
}
private void ProcessWebSocketMessage(string json)
{
try
{
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.TryGetProperty("subscription_id", out var subIdElement))
{
var subscriptionId = subIdElement.GetString();
if (subscriptionId != null && _subscriptionCallbacks.TryGetValue(subscriptionId, out var callback))
{
if (root.TryGetProperty("data", out var data))
{
callback(data);
}
}
}
}
catch (Exception ex)
{
if (_config.Debug)
{
Console.WriteLine($"Error processing WebSocket message: {ex.Message}");
}
}
}
private async Task SendWebSocketMessageAsync(string message, CancellationToken cancellationToken)
{
if (_webSocket?.State == WebSocketState.Open)
{
var bytes = Encoding.UTF8.GetBytes(message);
await _webSocket.SendAsync(bytes, WebSocketMessageType.Text, true, cancellationToken);
}
}
private async Task<T> GetAsync<T>(string path, CancellationToken cancellationToken)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.GetAsync(path, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new RpcException("Invalid response");
});
}
private async Task<T> PostAsync<T>(string path, object body, CancellationToken cancellationToken)
{
return await ExecuteWithRetryAsync(async () =>
{
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, cancellationToken);
await EnsureSuccessAsync(response);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new RpcException("Invalid response");
});
}
private async Task EnsureSuccessAsync(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new RpcException(
$"HTTP error: {content}",
statusCode: (int)response.StatusCode);
}
}
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
{
Exception? lastException = null;
for (int attempt = 0; attempt < _config.Retries; attempt++)
{
try
{
return await operation();
}
catch (Exception ex)
{
lastException = ex;
if (_config.Debug)
{
Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}");
}
if (attempt < _config.Retries - 1)
{
await Task.Delay(TimeSpan.FromSeconds(attempt + 1));
}
}
}
throw lastException ?? new RpcException("Unknown error after retries");
}
public void Dispose()
{
if (!_disposed)
{
_wsCancellation?.Cancel();
_wsCancellation?.Dispose();
_webSocket?.Dispose();
_httpClient.Dispose();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}