# frozen_string_literal: true require "faraday" require "json" require "uri" module SynorMining # Synor Mining SDK client for Ruby. # Pool connections, block templates, hashrate stats, and GPU management. class Client attr_reader :closed, :active_connection def initialize(config) @config = config @closed = false @active_connection = nil @conn = Faraday.new(url: config.endpoint) do |f| f.request :json f.response :json f.options.timeout = config.timeout f.headers["Authorization"] = "Bearer #{config.api_key}" f.headers["Content-Type"] = "application/json" f.headers["X-SDK-Version"] = "ruby/#{VERSION}" end end # ==================== Pool Operations ==================== def connect(pool:) body = { url: pool.url, user: pool.user } body[:password] = pool.password if pool.password body[:algorithm] = pool.algorithm if pool.algorithm body[:difficulty] = pool.difficulty if pool.difficulty response = post("/pool/connect", body) @active_connection = parse_stratum_connection(response) end def disconnect return unless @active_connection post("/pool/disconnect", {}) @active_connection = nil end def get_connection response = get("/pool/connection") @active_connection = parse_stratum_connection(response) end def get_pool_stats response = get("/pool/stats") parse_pool_stats(response) end def connected? @active_connection&.status == ConnectionStatus::CONNECTED end # ==================== Mining Operations ==================== def get_block_template response = get("/mining/template") parse_block_template(response) end def submit_work(work:) body = { template_id: work.template_id, nonce: work.nonce, extra_nonce: work.extra_nonce, timestamp: work.timestamp, hash: work.hash } response = post("/mining/submit", body) parse_submit_result(response) end def start_mining(algorithm: nil) body = algorithm ? { algorithm: algorithm } : {} post("/mining/start", body) end def stop_mining post("/mining/stop", {}) end def mining? response = get("/mining/status") response["mining"] == true end # ==================== Stats Operations ==================== def get_hashrate response = get("/stats/hashrate") parse_hashrate(response) end def get_stats response = get("/stats") parse_mining_stats(response) end def get_earnings(period: nil) path = period ? "/stats/earnings?period=#{period}" : "/stats/earnings" response = get(path) parse_earnings(response) end def get_share_stats response = get("/stats/shares") parse_share_stats(response) end # ==================== Device Operations ==================== def list_devices response = get("/devices") (response["devices"] || []).map { |d| parse_mining_device(d) } end def get_device(device_id) response = get("/devices/#{encode(device_id)}") parse_mining_device(response) end def set_device_config(device_id:, config:) body = { enabled: config.enabled } body[:intensity] = config.intensity if config.intensity body[:power_limit] = config.power_limit if config.power_limit body[:core_clock_offset] = config.core_clock_offset if config.core_clock_offset body[:memory_clock_offset] = config.memory_clock_offset if config.memory_clock_offset body[:fan_speed] = config.fan_speed if config.fan_speed response = put("/devices/#{encode(device_id)}/config", body) parse_mining_device(response) end def enable_device(device_id) set_device_config(device_id: device_id, config: DeviceConfig.new(enabled: true)) end def disable_device(device_id) set_device_config(device_id: device_id, config: DeviceConfig.new(enabled: false)) end # ==================== Worker Operations ==================== def list_workers response = get("/workers") (response["workers"] || []).map { |w| parse_worker_info(w) } end def get_worker(worker_id) response = get("/workers/#{encode(worker_id)}") parse_worker_info(response) end def remove_worker(worker_id) delete("/workers/#{encode(worker_id)}") end # ==================== Algorithm Operations ==================== def list_algorithms response = get("/algorithms") (response["algorithms"] || []).map { |a| parse_mining_algorithm(a) } end def get_algorithm(name) response = get("/algorithms/#{encode(name)}") parse_mining_algorithm(response) end def switch_algorithm(algorithm) post("/algorithms/switch", { algorithm: algorithm }) end def get_most_profitable response = get("/algorithms/profitable") parse_mining_algorithm(response) end # ==================== Lifecycle ==================== def health_check response = get("/health") response["status"] == "healthy" rescue StandardError false end def close @closed = true @active_connection = nil @conn.close if @conn.respond_to?(:close) end private def get(path, params = {}) execute { @conn.get(path, params).body } end def post(path, body) execute { @conn.post(path, body).body } end def put(path, body) execute { @conn.put(path, body).body } end def delete(path) execute { @conn.delete(path).body } end def execute raise ClientClosedError, "Client has been closed" if @closed last_error = nil @config.retries.times do |attempt| begin response = yield check_error(response) if response.is_a?(Hash) return response rescue StandardError => e last_error = e sleep(2**attempt) if attempt < @config.retries - 1 end end raise last_error end def check_error(response) return unless response["error"] || (response["code"] && response["message"]) message = response["message"] || response["error"] || "Unknown error" code = response["code"] status = response["status_code"] || 0 raise HttpError.new(message, status_code: status, code: code) end def encode(str) URI.encode_www_form_component(str) end # Parsing methods def parse_stratum_connection(data) StratumConnection.new( id: data["id"], pool: data["pool"], status: data["status"], algorithm: data["algorithm"], difficulty: data["difficulty"], connected_at: data["connected_at"], accepted_shares: data["accepted_shares"], rejected_shares: data["rejected_shares"], stale_shares: data["stale_shares"], last_share_at: data["last_share_at"] ) end def parse_pool_stats(data) PoolStats.new( url: data["url"], workers: data["workers"], hashrate: data["hashrate"], difficulty: data["difficulty"], last_block: data["last_block"], blocks_found_24h: data["blocks_found_24h"], luck: data["luck"] ) end def parse_block_template(data) BlockTemplate.new( id: data["id"], previous_block_hash: data["previous_block_hash"], merkle_root: data["merkle_root"], timestamp: data["timestamp"], bits: data["bits"], height: data["height"], coinbase_value: data["coinbase_value"], transactions: (data["transactions"] || []).map { |t| parse_template_transaction(t) }, target: data["target"], algorithm: data["algorithm"], extra_nonce: data["extra_nonce"] ) end def parse_template_transaction(data) TemplateTransaction.new( txid: data["txid"], data: data["data"], fee: data["fee"], weight: data["weight"] ) end def parse_submit_result(data) SubmitResult.new( status: data["status"], share: data["share"] ? parse_share_info(data["share"]) : nil, block_found: data["block_found"], reason: data["reason"], block_hash: data["block_hash"], reward: data["reward"] ) end def parse_share_info(data) ShareInfo.new( hash: data["hash"], difficulty: data["difficulty"], timestamp: data["timestamp"], accepted: data["accepted"] ) end def parse_hashrate(data) Hashrate.new( current: data["current"], average_1h: data["average_1h"], average_24h: data["average_24h"], peak: data["peak"], unit: data["unit"] ) end def parse_share_stats(data) ShareStats.new( accepted: data["accepted"], rejected: data["rejected"], stale: data["stale"], total: data["total"], accept_rate: data["accept_rate"] ) end def parse_mining_stats(data) MiningStats.new( hashrate: data["hashrate"] ? parse_hashrate(data["hashrate"]) : nil, shares: data["shares"] ? parse_share_stats(data["shares"]) : nil, uptime: data["uptime"], efficiency: data["efficiency"], earnings: data["earnings"] ? parse_earnings_snapshot(data["earnings"]) : nil, power_consumption: data["power_consumption"], temperature: data["temperature"] ? parse_device_temperature(data["temperature"]) : nil ) end def parse_earnings_snapshot(data) EarningsSnapshot.new( today: data["today"], yesterday: data["yesterday"], this_week: data["this_week"], this_month: data["this_month"], total: data["total"], currency: data["currency"] ) end def parse_device_temperature(data) DeviceTemperature.new( current: data["current"], max: data["max"], throttling: data["throttling"] ) end def parse_earnings(data) Earnings.new( period: data["period"], start_date: data["start_date"], end_date: data["end_date"], amount: data["amount"], blocks: data["blocks"], shares: data["shares"], average_hashrate: data["average_hashrate"], currency: data["currency"], breakdown: (data["breakdown"] || []).map { |b| parse_earnings_breakdown(b) } ) end def parse_earnings_breakdown(data) EarningsBreakdown.new( date: data["date"], amount: data["amount"], blocks: data["blocks"], shares: data["shares"], hashrate: data["hashrate"] ) end def parse_mining_device(data) MiningDevice.new( id: data["id"], name: data["name"], type: data["type"], status: data["status"], hashrate: data["hashrate"], temperature: data["temperature"], fan_speed: data["fan_speed"], power_draw: data["power_draw"], memory_used: data["memory_used"], memory_total: data["memory_total"], driver: data["driver"], firmware: data["firmware"] ) end def parse_worker_info(data) WorkerInfo.new( id: data["id"], name: data["name"], status: data["status"], hashrate: data["hashrate"] ? parse_hashrate(data["hashrate"]) : nil, shares: data["shares"] ? parse_share_stats(data["shares"]) : nil, devices: (data["devices"] || []).map { |d| parse_mining_device(d) }, last_seen: data["last_seen"], uptime: data["uptime"] ) end def parse_mining_algorithm(data) MiningAlgorithm.new( name: data["name"], display_name: data["display_name"], hash_unit: data["hash_unit"], profitability: data["profitability"], difficulty: data["difficulty"], block_reward: data["block_reward"], block_time: data["block_time"] ) end end end