Phase 4 implementation - adds three new service SDKs: Economics SDK: - Pricing calculations for all service types - Billing and invoice management - Staking with APY rewards and vesting - Discount code system Governance SDK: - Proposal lifecycle (create, vote, execute, cancel) - Voting power with delegation support - DAO creation (token, multisig, hybrid types) - Vesting schedules with cliff periods Mining SDK: - Stratum pool connections - Block template retrieval and work submission - Hashrate and earnings statistics - GPU device management and configuration - Worker management and algorithm switching Languages: JavaScript, Python, Go, Rust Total: 12 SDK packages
786 lines
21 KiB
Go
786 lines
21 KiB
Go
// Package mining provides the Synor Mining SDK for Go.
|
|
// Pool connections, block templates, hashrate stats, and GPU management.
|
|
package mining
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultEndpoint = "https://mining.synor.io/v1"
|
|
DefaultTimeout = 30 * time.Second
|
|
DefaultRetries = 3
|
|
)
|
|
|
|
// DeviceType represents device type.
|
|
type DeviceType string
|
|
|
|
const (
|
|
DeviceCPU DeviceType = "cpu"
|
|
DeviceGPUNvidia DeviceType = "gpu_nvidia"
|
|
DeviceGPUAMD DeviceType = "gpu_amd"
|
|
DeviceASIC DeviceType = "asic"
|
|
)
|
|
|
|
// DeviceStatus represents device status.
|
|
type DeviceStatus string
|
|
|
|
const (
|
|
DeviceIdle DeviceStatus = "idle"
|
|
DeviceMining DeviceStatus = "mining"
|
|
DeviceError DeviceStatus = "error"
|
|
DeviceOffline DeviceStatus = "offline"
|
|
)
|
|
|
|
// ConnectionStatus represents connection status.
|
|
type ConnectionStatus string
|
|
|
|
const (
|
|
StatusDisconnected ConnectionStatus = "disconnected"
|
|
StatusConnecting ConnectionStatus = "connecting"
|
|
StatusConnected ConnectionStatus = "connected"
|
|
StatusReconnecting ConnectionStatus = "reconnecting"
|
|
)
|
|
|
|
// TimePeriod for stats.
|
|
type TimePeriod string
|
|
|
|
const (
|
|
PeriodHour TimePeriod = "hour"
|
|
PeriodDay TimePeriod = "day"
|
|
PeriodWeek TimePeriod = "week"
|
|
PeriodMonth TimePeriod = "month"
|
|
PeriodAll TimePeriod = "all"
|
|
)
|
|
|
|
// SubmitResultStatus represents submit result status.
|
|
type SubmitResultStatus string
|
|
|
|
const (
|
|
SubmitAccepted SubmitResultStatus = "accepted"
|
|
SubmitRejected SubmitResultStatus = "rejected"
|
|
SubmitStale SubmitResultStatus = "stale"
|
|
)
|
|
|
|
// Config for the Mining client.
|
|
type Config struct {
|
|
APIKey string
|
|
Endpoint string
|
|
Timeout time.Duration
|
|
Retries int
|
|
Debug bool
|
|
}
|
|
|
|
// PoolConfig for connecting to a pool.
|
|
type PoolConfig struct {
|
|
URL string `json:"url"`
|
|
User string `json:"user"`
|
|
Password string `json:"password,omitempty"`
|
|
Algorithm string `json:"algorithm,omitempty"`
|
|
Difficulty *float64 `json:"difficulty,omitempty"`
|
|
}
|
|
|
|
// StratumConnection represents a pool connection.
|
|
type StratumConnection struct {
|
|
ID string `json:"id"`
|
|
Pool string `json:"pool"`
|
|
Status ConnectionStatus `json:"status"`
|
|
Algorithm string `json:"algorithm"`
|
|
Difficulty float64 `json:"difficulty"`
|
|
ConnectedAt int64 `json:"connected_at"`
|
|
LastShareAt *int64 `json:"last_share_at,omitempty"`
|
|
AcceptedShares int64 `json:"accepted_shares"`
|
|
RejectedShares int64 `json:"rejected_shares"`
|
|
StaleShares int64 `json:"stale_shares"`
|
|
}
|
|
|
|
// TemplateTransaction in a block template.
|
|
type TemplateTransaction struct {
|
|
TxID string `json:"txid"`
|
|
Data string `json:"data"`
|
|
Fee string `json:"fee"`
|
|
Weight int `json:"weight"`
|
|
}
|
|
|
|
// BlockTemplate for mining.
|
|
type BlockTemplate struct {
|
|
ID string `json:"id"`
|
|
PreviousBlockHash string `json:"previous_block_hash"`
|
|
MerkleRoot string `json:"merkle_root"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
Bits string `json:"bits"`
|
|
Height int64 `json:"height"`
|
|
CoinbaseValue string `json:"coinbase_value"`
|
|
Transactions []TemplateTransaction `json:"transactions"`
|
|
Target string `json:"target"`
|
|
Algorithm string `json:"algorithm"`
|
|
ExtraNonce string `json:"extra_nonce"`
|
|
}
|
|
|
|
// MinedWork to submit.
|
|
type MinedWork struct {
|
|
TemplateID string `json:"template_id"`
|
|
Nonce string `json:"nonce"`
|
|
ExtraNonce string `json:"extra_nonce"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
Hash string `json:"hash"`
|
|
}
|
|
|
|
// ShareInfo in submit result.
|
|
type ShareInfo struct {
|
|
Hash string `json:"hash"`
|
|
Difficulty float64 `json:"difficulty"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
Accepted bool `json:"accepted"`
|
|
}
|
|
|
|
// SubmitResult from submitting work.
|
|
type SubmitResult struct {
|
|
Status SubmitResultStatus `json:"status"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Share ShareInfo `json:"share"`
|
|
BlockFound bool `json:"block_found"`
|
|
BlockHash string `json:"block_hash,omitempty"`
|
|
Reward string `json:"reward,omitempty"`
|
|
}
|
|
|
|
// Hashrate statistics.
|
|
type Hashrate struct {
|
|
Current float64 `json:"current"`
|
|
Average1h float64 `json:"average_1h"`
|
|
Average24h float64 `json:"average_24h"`
|
|
Peak float64 `json:"peak"`
|
|
Unit string `json:"unit"`
|
|
}
|
|
|
|
// ShareStats for mining.
|
|
type ShareStats struct {
|
|
Accepted int64 `json:"accepted"`
|
|
Rejected int64 `json:"rejected"`
|
|
Stale int64 `json:"stale"`
|
|
Total int64 `json:"total"`
|
|
AcceptRate float64 `json:"accept_rate"`
|
|
}
|
|
|
|
// DeviceTemperature info.
|
|
type DeviceTemperature struct {
|
|
Current float64 `json:"current"`
|
|
Max float64 `json:"max"`
|
|
Throttling bool `json:"throttling"`
|
|
}
|
|
|
|
// EarningsSnapshot for quick view.
|
|
type EarningsSnapshot struct {
|
|
Today string `json:"today"`
|
|
Yesterday string `json:"yesterday"`
|
|
ThisWeek string `json:"this_week"`
|
|
ThisMonth string `json:"this_month"`
|
|
Total string `json:"total"`
|
|
Currency string `json:"currency"`
|
|
}
|
|
|
|
// MiningStats comprehensive stats.
|
|
type MiningStats struct {
|
|
Hashrate Hashrate `json:"hashrate"`
|
|
Shares ShareStats `json:"shares"`
|
|
Uptime int64 `json:"uptime"`
|
|
Efficiency float64 `json:"efficiency"`
|
|
PowerConsumption *float64 `json:"power_consumption,omitempty"`
|
|
Temperature *DeviceTemperature `json:"temperature,omitempty"`
|
|
Earnings EarningsSnapshot `json:"earnings"`
|
|
}
|
|
|
|
// EarningsBreakdown by date.
|
|
type EarningsBreakdown struct {
|
|
Date int64 `json:"date"`
|
|
Amount string `json:"amount"`
|
|
Blocks int `json:"blocks"`
|
|
Shares int64 `json:"shares"`
|
|
Hashrate float64 `json:"hashrate"`
|
|
}
|
|
|
|
// Earnings detailed info.
|
|
type Earnings struct {
|
|
Period TimePeriod `json:"period"`
|
|
StartDate int64 `json:"start_date"`
|
|
EndDate int64 `json:"end_date"`
|
|
Amount string `json:"amount"`
|
|
Blocks int `json:"blocks"`
|
|
Shares int64 `json:"shares"`
|
|
AverageHashrate float64 `json:"average_hashrate"`
|
|
Currency string `json:"currency"`
|
|
Breakdown []EarningsBreakdown `json:"breakdown"`
|
|
}
|
|
|
|
// MiningDevice represents a mining device.
|
|
type MiningDevice struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type DeviceType `json:"type"`
|
|
Status DeviceStatus `json:"status"`
|
|
Hashrate float64 `json:"hashrate"`
|
|
Temperature float64 `json:"temperature"`
|
|
FanSpeed float64 `json:"fan_speed"`
|
|
PowerDraw float64 `json:"power_draw"`
|
|
MemoryUsed int64 `json:"memory_used"`
|
|
MemoryTotal int64 `json:"memory_total"`
|
|
Driver string `json:"driver,omitempty"`
|
|
Firmware string `json:"firmware,omitempty"`
|
|
}
|
|
|
|
// DeviceConfig for device settings.
|
|
type DeviceConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
Intensity *int `json:"intensity,omitempty"`
|
|
PowerLimit *int `json:"power_limit,omitempty"`
|
|
CoreClockOffset *int `json:"core_clock_offset,omitempty"`
|
|
MemoryClockOffset *int `json:"memory_clock_offset,omitempty"`
|
|
FanSpeed *int `json:"fan_speed,omitempty"`
|
|
}
|
|
|
|
// WorkerInfo for worker details.
|
|
type WorkerInfo struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Status ConnectionStatus `json:"status"`
|
|
Hashrate Hashrate `json:"hashrate"`
|
|
Shares ShareStats `json:"shares"`
|
|
Devices []MiningDevice `json:"devices"`
|
|
LastSeen int64 `json:"last_seen"`
|
|
Uptime int64 `json:"uptime"`
|
|
}
|
|
|
|
// PoolStats for pool info.
|
|
type PoolStats struct {
|
|
URL string `json:"url"`
|
|
Workers int `json:"workers"`
|
|
Hashrate float64 `json:"hashrate"`
|
|
Difficulty float64 `json:"difficulty"`
|
|
LastBlock int64 `json:"last_block"`
|
|
BlocksFound24h int `json:"blocks_found_24h"`
|
|
Luck float64 `json:"luck"`
|
|
}
|
|
|
|
// MiningAlgorithm info.
|
|
type MiningAlgorithm struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name"`
|
|
HashUnit string `json:"hash_unit"`
|
|
Profitability string `json:"profitability"`
|
|
Difficulty float64 `json:"difficulty"`
|
|
BlockReward string `json:"block_reward"`
|
|
BlockTime int `json:"block_time"`
|
|
}
|
|
|
|
// WorkResult from getwork.
|
|
type WorkResult struct {
|
|
Work string `json:"work"`
|
|
Target string `json:"target"`
|
|
Algorithm string `json:"algorithm"`
|
|
}
|
|
|
|
// StartMiningResult from start mining.
|
|
type StartMiningResult struct {
|
|
Started bool `json:"started"`
|
|
Devices []string `json:"devices"`
|
|
}
|
|
|
|
// Client for the Synor Mining SDK.
|
|
type Client struct {
|
|
config Config
|
|
client *http.Client
|
|
activeConnection *StratumConnection
|
|
mu sync.RWMutex
|
|
closed bool
|
|
}
|
|
|
|
// NewClient creates a new Mining client.
|
|
func NewClient(config Config) *Client {
|
|
if config.Endpoint == "" {
|
|
config.Endpoint = DefaultEndpoint
|
|
}
|
|
if config.Timeout == 0 {
|
|
config.Timeout = DefaultTimeout
|
|
}
|
|
if config.Retries == 0 {
|
|
config.Retries = DefaultRetries
|
|
}
|
|
|
|
return &Client{
|
|
config: config,
|
|
client: &http.Client{Timeout: config.Timeout},
|
|
}
|
|
}
|
|
|
|
// ==================== Pool Connection ====================
|
|
|
|
// Connect connects to a mining pool.
|
|
func (c *Client) Connect(ctx context.Context, pool PoolConfig) (*StratumConnection, error) {
|
|
var resp StratumConnection
|
|
err := c.post(ctx, "/pool/connect", pool, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.mu.Lock()
|
|
c.activeConnection = &resp
|
|
c.mu.Unlock()
|
|
return &resp, nil
|
|
}
|
|
|
|
// Disconnect disconnects from the current pool.
|
|
func (c *Client) Disconnect(ctx context.Context) error {
|
|
c.mu.Lock()
|
|
conn := c.activeConnection
|
|
c.mu.Unlock()
|
|
if conn != nil {
|
|
err := c.post(ctx, fmt.Sprintf("/pool/disconnect/%s", conn.ID), nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.mu.Lock()
|
|
c.activeConnection = nil
|
|
c.mu.Unlock()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetConnectionStatus gets current connection status.
|
|
func (c *Client) GetConnectionStatus(ctx context.Context) (*StratumConnection, error) {
|
|
c.mu.RLock()
|
|
conn := c.activeConnection
|
|
c.mu.RUnlock()
|
|
if conn == nil {
|
|
return nil, nil
|
|
}
|
|
var resp StratumConnection
|
|
err := c.get(ctx, fmt.Sprintf("/pool/status/%s", conn.ID), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// Reconnect reconnects to the pool.
|
|
func (c *Client) Reconnect(ctx context.Context) (*StratumConnection, error) {
|
|
c.mu.RLock()
|
|
conn := c.activeConnection
|
|
c.mu.RUnlock()
|
|
if conn == nil {
|
|
return nil, &MiningError{Message: "No active connection to reconnect"}
|
|
}
|
|
var resp StratumConnection
|
|
err := c.post(ctx, fmt.Sprintf("/pool/reconnect/%s", conn.ID), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ==================== Mining Operations ====================
|
|
|
|
// GetBlockTemplate gets the current block template.
|
|
func (c *Client) GetBlockTemplate(ctx context.Context) (*BlockTemplate, error) {
|
|
var resp BlockTemplate
|
|
err := c.get(ctx, "/mining/template", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// SubmitWork submits mined work.
|
|
func (c *Client) SubmitWork(ctx context.Context, work MinedWork) (*SubmitResult, error) {
|
|
var resp SubmitResult
|
|
err := c.post(ctx, "/mining/submit", work, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetWork gets work from pool.
|
|
func (c *Client) GetWork(ctx context.Context) (*WorkResult, error) {
|
|
var resp WorkResult
|
|
err := c.get(ctx, "/mining/getwork", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// StartMining starts mining on all enabled devices.
|
|
func (c *Client) StartMining(ctx context.Context, algorithm string) (*StartMiningResult, error) {
|
|
body := make(map[string]interface{})
|
|
if algorithm != "" {
|
|
body["algorithm"] = algorithm
|
|
}
|
|
var resp StartMiningResult
|
|
err := c.post(ctx, "/mining/start", body, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// StopMining stops mining on all devices.
|
|
func (c *Client) StopMining(ctx context.Context) (bool, error) {
|
|
var resp struct {
|
|
Stopped bool `json:"stopped"`
|
|
}
|
|
err := c.post(ctx, "/mining/stop", nil, &resp)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return resp.Stopped, nil
|
|
}
|
|
|
|
// ==================== Stats ====================
|
|
|
|
// GetHashrate gets current hashrate.
|
|
func (c *Client) GetHashrate(ctx context.Context) (*Hashrate, error) {
|
|
var resp Hashrate
|
|
err := c.get(ctx, "/stats/hashrate", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetStats gets mining stats.
|
|
func (c *Client) GetStats(ctx context.Context) (*MiningStats, error) {
|
|
var resp MiningStats
|
|
err := c.get(ctx, "/stats", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetEarnings gets earnings for a time period.
|
|
func (c *Client) GetEarnings(ctx context.Context, period *TimePeriod) (*Earnings, error) {
|
|
path := "/stats/earnings"
|
|
if period != nil {
|
|
path = fmt.Sprintf("%s?period=%s", path, *period)
|
|
}
|
|
var resp Earnings
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetEarningsHistory gets earnings history.
|
|
func (c *Client) GetEarningsHistory(ctx context.Context, limit, offset *int) ([]Earnings, error) {
|
|
params := url.Values{}
|
|
if limit != nil {
|
|
params.Set("limit", fmt.Sprintf("%d", *limit))
|
|
}
|
|
if offset != nil {
|
|
params.Set("offset", fmt.Sprintf("%d", *offset))
|
|
}
|
|
path := "/stats/earnings/history"
|
|
if len(params) > 0 {
|
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
|
}
|
|
var resp struct {
|
|
Earnings []Earnings `json:"earnings"`
|
|
}
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Earnings, nil
|
|
}
|
|
|
|
// GetPoolStats gets pool stats.
|
|
func (c *Client) GetPoolStats(ctx context.Context) (*PoolStats, error) {
|
|
var resp PoolStats
|
|
err := c.get(ctx, "/pool/stats", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ==================== GPU Management ====================
|
|
|
|
// ListDevices lists all mining devices.
|
|
func (c *Client) ListDevices(ctx context.Context) ([]MiningDevice, error) {
|
|
var resp struct {
|
|
Devices []MiningDevice `json:"devices"`
|
|
}
|
|
err := c.get(ctx, "/devices", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Devices, nil
|
|
}
|
|
|
|
// GetDevice gets device details.
|
|
func (c *Client) GetDevice(ctx context.Context, deviceID string) (*MiningDevice, error) {
|
|
var resp MiningDevice
|
|
err := c.get(ctx, fmt.Sprintf("/devices/%s", url.PathEscape(deviceID)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// SetDeviceConfig sets device configuration.
|
|
func (c *Client) SetDeviceConfig(ctx context.Context, deviceID string, config DeviceConfig) (*MiningDevice, error) {
|
|
var resp MiningDevice
|
|
err := c.post(ctx, fmt.Sprintf("/devices/%s/config", url.PathEscape(deviceID)), config, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// EnableDevice enables a device for mining.
|
|
func (c *Client) EnableDevice(ctx context.Context, deviceID string) (*MiningDevice, error) {
|
|
var resp MiningDevice
|
|
err := c.post(ctx, fmt.Sprintf("/devices/%s/enable", url.PathEscape(deviceID)), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// DisableDevice disables a device.
|
|
func (c *Client) DisableDevice(ctx context.Context, deviceID string) (*MiningDevice, error) {
|
|
var resp MiningDevice
|
|
err := c.post(ctx, fmt.Sprintf("/devices/%s/disable", url.PathEscape(deviceID)), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ResetDevice resets device to default settings.
|
|
func (c *Client) ResetDevice(ctx context.Context, deviceID string) (*MiningDevice, error) {
|
|
var resp MiningDevice
|
|
err := c.post(ctx, fmt.Sprintf("/devices/%s/reset", url.PathEscape(deviceID)), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ==================== Workers ====================
|
|
|
|
// ListWorkers lists all workers.
|
|
func (c *Client) ListWorkers(ctx context.Context) ([]WorkerInfo, error) {
|
|
var resp struct {
|
|
Workers []WorkerInfo `json:"workers"`
|
|
}
|
|
err := c.get(ctx, "/workers", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Workers, nil
|
|
}
|
|
|
|
// GetWorker gets worker details.
|
|
func (c *Client) GetWorker(ctx context.Context, workerID string) (*WorkerInfo, error) {
|
|
var resp WorkerInfo
|
|
err := c.get(ctx, fmt.Sprintf("/workers/%s", url.PathEscape(workerID)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// CreateWorker creates a new worker.
|
|
func (c *Client) CreateWorker(ctx context.Context, name string) (*WorkerInfo, error) {
|
|
var resp WorkerInfo
|
|
err := c.post(ctx, "/workers", map[string]string{"name": name}, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// DeleteWorker deletes a worker.
|
|
func (c *Client) DeleteWorker(ctx context.Context, workerID string) error {
|
|
return c.delete(ctx, fmt.Sprintf("/workers/%s", url.PathEscape(workerID)))
|
|
}
|
|
|
|
// ==================== Algorithms ====================
|
|
|
|
// GetSupportedAlgorithms gets supported mining algorithms.
|
|
func (c *Client) GetSupportedAlgorithms(ctx context.Context) ([]MiningAlgorithm, error) {
|
|
var resp struct {
|
|
Algorithms []MiningAlgorithm `json:"algorithms"`
|
|
}
|
|
err := c.get(ctx, "/algorithms", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Algorithms, nil
|
|
}
|
|
|
|
// GetCurrentAlgorithm gets current algorithm.
|
|
func (c *Client) GetCurrentAlgorithm(ctx context.Context) (*MiningAlgorithm, error) {
|
|
var resp MiningAlgorithm
|
|
err := c.get(ctx, "/algorithms/current", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// SwitchAlgorithm switches to a different algorithm.
|
|
func (c *Client) SwitchAlgorithm(ctx context.Context, algorithm string) (bool, error) {
|
|
var resp struct {
|
|
Switched bool `json:"switched"`
|
|
}
|
|
err := c.post(ctx, "/algorithms/switch", map[string]string{"algorithm": algorithm}, &resp)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return resp.Switched, nil
|
|
}
|
|
|
|
// ==================== Lifecycle ====================
|
|
|
|
// HealthCheck performs a health check.
|
|
func (c *Client) HealthCheck(ctx context.Context) bool {
|
|
var resp struct {
|
|
Status string `json:"status"`
|
|
}
|
|
err := c.get(ctx, "/health", &resp)
|
|
return err == nil && resp.Status == "healthy"
|
|
}
|
|
|
|
// IsClosed returns true if the client is closed.
|
|
func (c *Client) IsClosed() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.closed
|
|
}
|
|
|
|
// Close closes the client.
|
|
func (c *Client) Close(ctx context.Context) error {
|
|
c.Disconnect(ctx)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.closed = true
|
|
return nil
|
|
}
|
|
|
|
// ==================== Private Methods ====================
|
|
|
|
func (c *Client) get(ctx context.Context, path string, result interface{}) error {
|
|
return c.execute(ctx, "GET", path, nil, result)
|
|
}
|
|
|
|
func (c *Client) post(ctx context.Context, path string, body, result interface{}) error {
|
|
return c.execute(ctx, "POST", path, body, result)
|
|
}
|
|
|
|
func (c *Client) delete(ctx context.Context, path string) error {
|
|
return c.execute(ctx, "DELETE", path, nil, nil)
|
|
}
|
|
|
|
func (c *Client) execute(ctx context.Context, method, path string, body, result interface{}) error {
|
|
c.mu.RLock()
|
|
if c.closed {
|
|
c.mu.RUnlock()
|
|
return &MiningError{Message: "Client has been closed"}
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
var lastErr error
|
|
for attempt := 0; attempt < c.config.Retries; attempt++ {
|
|
err := c.doRequest(ctx, method, path, body, result)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
if c.config.Debug {
|
|
fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
|
|
}
|
|
if attempt < c.config.Retries-1 {
|
|
time.Sleep(time.Duration(1<<attempt) * time.Second)
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func (c *Client) doRequest(ctx context.Context, method, path string, body, result interface{}) error {
|
|
url := c.config.Endpoint + path
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bodyReader = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-SDK-Version", "go/0.1.0")
|
|
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
var errResp struct {
|
|
Message string `json:"message"`
|
|
Code string `json:"code"`
|
|
}
|
|
json.Unmarshal(respBody, &errResp)
|
|
msg := errResp.Message
|
|
if msg == "" {
|
|
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return &MiningError{
|
|
Message: msg,
|
|
Code: errResp.Code,
|
|
StatusCode: resp.StatusCode,
|
|
}
|
|
}
|
|
|
|
if result != nil && len(respBody) > 0 {
|
|
return json.Unmarshal(respBody, result)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MiningError represents an error from the Mining API.
|
|
type MiningError struct {
|
|
Message string
|
|
Code string
|
|
StatusCode int
|
|
}
|
|
|
|
func (e *MiningError) Error() string {
|
|
if e.Code != "" {
|
|
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
|
|
}
|
|
return e.Message
|
|
}
|