synor/sdk/swift/Sources/Synor/Rpc/SynorRpc.swift
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

343 lines
11 KiB
Swift

import Foundation
/// Synor RPC SDK client for Swift.
///
/// Provides blockchain queries, transaction submission, and real-time
/// subscriptions via WebSocket.
///
/// Example:
/// ```swift
/// let rpc = SynorRpc(config: RpcConfig(apiKey: "your-api-key"))
///
/// // Get latest block
/// let block = try await rpc.getLatestBlock()
/// print("Latest block: \(block.height)")
///
/// // Subscribe to new blocks
/// let subscription = try await rpc.subscribeBlocks { block in
/// print("New block: \(block.height)")
/// }
///
/// // Later: cancel subscription
/// subscription.cancel()
/// ```
public class SynorRpc {
private let config: RpcConfig
private let session: URLSession
private let decoder: JSONDecoder
private let encoder: JSONEncoder
private var wsTask: URLSessionWebSocketTask?
private var subscriptions: [String: (String) -> Void] = [:]
private let subscriptionLock = NSLock()
public init(config: RpcConfig) {
self.config = config
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = config.timeout
sessionConfig.timeoutIntervalForResource = config.timeout * 2
self.session = URLSession(configuration: sessionConfig)
self.decoder = JSONDecoder()
self.encoder = JSONEncoder()
}
deinit {
wsTask?.cancel(with: .goingAway, reason: nil)
}
// MARK: - Block Operations
/// Get the latest block.
public func getLatestBlock() async throws -> Block {
return try await get(path: "/blocks/latest")
}
/// Get a block by hash or height.
public func getBlock(hashOrHeight: String) async throws -> Block {
return try await get(path: "/blocks/\(hashOrHeight)")
}
/// Get a block header by hash or height.
public func getBlockHeader(hashOrHeight: String) async throws -> BlockHeader {
return try await get(path: "/blocks/\(hashOrHeight)/header")
}
/// Get blocks in a range.
public func getBlocks(startHeight: Int64, endHeight: Int64) async throws -> [Block] {
return try await get(path: "/blocks?start=\(startHeight)&end=\(endHeight)")
}
// MARK: - Transaction Operations
/// Get a transaction by ID.
public func getTransaction(txid: String) async throws -> RpcTransaction {
return try await get(path: "/transactions/\(txid)")
}
/// Get raw transaction hex.
public func getRawTransaction(txid: String) async throws -> String {
let response: RawTxResponse = try await get(path: "/transactions/\(txid)/raw")
return response.hex
}
/// Send a raw transaction.
public func sendRawTransaction(hex: String) async throws -> SubmitResult {
let request = SendRawTxRequest(hex: hex)
return try await post(path: "/transactions/send", body: request)
}
/// Decode a raw transaction without broadcasting.
public func decodeRawTransaction(hex: String) async throws -> RpcTransaction {
let request = DecodeRawTxRequest(hex: hex)
return try await post(path: "/transactions/decode", body: request)
}
/// Get transactions for an address.
public func getAddressTransactions(
address: String,
limit: Int = 50,
offset: Int = 0
) async throws -> [RpcTransaction] {
return try await get(path: "/addresses/\(address)/transactions?limit=\(limit)&offset=\(offset)")
}
// MARK: - Fee Estimation
/// Estimate fee for a given priority.
public func estimateFee(priority: RpcPriority = .medium) async throws -> FeeEstimate {
return try await get(path: "/fees/estimate?priority=\(priority.rawValue)")
}
/// Get all fee estimates.
public func getAllFeeEstimates() async throws -> [RpcPriority: FeeEstimate] {
let estimates: [FeeEstimate] = try await get(path: "/fees/estimates")
return Dictionary(uniqueKeysWithValues: estimates.map { ($0.priority, $0) })
}
// MARK: - Chain Information
/// Get chain information.
public func getChainInfo() async throws -> ChainInfo {
return try await get(path: "/chain/info")
}
/// Get mempool information.
public func getMempoolInfo() async throws -> MempoolInfo {
return try await get(path: "/mempool/info")
}
/// Get mempool transactions.
public func getMempoolTransactions(limit: Int = 100) async throws -> [String] {
return try await get(path: "/mempool/transactions?limit=\(limit)")
}
// MARK: - WebSocket Subscriptions
/// Subscribe to new blocks.
public func subscribeBlocks(callback: @escaping (Block) -> Void) async throws -> Subscription {
return try await subscribe(channel: "blocks", filter: nil) { [weak self] data in
guard let self = self else { return }
if let block = try? self.decoder.decode(Block.self, from: Data(data.utf8)) {
callback(block)
}
}
}
/// Subscribe to transactions for a specific address.
public func subscribeAddress(
address: String,
callback: @escaping (RpcTransaction) -> Void
) async throws -> Subscription {
return try await subscribe(channel: "address", filter: address) { [weak self] data in
guard let self = self else { return }
if let tx = try? self.decoder.decode(RpcTransaction.self, from: Data(data.utf8)) {
callback(tx)
}
}
}
/// Subscribe to mempool transactions.
public func subscribeMempool(callback: @escaping (RpcTransaction) -> Void) async throws -> Subscription {
return try await subscribe(channel: "mempool", filter: nil) { [weak self] data in
guard let self = self else { return }
if let tx = try? self.decoder.decode(RpcTransaction.self, from: Data(data.utf8)) {
callback(tx)
}
}
}
private func subscribe(
channel: String,
filter: String?,
callback: @escaping (String) -> Void
) async throws -> Subscription {
try await ensureWebSocketConnection()
let subscriptionId = UUID().uuidString
subscriptionLock.lock()
subscriptions[subscriptionId] = callback
subscriptionLock.unlock()
let message = WsMessage(type: "subscribe", channel: channel, data: nil, filter: filter)
let messageData = try encoder.encode(message)
let messageString = String(data: messageData, encoding: .utf8)!
try await wsTask?.send(.string(messageString))
return Subscription(id: subscriptionId, channel: channel) { [weak self] in
self?.subscriptionLock.lock()
self?.subscriptions.removeValue(forKey: subscriptionId)
self?.subscriptionLock.unlock()
Task {
let unsubMessage = WsMessage(type: "unsubscribe", channel: channel, data: nil, filter: nil)
if let data = try? self?.encoder.encode(unsubMessage),
let str = String(data: data, encoding: .utf8) {
try? await self?.wsTask?.send(.string(str))
}
}
}
}
private func ensureWebSocketConnection() async throws {
guard wsTask == nil || wsTask?.state != .running else { return }
guard let url = URL(string: "\(config.wsEndpoint)?token=\(config.apiKey)") else {
throw RpcError.invalidResponse
}
wsTask = session.webSocketTask(with: url)
wsTask?.resume()
// Start receiving messages
receiveMessages()
}
private func receiveMessages() {
wsTask?.receive { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let message):
switch message {
case .string(let text):
self.handleWebSocketMessage(text)
case .data(let data):
if let text = String(data: data, encoding: .utf8) {
self.handleWebSocketMessage(text)
}
@unknown default:
break
}
// Continue receiving
self.receiveMessages()
case .failure(let error):
if self.config.debug {
print("WebSocket error: \(error)")
}
}
}
}
private func handleWebSocketMessage(_ text: String) {
guard let data = text.data(using: .utf8),
let message = try? decoder.decode(WsMessage.self, from: data),
message.type == "data",
let messageData = message.data else {
return
}
subscriptionLock.lock()
let callbacks = Array(subscriptions.values)
subscriptionLock.unlock()
for callback in callbacks {
callback(messageData)
}
}
// MARK: - HTTP Methods
private func get<T: Decodable>(path: String) async throws -> T {
return try await executeWithRetry {
try await self.performRequest(method: "GET", path: path, body: nil as Empty?)
}
}
private func post<T: Decodable, R: Encodable>(path: String, body: R) async throws -> T {
return try await executeWithRetry {
try await self.performRequest(method: "POST", path: path, body: body)
}
}
private func performRequest<T: Decodable, R: Encodable>(
method: String,
path: String,
body: R?
) async throws -> T {
guard let url = URL(string: "\(config.endpoint)\(path)") else {
throw RpcError.invalidResponse
}
var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let body = body {
do {
request.httpBody = try encoder.encode(body)
} catch {
throw RpcError.encodingError(error)
}
}
let (data, response): (Data, URLResponse)
do {
(data, response) = try await session.data(for: request)
} catch {
throw RpcError.networkError(error)
}
guard let httpResponse = response as? HTTPURLResponse else {
throw RpcError.invalidResponse
}
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
do {
return try decoder.decode(T.self, from: data)
} catch {
throw RpcError.decodingError(error)
}
}
let message = String(data: data, encoding: .utf8) ?? "Unknown error"
throw RpcError.httpError(statusCode: httpResponse.statusCode, message: message)
}
private func executeWithRetry<T>(_ operation: () async throws -> T) async throws -> T {
var lastError: Error?
for attempt in 0..<config.retries {
do {
return try await operation()
} catch {
lastError = error
if config.debug {
print("Attempt \(attempt + 1) failed: \(error)")
}
if attempt < config.retries - 1 {
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (attempt + 1)))
}
}
}
throw lastError ?? RpcError.invalidResponse
}
}
// Helper type for empty body
private struct Empty: Encodable {}