synor/sdk/ruby/lib/synor_rpc/client.rb
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

384 lines
9.5 KiB
Ruby

# frozen_string_literal: true
require "faraday"
require "json"
module SynorRpc
# Synor RPC SDK Client
#
# @example
# client = SynorRpc::Client.new(api_key: 'your-api-key')
#
# # Get latest block
# block = client.get_latest_block
#
# # Get transaction
# tx = client.get_transaction(txid)
#
class Client
attr_reader :config
def initialize(api_key: nil, **options)
@config = Config.new(api_key: api_key, **options)
raise ArgumentError, "API key is required" unless @config.api_key
@conn = Faraday.new(url: @config.base_url) do |f|
f.request :json
f.response :json
f.options.timeout = @config.timeout
f.headers["Authorization"] = "Bearer #{@config.api_key}"
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
end
@closed = false
@subscriptions = {}
@ws = nil
end
# ==================== Block Operations ====================
# Get the latest block
#
# @return [Block]
def get_latest_block
check_closed!
response = with_retry { @conn.get("/blocks/latest") }
Block.from_hash(response.body)
end
# Get a block by hash or height
#
# @param hash_or_height [String, Integer]
# @return [Block]
def get_block(hash_or_height)
check_closed!
response = with_retry { @conn.get("/blocks/#{hash_or_height}") }
Block.from_hash(response.body)
end
# Get a block header
#
# @param hash_or_height [String, Integer]
# @return [BlockHeader]
def get_block_header(hash_or_height)
check_closed!
response = with_retry { @conn.get("/blocks/#{hash_or_height}/header") }
BlockHeader.from_hash(response.body)
end
# Get a range of blocks
#
# @param start_height [Integer]
# @param end_height [Integer]
# @return [Array<Block>]
def get_blocks(start_height, end_height)
check_closed!
response = with_retry do
@conn.get("/blocks", start: start_height, end: end_height)
end
response.body.map { |b| Block.from_hash(b) }
end
# ==================== Transaction Operations ====================
# Get a transaction by ID
#
# @param txid [String]
# @return [Transaction]
def get_transaction(txid)
check_closed!
response = with_retry { @conn.get("/transactions/#{txid}") }
Transaction.from_hash(response.body)
end
# Get raw transaction hex
#
# @param txid [String]
# @return [String]
def get_raw_transaction(txid)
check_closed!
response = with_retry { @conn.get("/transactions/#{txid}/raw") }
response.body["hex"]
end
# Send a raw transaction
#
# @param hex [String]
# @return [SubmitResult]
def send_raw_transaction(hex)
check_closed!
response = with_retry { @conn.post("/transactions/send", { hex: hex }) }
SubmitResult.from_hash(response.body)
end
# Decode a raw transaction
#
# @param hex [String]
# @return [Transaction]
def decode_raw_transaction(hex)
check_closed!
response = with_retry { @conn.post("/transactions/decode", { hex: hex }) }
Transaction.from_hash(response.body)
end
# Get transactions for an address
#
# @param address [String]
# @param limit [Integer]
# @param offset [Integer]
# @return [Array<Transaction>]
def get_address_transactions(address, limit: 20, offset: 0)
check_closed!
response = with_retry do
@conn.get("/addresses/#{address}/transactions", limit: limit, offset: offset)
end
response.body.map { |t| Transaction.from_hash(t) }
end
# ==================== Fee Estimation ====================
# Estimate fee
#
# @param priority [String]
# @return [FeeEstimate]
def estimate_fee(priority: Priority::MEDIUM)
check_closed!
response = with_retry { @conn.get("/fees/estimate", priority: priority) }
FeeEstimate.from_hash(response.body)
end
# Get all fee estimates
#
# @return [Hash<String, FeeEstimate>]
def get_all_fee_estimates
check_closed!
response = with_retry { @conn.get("/fees/estimates") }
response.body.to_h { |e| [e["priority"], FeeEstimate.from_hash(e)] }
end
# ==================== Chain Information ====================
# Get chain info
#
# @return [ChainInfo]
def get_chain_info
check_closed!
response = with_retry { @conn.get("/chain/info") }
ChainInfo.from_hash(response.body)
end
# Get mempool info
#
# @return [MempoolInfo]
def get_mempool_info
check_closed!
response = with_retry { @conn.get("/mempool/info") }
MempoolInfo.from_hash(response.body)
end
# Get mempool transaction IDs
#
# @param limit [Integer]
# @return [Array<String>]
def get_mempool_transactions(limit: 100)
check_closed!
response = with_retry { @conn.get("/mempool/transactions", limit: limit) }
response.body
end
# ==================== Subscriptions ====================
# Subscribe to new blocks
#
# @yield [Block] Called for each new block
# @return [Subscription]
def subscribe_blocks(&block)
check_closed!
raise ArgumentError, "Block required" unless block_given?
ensure_websocket_connected
subscription_id = SecureRandom.uuid
@subscriptions[subscription_id] = {
channel: "blocks",
callback: ->(data) { block.call(Block.from_hash(data)) }
}
send_ws_message({
type: "subscribe",
channel: "blocks",
subscription_id: subscription_id
})
Subscription.new(id: subscription_id, channel: "blocks") do
@subscriptions.delete(subscription_id)
send_ws_message({
type: "unsubscribe",
subscription_id: subscription_id
})
end
end
# Subscribe to address transactions
#
# @param address [String]
# @yield [Transaction] Called for each transaction
# @return [Subscription]
def subscribe_address(address, &block)
check_closed!
raise ArgumentError, "Block required" unless block_given?
ensure_websocket_connected
subscription_id = SecureRandom.uuid
@subscriptions[subscription_id] = {
channel: "address:#{address}",
callback: ->(data) { block.call(Transaction.from_hash(data)) }
}
send_ws_message({
type: "subscribe",
channel: "address",
address: address,
subscription_id: subscription_id
})
Subscription.new(id: subscription_id, channel: "address:#{address}") do
@subscriptions.delete(subscription_id)
send_ws_message({
type: "unsubscribe",
subscription_id: subscription_id
})
end
end
# Subscribe to mempool transactions
#
# @yield [Transaction] Called for each transaction
# @return [Subscription]
def subscribe_mempool(&block)
check_closed!
raise ArgumentError, "Block required" unless block_given?
ensure_websocket_connected
subscription_id = SecureRandom.uuid
@subscriptions[subscription_id] = {
channel: "mempool",
callback: ->(data) { block.call(Transaction.from_hash(data)) }
}
send_ws_message({
type: "subscribe",
channel: "mempool",
subscription_id: subscription_id
})
Subscription.new(id: subscription_id, channel: "mempool") do
@subscriptions.delete(subscription_id)
send_ws_message({
type: "unsubscribe",
subscription_id: subscription_id
})
end
end
# ==================== Lifecycle ====================
def close
@closed = true
@ws&.close
@conn.close if @conn.respond_to?(:close)
end
def closed?
@closed
end
private
def check_closed!
raise ClientClosedError, "Client has been closed" if @closed
end
def with_retry
attempts = 0
begin
attempts += 1
response = yield
handle_response(response)
response
rescue Faraday::Error, ApiError => e
if attempts < @config.retries
sleep(attempts)
retry
end
raise
end
end
def handle_response(response)
return if response.success?
error_message = response.body["error"] || response.body["message"] || "Unknown error"
raise ApiError.new(error_message, status_code: response.status)
end
def ensure_websocket_connected
return if @ws && @ws_connected
require "websocket-client-simple"
@ws = WebSocket::Client::Simple.connect(@config.ws_url, headers: {
"Authorization" => "Bearer #{@config.api_key}"
})
@ws.on :message do |msg|
handle_ws_message(msg.data)
end
@ws.on :open do
@ws_connected = true
end
@ws.on :close do
@ws_connected = false
end
@ws.on :error do |e|
puts "WebSocket error: #{e.message}" if @config.debug
end
# Wait for connection
sleep(0.1) until @ws_connected || @closed
end
def send_ws_message(message)
@ws&.send(JSON.generate(message))
end
def handle_ws_message(data)
message = JSON.parse(data)
subscription_id = message["subscription_id"]
if subscription_id && @subscriptions[subscription_id]
@subscriptions[subscription_id][:callback].call(message["data"])
end
rescue JSON::ParserError
# Skip malformed messages
end
end
end