Add Economics, Governance, and Mining SDKs for: - Java: Full SDK with CompletableFuture async operations - Kotlin: Coroutine-based SDK with suspend functions - Swift: Modern Swift SDK with async/await - Flutter/Dart: Complete Dart SDK with Future-based API - C: Header files and implementations with opaque handles - C++: Modern C++17 with std::future and PIMPL pattern - C#: Records, async/await Tasks, and IDisposable - Ruby: Struct-based types with Faraday HTTP client Also includes minor Dart lint fixes (const exceptions).
437 lines
12 KiB
Ruby
437 lines
12 KiB
Ruby
# 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
|