feat: add Economics, Governance, and Mining SDKs for core languages
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
This commit is contained in:
parent
a874faef13
commit
58e57db661
24 changed files with 8998 additions and 0 deletions
723
sdk/go/economics/economics.go
Normal file
723
sdk/go/economics/economics.go
Normal file
|
|
@ -0,0 +1,723 @@
|
||||||
|
// Package economics provides the Synor Economics SDK for Go.
|
||||||
|
// Pricing, billing, staking, and discount management.
|
||||||
|
package economics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultEndpoint = "https://economics.synor.io/v1"
|
||||||
|
DefaultTimeout = 30 * time.Second
|
||||||
|
DefaultRetries = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceType represents a service type for pricing.
|
||||||
|
type ServiceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServiceCompute ServiceType = "compute"
|
||||||
|
ServiceStorage ServiceType = "storage"
|
||||||
|
ServiceDatabase ServiceType = "database"
|
||||||
|
ServiceHosting ServiceType = "hosting"
|
||||||
|
ServiceBridge ServiceType = "bridge"
|
||||||
|
ServiceRPC ServiceType = "rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BillingPeriod represents a billing period.
|
||||||
|
type BillingPeriod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PeriodHourly BillingPeriod = "hourly"
|
||||||
|
PeriodDaily BillingPeriod = "daily"
|
||||||
|
PeriodWeekly BillingPeriod = "weekly"
|
||||||
|
PeriodMonthly BillingPeriod = "monthly"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StakeStatus represents stake status.
|
||||||
|
type StakeStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StakeActive StakeStatus = "active"
|
||||||
|
StakeUnstaking StakeStatus = "unstaking"
|
||||||
|
StakeWithdrawn StakeStatus = "withdrawn"
|
||||||
|
StakeSlashed StakeStatus = "slashed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DiscountType represents discount type.
|
||||||
|
type DiscountType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DiscountPercentage DiscountType = "percentage"
|
||||||
|
DiscountFixed DiscountType = "fixed"
|
||||||
|
DiscountVolume DiscountType = "volume"
|
||||||
|
DiscountReferral DiscountType = "referral"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvoiceStatus represents invoice status.
|
||||||
|
type InvoiceStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
InvoicePending InvoiceStatus = "pending"
|
||||||
|
InvoicePaid InvoiceStatus = "paid"
|
||||||
|
InvoiceOverdue InvoiceStatus = "overdue"
|
||||||
|
InvoiceCancelled InvoiceStatus = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config for the Economics client.
|
||||||
|
type Config struct {
|
||||||
|
APIKey string
|
||||||
|
Endpoint string
|
||||||
|
Timeout time.Duration
|
||||||
|
Retries int
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsageMetrics for pricing calculations.
|
||||||
|
type UsageMetrics struct {
|
||||||
|
ComputeHours *float64 `json:"compute_hours,omitempty"`
|
||||||
|
StorageBytes *int64 `json:"storage_bytes,omitempty"`
|
||||||
|
DatabaseOps *int64 `json:"database_ops,omitempty"`
|
||||||
|
HostingRequests *int64 `json:"hosting_requests,omitempty"`
|
||||||
|
BridgeTransfers *int64 `json:"bridge_transfers,omitempty"`
|
||||||
|
RPCCalls *int64 `json:"rpc_calls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceUsage in a plan.
|
||||||
|
type ServiceUsage struct {
|
||||||
|
Service ServiceType `json:"service"`
|
||||||
|
Metrics UsageMetrics `json:"metrics"`
|
||||||
|
Tier string `json:"tier,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsagePlan for cost estimation.
|
||||||
|
type UsagePlan struct {
|
||||||
|
Services []ServiceUsage `json:"services"`
|
||||||
|
Period BillingPeriod `json:"period"`
|
||||||
|
StartDate *int64 `json:"start_date,omitempty"`
|
||||||
|
EndDate *int64 `json:"end_date,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppliedDiscount represents a discount applied to a price.
|
||||||
|
type AppliedDiscount struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Type DiscountType `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Savings string `json:"savings"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaxAmount represents tax.
|
||||||
|
type TaxAmount struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Rate float64 `json:"rate"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price breakdown.
|
||||||
|
type Price struct {
|
||||||
|
Service ServiceType `json:"service"`
|
||||||
|
BasePrice string `json:"base_price"`
|
||||||
|
Quantity string `json:"quantity"`
|
||||||
|
Unit string `json:"unit"`
|
||||||
|
Subtotal string `json:"subtotal"`
|
||||||
|
Discounts []AppliedDiscount `json:"discounts"`
|
||||||
|
Total string `json:"total"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CostEstimate for a usage plan.
|
||||||
|
type CostEstimate struct {
|
||||||
|
Services []Price `json:"services"`
|
||||||
|
Subtotal string `json:"subtotal"`
|
||||||
|
Discounts []AppliedDiscount `json:"discounts"`
|
||||||
|
Taxes []TaxAmount `json:"taxes"`
|
||||||
|
Total string `json:"total"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
ValidUntil int64 `json:"valid_until"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsageDetail for a single operation.
|
||||||
|
type UsageDetail struct {
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Unit string `json:"unit"`
|
||||||
|
Cost string `json:"cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceUsageRecord for billing.
|
||||||
|
type ServiceUsageRecord struct {
|
||||||
|
Service ServiceType `json:"service"`
|
||||||
|
Metrics UsageMetrics `json:"metrics"`
|
||||||
|
Cost string `json:"cost"`
|
||||||
|
Details []UsageDetail `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage record.
|
||||||
|
type Usage struct {
|
||||||
|
Period BillingPeriod `json:"period"`
|
||||||
|
StartDate int64 `json:"start_date"`
|
||||||
|
EndDate int64 `json:"end_date"`
|
||||||
|
Services []ServiceUsageRecord `json:"services"`
|
||||||
|
TotalCost string `json:"total_cost"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvoiceItem in an invoice.
|
||||||
|
type InvoiceItem struct {
|
||||||
|
Service ServiceType `json:"service"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Quantity string `json:"quantity"`
|
||||||
|
UnitPrice string `json:"unit_price"`
|
||||||
|
Total string `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoice record.
|
||||||
|
type Invoice struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status InvoiceStatus `json:"status"`
|
||||||
|
Period BillingPeriod `json:"period"`
|
||||||
|
StartDate int64 `json:"start_date"`
|
||||||
|
EndDate int64 `json:"end_date"`
|
||||||
|
Items []InvoiceItem `json:"items"`
|
||||||
|
Subtotal string `json:"subtotal"`
|
||||||
|
Discounts []AppliedDiscount `json:"discounts"`
|
||||||
|
Taxes []TaxAmount `json:"taxes"`
|
||||||
|
Total string `json:"total"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
DueDate int64 `json:"due_date"`
|
||||||
|
PaidAt *int64 `json:"paid_at,omitempty"`
|
||||||
|
PaymentMethod string `json:"payment_method,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountBalance for billing.
|
||||||
|
type AccountBalance struct {
|
||||||
|
Available string `json:"available"`
|
||||||
|
Pending string `json:"pending"`
|
||||||
|
Reserved string `json:"reserved"`
|
||||||
|
Total string `json:"total"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
CreditLimit string `json:"credit_limit,omitempty"`
|
||||||
|
LastUpdated int64 `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StakeReceipt from staking.
|
||||||
|
type StakeReceipt struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
LockDuration int `json:"lock_duration"`
|
||||||
|
StartDate int64 `json:"start_date"`
|
||||||
|
EndDate int64 `json:"end_date"`
|
||||||
|
APY string `json:"apy"`
|
||||||
|
EstimatedRewards string `json:"estimated_rewards"`
|
||||||
|
TxHash string `json:"tx_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnstakeReceipt from unstaking.
|
||||||
|
type UnstakeReceipt struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
StakeID string `json:"stake_id"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
Rewards string `json:"rewards"`
|
||||||
|
Total string `json:"total"`
|
||||||
|
UnbondingPeriod int `json:"unbonding_period"`
|
||||||
|
AvailableAt int64 `json:"available_at"`
|
||||||
|
TxHash string `json:"tx_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StakeInfo for active stakes.
|
||||||
|
type StakeInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
Status StakeStatus `json:"status"`
|
||||||
|
LockDuration int `json:"lock_duration"`
|
||||||
|
StartDate int64 `json:"start_date"`
|
||||||
|
EndDate int64 `json:"end_date"`
|
||||||
|
APY string `json:"apy"`
|
||||||
|
EarnedRewards string `json:"earned_rewards"`
|
||||||
|
PendingRewards string `json:"pending_rewards"`
|
||||||
|
Validator string `json:"validator,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StakeRewardDetail for individual stake rewards.
|
||||||
|
type StakeRewardDetail struct {
|
||||||
|
StakeID string `json:"stake_id"`
|
||||||
|
Earned string `json:"earned"`
|
||||||
|
Pending string `json:"pending"`
|
||||||
|
APY string `json:"apy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StakingRewards summary.
|
||||||
|
type StakingRewards struct {
|
||||||
|
TotalEarned string `json:"total_earned"`
|
||||||
|
Pending string `json:"pending"`
|
||||||
|
Claimed string `json:"claimed"`
|
||||||
|
LastClaimDate *int64 `json:"last_claim_date,omitempty"`
|
||||||
|
NextClaimAvailable *int64 `json:"next_claim_available,omitempty"`
|
||||||
|
Stakes []StakeRewardDetail `json:"stakes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discount available or active.
|
||||||
|
type Discount struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Type DiscountType `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ValidFrom int64 `json:"valid_from"`
|
||||||
|
ValidUntil int64 `json:"valid_until"`
|
||||||
|
MinPurchase string `json:"min_purchase,omitempty"`
|
||||||
|
MaxDiscount string `json:"max_discount,omitempty"`
|
||||||
|
ApplicableServices []ServiceType `json:"applicable_services"`
|
||||||
|
UsageLimit *int `json:"usage_limit,omitempty"`
|
||||||
|
UsedCount int `json:"used_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StakeOptions for staking.
|
||||||
|
type StakeOptions struct {
|
||||||
|
Validator string `json:"validator,omitempty"`
|
||||||
|
AutoCompound *bool `json:"auto_compound,omitempty"`
|
||||||
|
LockDuration *int `json:"lock_duration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StakingAPY response.
|
||||||
|
type StakingAPY struct {
|
||||||
|
APY string `json:"apy"`
|
||||||
|
MinLockDuration int `json:"min_lock_duration"`
|
||||||
|
MaxLockDuration int `json:"max_lock_duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client for the Synor Economics SDK.
|
||||||
|
type Client struct {
|
||||||
|
config Config
|
||||||
|
client *http.Client
|
||||||
|
mu sync.RWMutex
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Economics 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},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Pricing Operations ====================
|
||||||
|
|
||||||
|
// GetPrice gets price for a service based on usage.
|
||||||
|
func (c *Client) GetPrice(ctx context.Context, service ServiceType, usage UsageMetrics) (*Price, error) {
|
||||||
|
var resp struct {
|
||||||
|
Price Price `json:"price"`
|
||||||
|
}
|
||||||
|
err := c.post(ctx, "/pricing/calculate", map[string]interface{}{
|
||||||
|
"service": service,
|
||||||
|
"usage": usage,
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp.Price, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateCost estimates cost for a usage plan.
|
||||||
|
func (c *Client) EstimateCost(ctx context.Context, plan UsagePlan) (*CostEstimate, error) {
|
||||||
|
var resp CostEstimate
|
||||||
|
err := c.post(ctx, "/pricing/estimate", plan, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPricingTiers gets pricing tiers for a service.
|
||||||
|
func (c *Client) GetPricingTiers(ctx context.Context, service ServiceType) ([]Price, error) {
|
||||||
|
var resp struct {
|
||||||
|
Tiers []Price `json:"tiers"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, fmt.Sprintf("/pricing/%s/tiers", service), &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Tiers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Usage & Billing Operations ====================
|
||||||
|
|
||||||
|
// GetUsage gets usage for a billing period.
|
||||||
|
func (c *Client) GetUsage(ctx context.Context, period *BillingPeriod) (*Usage, error) {
|
||||||
|
path := "/usage"
|
||||||
|
if period != nil {
|
||||||
|
path = fmt.Sprintf("/usage?period=%s", *period)
|
||||||
|
}
|
||||||
|
var resp Usage
|
||||||
|
err := c.get(ctx, path, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsageHistory gets usage history.
|
||||||
|
func (c *Client) GetUsageHistory(ctx context.Context, limit, offset *int) ([]Usage, 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 := "/usage/history"
|
||||||
|
if len(params) > 0 {
|
||||||
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Usage []Usage `json:"usage"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, path, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Usage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInvoices gets all invoices.
|
||||||
|
func (c *Client) GetInvoices(ctx context.Context) ([]Invoice, error) {
|
||||||
|
var resp struct {
|
||||||
|
Invoices []Invoice `json:"invoices"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, "/invoices", &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Invoices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInvoice gets a specific invoice.
|
||||||
|
func (c *Client) GetInvoice(ctx context.Context, invoiceID string) (*Invoice, error) {
|
||||||
|
var resp Invoice
|
||||||
|
err := c.get(ctx, fmt.Sprintf("/invoices/%s", url.PathEscape(invoiceID)), &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayInvoice pays an invoice.
|
||||||
|
func (c *Client) PayInvoice(ctx context.Context, invoiceID, paymentMethod string) (*Invoice, error) {
|
||||||
|
var resp Invoice
|
||||||
|
body := make(map[string]interface{})
|
||||||
|
if paymentMethod != "" {
|
||||||
|
body["payment_method"] = paymentMethod
|
||||||
|
}
|
||||||
|
err := c.post(ctx, fmt.Sprintf("/invoices/%s/pay", url.PathEscape(invoiceID)), body, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBalance gets account balance.
|
||||||
|
func (c *Client) GetBalance(ctx context.Context) (*AccountBalance, error) {
|
||||||
|
var resp AccountBalance
|
||||||
|
err := c.get(ctx, "/balance", &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFunds adds funds to account.
|
||||||
|
func (c *Client) AddFunds(ctx context.Context, amount, paymentMethod string) (*AccountBalance, error) {
|
||||||
|
var resp AccountBalance
|
||||||
|
err := c.post(ctx, "/balance/deposit", map[string]interface{}{
|
||||||
|
"amount": amount,
|
||||||
|
"payment_method": paymentMethod,
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Staking Operations ====================
|
||||||
|
|
||||||
|
// Stake stakes tokens.
|
||||||
|
func (c *Client) Stake(ctx context.Context, amount string, options *StakeOptions) (*StakeReceipt, error) {
|
||||||
|
body := map[string]interface{}{"amount": amount}
|
||||||
|
if options != nil {
|
||||||
|
if options.Validator != "" {
|
||||||
|
body["validator"] = options.Validator
|
||||||
|
}
|
||||||
|
if options.AutoCompound != nil {
|
||||||
|
body["auto_compound"] = *options.AutoCompound
|
||||||
|
}
|
||||||
|
if options.LockDuration != nil {
|
||||||
|
body["lock_duration"] = *options.LockDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var resp StakeReceipt
|
||||||
|
err := c.post(ctx, "/staking/stake", body, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unstake unstakes tokens.
|
||||||
|
func (c *Client) Unstake(ctx context.Context, stakeID string) (*UnstakeReceipt, error) {
|
||||||
|
var resp UnstakeReceipt
|
||||||
|
err := c.post(ctx, fmt.Sprintf("/staking/stakes/%s/unstake", url.PathEscape(stakeID)), nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStakingRewards gets staking rewards.
|
||||||
|
func (c *Client) GetStakingRewards(ctx context.Context) (*StakingRewards, error) {
|
||||||
|
var resp StakingRewards
|
||||||
|
err := c.get(ctx, "/staking/rewards", &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimRewards claims staking rewards.
|
||||||
|
func (c *Client) ClaimRewards(ctx context.Context, stakeID string) (*StakingRewards, error) {
|
||||||
|
path := "/staking/rewards/claim"
|
||||||
|
if stakeID != "" {
|
||||||
|
path = fmt.Sprintf("/staking/stakes/%s/claim", url.PathEscape(stakeID))
|
||||||
|
}
|
||||||
|
var resp StakingRewards
|
||||||
|
err := c.post(ctx, path, nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListStakes lists active stakes.
|
||||||
|
func (c *Client) ListStakes(ctx context.Context) ([]StakeInfo, error) {
|
||||||
|
var resp struct {
|
||||||
|
Stakes []StakeInfo `json:"stakes"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, "/staking/stakes", &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Stakes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStake gets stake details.
|
||||||
|
func (c *Client) GetStake(ctx context.Context, stakeID string) (*StakeInfo, error) {
|
||||||
|
var resp StakeInfo
|
||||||
|
err := c.get(ctx, fmt.Sprintf("/staking/stakes/%s", url.PathEscape(stakeID)), &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStakingAPY gets current APY for staking.
|
||||||
|
func (c *Client) GetStakingAPY(ctx context.Context) (*StakingAPY, error) {
|
||||||
|
var resp StakingAPY
|
||||||
|
err := c.get(ctx, "/staking/apy", &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Discount Operations ====================
|
||||||
|
|
||||||
|
// ApplyDiscount applies a discount code.
|
||||||
|
func (c *Client) ApplyDiscount(ctx context.Context, code string) (*Discount, error) {
|
||||||
|
var resp Discount
|
||||||
|
err := c.post(ctx, "/discounts/apply", map[string]interface{}{"code": code}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAvailableDiscounts gets available discounts.
|
||||||
|
func (c *Client) GetAvailableDiscounts(ctx context.Context) ([]Discount, error) {
|
||||||
|
var resp struct {
|
||||||
|
Discounts []Discount `json:"discounts"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, "/discounts", &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Discounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveDiscounts gets active discounts on account.
|
||||||
|
func (c *Client) GetActiveDiscounts(ctx context.Context) ([]Discount, error) {
|
||||||
|
var resp struct {
|
||||||
|
Discounts []Discount `json:"discounts"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, "/discounts/active", &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Discounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveDiscount removes a discount.
|
||||||
|
func (c *Client) RemoveDiscount(ctx context.Context, code string) error {
|
||||||
|
return c.delete(ctx, fmt.Sprintf("/discounts/%s", url.PathEscape(code)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 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() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.closed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 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 &EconomicsError{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 &EconomicsError{
|
||||||
|
Message: msg,
|
||||||
|
Code: errResp.Code,
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != nil && len(respBody) > 0 {
|
||||||
|
return json.Unmarshal(respBody, result)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EconomicsError represents an error from the Economics API.
|
||||||
|
type EconomicsError struct {
|
||||||
|
Message string
|
||||||
|
Code string
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EconomicsError) Error() string {
|
||||||
|
if e.Code != "" {
|
||||||
|
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
|
||||||
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
722
sdk/go/governance/governance.go
Normal file
722
sdk/go/governance/governance.go
Normal file
|
|
@ -0,0 +1,722 @@
|
||||||
|
// Package governance provides the Synor Governance SDK for Go.
|
||||||
|
// Proposals, voting, DAOs, and vesting.
|
||||||
|
package governance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultEndpoint = "https://governance.synor.io/v1"
|
||||||
|
DefaultTimeout = 30 * time.Second
|
||||||
|
DefaultRetries = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProposalStatus represents proposal status.
|
||||||
|
type ProposalStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProposalDraft ProposalStatus = "draft"
|
||||||
|
ProposalActive ProposalStatus = "active"
|
||||||
|
ProposalPassed ProposalStatus = "passed"
|
||||||
|
ProposalRejected ProposalStatus = "rejected"
|
||||||
|
ProposalExecuted ProposalStatus = "executed"
|
||||||
|
ProposalCancelled ProposalStatus = "cancelled"
|
||||||
|
ProposalExpired ProposalStatus = "expired"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VoteChoice represents vote choice.
|
||||||
|
type VoteChoice string
|
||||||
|
|
||||||
|
const (
|
||||||
|
VoteFor VoteChoice = "for"
|
||||||
|
VoteAgainst VoteChoice = "against"
|
||||||
|
VoteAbstain VoteChoice = "abstain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProposalType represents proposal type.
|
||||||
|
type ProposalType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProposalParameterChange ProposalType = "parameter_change"
|
||||||
|
ProposalTreasurySpend ProposalType = "treasury_spend"
|
||||||
|
ProposalUpgrade ProposalType = "upgrade"
|
||||||
|
ProposalText ProposalType = "text"
|
||||||
|
ProposalCustom ProposalType = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DaoType represents DAO type.
|
||||||
|
type DaoType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DaoToken DaoType = "token"
|
||||||
|
DaoMultisig DaoType = "multisig"
|
||||||
|
DaoHybrid DaoType = "hybrid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VestingStatus represents vesting status.
|
||||||
|
type VestingStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
VestingActive VestingStatus = "active"
|
||||||
|
VestingPaused VestingStatus = "paused"
|
||||||
|
VestingCompleted VestingStatus = "completed"
|
||||||
|
VestingCancelled VestingStatus = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config for the Governance client.
|
||||||
|
type Config struct {
|
||||||
|
APIKey string
|
||||||
|
Endpoint string
|
||||||
|
Timeout time.Duration
|
||||||
|
Retries int
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProposalAction represents a proposal action.
|
||||||
|
type ProposalAction struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Calldata string `json:"calldata"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProposalDraft for creating proposals.
|
||||||
|
type ProposalDraft struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Type ProposalType `json:"type"`
|
||||||
|
Actions []ProposalAction `json:"actions,omitempty"`
|
||||||
|
StartTime *int64 `json:"start_time,omitempty"`
|
||||||
|
EndTime *int64 `json:"end_time,omitempty"`
|
||||||
|
Quorum string `json:"quorum,omitempty"`
|
||||||
|
Threshold string `json:"threshold,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proposal represents a governance proposal.
|
||||||
|
type Proposal struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Type ProposalType `json:"type"`
|
||||||
|
Status ProposalStatus `json:"status"`
|
||||||
|
Proposer string `json:"proposer"`
|
||||||
|
Actions []ProposalAction `json:"actions"`
|
||||||
|
StartTime int64 `json:"start_time"`
|
||||||
|
EndTime int64 `json:"end_time"`
|
||||||
|
Quorum string `json:"quorum"`
|
||||||
|
Threshold string `json:"threshold"`
|
||||||
|
ForVotes string `json:"for_votes"`
|
||||||
|
AgainstVotes string `json:"against_votes"`
|
||||||
|
AbstainVotes string `json:"abstain_votes"`
|
||||||
|
TotalVotes string `json:"total_votes"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
ExecutedAt *int64 `json:"executed_at,omitempty"`
|
||||||
|
ExecutionTxHash string `json:"execution_tx_hash,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProposalFilter for filtering proposals.
|
||||||
|
type ProposalFilter struct {
|
||||||
|
Status *ProposalStatus
|
||||||
|
Type *ProposalType
|
||||||
|
Proposer string
|
||||||
|
Limit *int
|
||||||
|
Offset *int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vote represents a vote.
|
||||||
|
type Vote struct {
|
||||||
|
Choice VoteChoice `json:"choice"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoteReceipt represents a vote receipt.
|
||||||
|
type VoteReceipt struct {
|
||||||
|
ProposalID string `json:"proposal_id"`
|
||||||
|
Voter string `json:"voter"`
|
||||||
|
Choice VoteChoice `json:"choice"`
|
||||||
|
Weight string `json:"weight"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
TxHash string `json:"tx_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelegationInfo represents delegation information.
|
||||||
|
type DelegationInfo struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelegationReceipt represents a delegation receipt.
|
||||||
|
type DelegationReceipt struct {
|
||||||
|
Delegator string `json:"delegator"`
|
||||||
|
Delegatee string `json:"delegatee"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
TxHash string `json:"tx_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VotingPower represents voting power.
|
||||||
|
type VotingPower struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
OwnPower string `json:"own_power"`
|
||||||
|
DelegatedPower string `json:"delegated_power"`
|
||||||
|
TotalPower string `json:"total_power"`
|
||||||
|
DelegatedFrom []DelegationInfo `json:"delegated_from"`
|
||||||
|
DelegatedTo *DelegationInfo `json:"delegated_to,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaoConfig for creating DAOs.
|
||||||
|
type DaoConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type DaoType `json:"type"`
|
||||||
|
TokenAddress string `json:"token_address,omitempty"`
|
||||||
|
Signers []string `json:"signers,omitempty"`
|
||||||
|
Threshold *int `json:"threshold,omitempty"`
|
||||||
|
VotingPeriod int `json:"voting_period"`
|
||||||
|
Quorum string `json:"quorum"`
|
||||||
|
ProposalThreshold string `json:"proposal_threshold"`
|
||||||
|
TimelockDelay *int `json:"timelock_delay,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dao represents a DAO.
|
||||||
|
type Dao struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type DaoType `json:"type"`
|
||||||
|
TokenAddress string `json:"token_address,omitempty"`
|
||||||
|
Signers []string `json:"signers,omitempty"`
|
||||||
|
Threshold *int `json:"threshold,omitempty"`
|
||||||
|
VotingPeriod int `json:"voting_period"`
|
||||||
|
Quorum string `json:"quorum"`
|
||||||
|
ProposalThreshold string `json:"proposal_threshold"`
|
||||||
|
TimelockDelay *int `json:"timelock_delay,omitempty"`
|
||||||
|
Treasury string `json:"treasury"`
|
||||||
|
TotalProposals int `json:"total_proposals"`
|
||||||
|
ActiveProposals int `json:"active_proposals"`
|
||||||
|
TotalMembers int `json:"total_members"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VestingSchedule for creating vesting contracts.
|
||||||
|
type VestingSchedule struct {
|
||||||
|
Beneficiary string `json:"beneficiary"`
|
||||||
|
TotalAmount string `json:"total_amount"`
|
||||||
|
StartTime int64 `json:"start_time"`
|
||||||
|
CliffDuration int `json:"cliff_duration"`
|
||||||
|
VestingDuration int `json:"vesting_duration"`
|
||||||
|
Revocable bool `json:"revocable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VestingContract represents a vesting contract.
|
||||||
|
type VestingContract struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Beneficiary string `json:"beneficiary"`
|
||||||
|
TotalAmount string `json:"total_amount"`
|
||||||
|
ReleasedAmount string `json:"released_amount"`
|
||||||
|
VestedAmount string `json:"vested_amount"`
|
||||||
|
StartTime int64 `json:"start_time"`
|
||||||
|
CliffTime int64 `json:"cliff_time"`
|
||||||
|
EndTime int64 `json:"end_time"`
|
||||||
|
Revocable bool `json:"revocable"`
|
||||||
|
Status VestingStatus `json:"status"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
TxHash string `json:"tx_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimResult represents a claim result.
|
||||||
|
type ClaimResult struct {
|
||||||
|
VestingID string `json:"vesting_id"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
Recipient string `json:"recipient"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
TxHash string `json:"tx_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GovernanceTransaction represents a transaction result.
|
||||||
|
type GovernanceTransaction struct {
|
||||||
|
TxHash string `json:"tx_hash"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
BlockNumber int64 `json:"block_number"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaoMember represents a DAO member.
|
||||||
|
type DaoMember struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Power string `json:"power"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimableAmount represents claimable amount.
|
||||||
|
type ClaimableAmount struct {
|
||||||
|
Claimable string `json:"claimable"`
|
||||||
|
Vested string `json:"vested"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client for the Synor Governance SDK.
|
||||||
|
type Client struct {
|
||||||
|
config Config
|
||||||
|
client *http.Client
|
||||||
|
mu sync.RWMutex
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Governance 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},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Proposal Operations ====================
|
||||||
|
|
||||||
|
// CreateProposal creates a new proposal.
|
||||||
|
func (c *Client) CreateProposal(ctx context.Context, draft ProposalDraft) (*Proposal, error) {
|
||||||
|
var resp Proposal
|
||||||
|
err := c.post(ctx, "/proposals", draft, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProposal gets a proposal by ID.
|
||||||
|
func (c *Client) GetProposal(ctx context.Context, proposalID string) (*Proposal, error) {
|
||||||
|
var resp Proposal
|
||||||
|
err := c.get(ctx, fmt.Sprintf("/proposals/%s", url.PathEscape(proposalID)), &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProposals lists proposals with optional filtering.
|
||||||
|
func (c *Client) ListProposals(ctx context.Context, filter *ProposalFilter) ([]Proposal, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
if filter != nil {
|
||||||
|
if filter.Status != nil {
|
||||||
|
params.Set("status", string(*filter.Status))
|
||||||
|
}
|
||||||
|
if filter.Type != nil {
|
||||||
|
params.Set("type", string(*filter.Type))
|
||||||
|
}
|
||||||
|
if filter.Proposer != "" {
|
||||||
|
params.Set("proposer", filter.Proposer)
|
||||||
|
}
|
||||||
|
if filter.Limit != nil {
|
||||||
|
params.Set("limit", fmt.Sprintf("%d", *filter.Limit))
|
||||||
|
}
|
||||||
|
if filter.Offset != nil {
|
||||||
|
params.Set("offset", fmt.Sprintf("%d", *filter.Offset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path := "/proposals"
|
||||||
|
if len(params) > 0 {
|
||||||
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Proposals []Proposal `json:"proposals"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, path, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Proposals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelProposal cancels a proposal.
|
||||||
|
func (c *Client) CancelProposal(ctx context.Context, proposalID string) (*Proposal, error) {
|
||||||
|
var resp Proposal
|
||||||
|
err := c.post(ctx, fmt.Sprintf("/proposals/%s/cancel", url.PathEscape(proposalID)), nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteProposal executes a passed proposal.
|
||||||
|
func (c *Client) ExecuteProposal(ctx context.Context, proposalID string) (*GovernanceTransaction, error) {
|
||||||
|
var resp GovernanceTransaction
|
||||||
|
err := c.post(ctx, fmt.Sprintf("/proposals/%s/execute", url.PathEscape(proposalID)), nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Voting Operations ====================
|
||||||
|
|
||||||
|
// Vote votes on a proposal.
|
||||||
|
func (c *Client) Vote(ctx context.Context, proposalID string, vote Vote, weight string) (*VoteReceipt, error) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"choice": vote.Choice,
|
||||||
|
}
|
||||||
|
if vote.Reason != "" {
|
||||||
|
body["reason"] = vote.Reason
|
||||||
|
}
|
||||||
|
if weight != "" {
|
||||||
|
body["weight"] = weight
|
||||||
|
}
|
||||||
|
var resp VoteReceipt
|
||||||
|
err := c.post(ctx, fmt.Sprintf("/proposals/%s/vote", url.PathEscape(proposalID)), body, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVotes gets votes for a proposal.
|
||||||
|
func (c *Client) GetVotes(ctx context.Context, proposalID string, limit, offset *int) ([]VoteReceipt, 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 := fmt.Sprintf("/proposals/%s/votes", url.PathEscape(proposalID))
|
||||||
|
if len(params) > 0 {
|
||||||
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Votes []VoteReceipt `json:"votes"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, path, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Votes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate delegates voting power.
|
||||||
|
func (c *Client) Delegate(ctx context.Context, to string, amount string) (*DelegationReceipt, error) {
|
||||||
|
body := map[string]interface{}{"to": to}
|
||||||
|
if amount != "" {
|
||||||
|
body["amount"] = amount
|
||||||
|
}
|
||||||
|
var resp DelegationReceipt
|
||||||
|
err := c.post(ctx, "/voting/delegate", body, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undelegate undelegates voting power.
|
||||||
|
func (c *Client) Undelegate(ctx context.Context, from string) (*DelegationReceipt, error) {
|
||||||
|
body := make(map[string]interface{})
|
||||||
|
if from != "" {
|
||||||
|
body["from"] = from
|
||||||
|
}
|
||||||
|
var resp DelegationReceipt
|
||||||
|
err := c.post(ctx, "/voting/undelegate", body, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVotingPower gets voting power for an address.
|
||||||
|
func (c *Client) GetVotingPower(ctx context.Context, address string) (*VotingPower, error) {
|
||||||
|
var resp VotingPower
|
||||||
|
err := c.get(ctx, fmt.Sprintf("/voting/power/%s", url.PathEscape(address)), &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMyVotingPower gets voting power for the authenticated user.
|
||||||
|
func (c *Client) GetMyVotingPower(ctx context.Context) (*VotingPower, error) {
|
||||||
|
var resp VotingPower
|
||||||
|
err := c.get(ctx, "/voting/power", &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DAO Operations ====================
|
||||||
|
|
||||||
|
// CreateDao creates a new DAO.
|
||||||
|
func (c *Client) CreateDao(ctx context.Context, config DaoConfig) (*Dao, error) {
|
||||||
|
var resp Dao
|
||||||
|
err := c.post(ctx, "/daos", config, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDao gets a DAO by ID.
|
||||||
|
func (c *Client) GetDao(ctx context.Context, daoID string) (*Dao, error) {
|
||||||
|
var resp Dao
|
||||||
|
err := c.get(ctx, fmt.Sprintf("/daos/%s", url.PathEscape(daoID)), &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDaos lists DAOs.
|
||||||
|
func (c *Client) ListDaos(ctx context.Context, limit, offset *int) ([]Dao, 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 := "/daos"
|
||||||
|
if len(params) > 0 {
|
||||||
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Daos []Dao `json:"daos"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, path, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Daos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDaoMembers gets DAO members.
|
||||||
|
func (c *Client) GetDaoMembers(ctx context.Context, daoID string, limit, offset *int) ([]DaoMember, 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 := fmt.Sprintf("/daos/%s/members", url.PathEscape(daoID))
|
||||||
|
if len(params) > 0 {
|
||||||
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Members []DaoMember `json:"members"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, path, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Vesting Operations ====================
|
||||||
|
|
||||||
|
// CreateVestingSchedule creates a vesting schedule.
|
||||||
|
func (c *Client) CreateVestingSchedule(ctx context.Context, schedule VestingSchedule) (*VestingContract, error) {
|
||||||
|
var resp VestingContract
|
||||||
|
err := c.post(ctx, "/vesting", schedule, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVestingContract gets a vesting contract.
|
||||||
|
func (c *Client) GetVestingContract(ctx context.Context, contractID string) (*VestingContract, error) {
|
||||||
|
var resp VestingContract
|
||||||
|
err := c.get(ctx, fmt.Sprintf("/vesting/%s", url.PathEscape(contractID)), &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVestingContracts lists vesting contracts.
|
||||||
|
func (c *Client) ListVestingContracts(ctx context.Context, beneficiary string) ([]VestingContract, error) {
|
||||||
|
path := "/vesting"
|
||||||
|
if beneficiary != "" {
|
||||||
|
path = fmt.Sprintf("%s?beneficiary=%s", path, url.QueryEscape(beneficiary))
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Contracts []VestingContract `json:"contracts"`
|
||||||
|
}
|
||||||
|
err := c.get(ctx, path, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Contracts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimVested claims vested tokens.
|
||||||
|
func (c *Client) ClaimVested(ctx context.Context, contractID string) (*ClaimResult, error) {
|
||||||
|
var resp ClaimResult
|
||||||
|
err := c.post(ctx, fmt.Sprintf("/vesting/%s/claim", url.PathEscape(contractID)), nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeVesting revokes a vesting contract.
|
||||||
|
func (c *Client) RevokeVesting(ctx context.Context, contractID string) (*VestingContract, error) {
|
||||||
|
var resp VestingContract
|
||||||
|
err := c.post(ctx, fmt.Sprintf("/vesting/%s/revoke", url.PathEscape(contractID)), nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClaimableAmount gets claimable amount for a vesting contract.
|
||||||
|
func (c *Client) GetClaimableAmount(ctx context.Context, contractID string) (*ClaimableAmount, error) {
|
||||||
|
var resp ClaimableAmount
|
||||||
|
err := c.get(ctx, fmt.Sprintf("/vesting/%s/claimable", url.PathEscape(contractID)), &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, 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() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.closed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 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) execute(ctx context.Context, method, path string, body, result interface{}) error {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.closed {
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return &GovernanceError{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 &GovernanceError{
|
||||||
|
Message: msg,
|
||||||
|
Code: errResp.Code,
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != nil && len(respBody) > 0 {
|
||||||
|
return json.Unmarshal(respBody, result)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GovernanceError represents an error from the Governance API.
|
||||||
|
type GovernanceError struct {
|
||||||
|
Message string
|
||||||
|
Code string
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GovernanceError) Error() string {
|
||||||
|
if e.Code != "" {
|
||||||
|
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
|
||||||
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
786
sdk/go/mining/mining.go
Normal file
786
sdk/go/mining/mining.go
Normal file
|
|
@ -0,0 +1,786 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
361
sdk/js/src/economics/client.ts
Normal file
361
sdk/js/src/economics/client.ts
Normal file
|
|
@ -0,0 +1,361 @@
|
||||||
|
/**
|
||||||
|
* Synor Economics SDK Client
|
||||||
|
* Pricing, billing, staking, and discount management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EconomicsConfig,
|
||||||
|
ServiceType,
|
||||||
|
UsageMetrics,
|
||||||
|
Price,
|
||||||
|
UsagePlan,
|
||||||
|
CostEstimate,
|
||||||
|
BillingPeriod,
|
||||||
|
Usage,
|
||||||
|
Invoice,
|
||||||
|
AccountBalance,
|
||||||
|
StakeReceipt,
|
||||||
|
UnstakeReceipt,
|
||||||
|
StakingRewards,
|
||||||
|
StakeInfo,
|
||||||
|
StakeOptions,
|
||||||
|
Discount,
|
||||||
|
PriceResponse,
|
||||||
|
InvoicesResponse,
|
||||||
|
StakesResponse,
|
||||||
|
DiscountsResponse,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT = 'https://economics.synor.io/v1';
|
||||||
|
const DEFAULT_TIMEOUT = 30000;
|
||||||
|
const DEFAULT_RETRIES = 3;
|
||||||
|
|
||||||
|
export class SynorEconomics {
|
||||||
|
private config: Required<EconomicsConfig>;
|
||||||
|
private closed = false;
|
||||||
|
|
||||||
|
constructor(config: EconomicsConfig) {
|
||||||
|
this.config = {
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
||||||
|
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
||||||
|
retries: config.retries ?? DEFAULT_RETRIES,
|
||||||
|
debug: config.debug ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Pricing Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get price for a service based on usage.
|
||||||
|
*/
|
||||||
|
async getPrice(service: ServiceType, usage: UsageMetrics): Promise<Price> {
|
||||||
|
const response = await this.post<PriceResponse>('/pricing/calculate', {
|
||||||
|
service,
|
||||||
|
usage,
|
||||||
|
});
|
||||||
|
return response.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate cost for a usage plan.
|
||||||
|
*/
|
||||||
|
async estimateCost(plan: UsagePlan): Promise<CostEstimate> {
|
||||||
|
return await this.post<CostEstimate>('/pricing/estimate', plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pricing tiers for a service.
|
||||||
|
*/
|
||||||
|
async getPricingTiers(service: ServiceType): Promise<Price[]> {
|
||||||
|
const response = await this.get<{ tiers: Price[] }>(`/pricing/${service}/tiers`);
|
||||||
|
return response.tiers ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Usage & Billing Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get usage for a billing period.
|
||||||
|
*/
|
||||||
|
async getUsage(period?: BillingPeriod): Promise<Usage> {
|
||||||
|
const params = period ? `?period=${period}` : '';
|
||||||
|
return await this.get<Usage>(`/usage${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get usage history.
|
||||||
|
*/
|
||||||
|
async getUsageHistory(limit?: number, offset?: number): Promise<Usage[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (limit) params.set('limit', limit.toString());
|
||||||
|
if (offset) params.set('offset', offset.toString());
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const response = await this.get<{ usage: Usage[] }>(`/usage/history${query}`);
|
||||||
|
return response.usage ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get invoices.
|
||||||
|
*/
|
||||||
|
async getInvoices(): Promise<Invoice[]> {
|
||||||
|
const response = await this.get<InvoicesResponse>('/invoices');
|
||||||
|
return response.invoices ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific invoice.
|
||||||
|
*/
|
||||||
|
async getInvoice(invoiceId: string): Promise<Invoice> {
|
||||||
|
return await this.get<Invoice>(`/invoices/${encodeURIComponent(invoiceId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pay an invoice.
|
||||||
|
*/
|
||||||
|
async payInvoice(invoiceId: string, paymentMethod?: string): Promise<Invoice> {
|
||||||
|
return await this.post<Invoice>(`/invoices/${encodeURIComponent(invoiceId)}/pay`, {
|
||||||
|
paymentMethod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get account balance.
|
||||||
|
*/
|
||||||
|
async getBalance(): Promise<AccountBalance> {
|
||||||
|
return await this.get<AccountBalance>('/balance');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add funds to account.
|
||||||
|
*/
|
||||||
|
async addFunds(amount: string, paymentMethod: string): Promise<AccountBalance> {
|
||||||
|
return await this.post<AccountBalance>('/balance/deposit', {
|
||||||
|
amount,
|
||||||
|
paymentMethod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Staking Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stake tokens.
|
||||||
|
*/
|
||||||
|
async stake(amount: string, options?: StakeOptions): Promise<StakeReceipt> {
|
||||||
|
const body: Record<string, unknown> = { amount };
|
||||||
|
if (options?.validator) body.validator = options.validator;
|
||||||
|
if (options?.autoCompound !== undefined) body.autoCompound = options.autoCompound;
|
||||||
|
if (options?.lockDuration) body.lockDuration = options.lockDuration;
|
||||||
|
return await this.post<StakeReceipt>('/staking/stake', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unstake tokens.
|
||||||
|
*/
|
||||||
|
async unstake(stakeId: string): Promise<UnstakeReceipt> {
|
||||||
|
return await this.post<UnstakeReceipt>(`/staking/stakes/${encodeURIComponent(stakeId)}/unstake`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get staking rewards.
|
||||||
|
*/
|
||||||
|
async getStakingRewards(): Promise<StakingRewards> {
|
||||||
|
return await this.get<StakingRewards>('/staking/rewards');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim staking rewards.
|
||||||
|
*/
|
||||||
|
async claimRewards(stakeId?: string): Promise<StakingRewards> {
|
||||||
|
const path = stakeId
|
||||||
|
? `/staking/stakes/${encodeURIComponent(stakeId)}/claim`
|
||||||
|
: '/staking/rewards/claim';
|
||||||
|
return await this.post<StakingRewards>(path, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List active stakes.
|
||||||
|
*/
|
||||||
|
async listStakes(): Promise<StakeInfo[]> {
|
||||||
|
const response = await this.get<StakesResponse>('/staking/stakes');
|
||||||
|
return response.stakes ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stake details.
|
||||||
|
*/
|
||||||
|
async getStake(stakeId: string): Promise<StakeInfo> {
|
||||||
|
return await this.get<StakeInfo>(`/staking/stakes/${encodeURIComponent(stakeId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current APY for staking.
|
||||||
|
*/
|
||||||
|
async getStakingApy(): Promise<{ apy: string; minLockDuration: number; maxLockDuration: number }> {
|
||||||
|
return await this.get('/staking/apy');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Discount Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a discount code.
|
||||||
|
*/
|
||||||
|
async applyDiscount(code: string): Promise<Discount> {
|
||||||
|
return await this.post<Discount>('/discounts/apply', { code });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available discounts.
|
||||||
|
*/
|
||||||
|
async getAvailableDiscounts(): Promise<Discount[]> {
|
||||||
|
const response = await this.get<DiscountsResponse>('/discounts');
|
||||||
|
return response.discounts ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active discounts on account.
|
||||||
|
*/
|
||||||
|
async getActiveDiscounts(): Promise<Discount[]> {
|
||||||
|
const response = await this.get<DiscountsResponse>('/discounts/active');
|
||||||
|
return response.discounts ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a discount.
|
||||||
|
*/
|
||||||
|
async removeDiscount(code: string): Promise<void> {
|
||||||
|
await this.delete(`/discounts/${encodeURIComponent(code)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check.
|
||||||
|
*/
|
||||||
|
async healthCheck(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.get<{ status: string }>('/health');
|
||||||
|
return response.status === 'healthy';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if client is closed.
|
||||||
|
*/
|
||||||
|
get isClosed(): boolean {
|
||||||
|
return this.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the client.
|
||||||
|
*/
|
||||||
|
close(): void {
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Private HTTP Methods ====================
|
||||||
|
|
||||||
|
private async get<T>(path: string): Promise<T> {
|
||||||
|
return await this.execute(async () => {
|
||||||
|
const response = await fetch(`${this.config.endpoint}${path}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.headers(),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
await this.ensureSuccess(response);
|
||||||
|
return await response.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async post<T>(path: string, body: unknown): Promise<T> {
|
||||||
|
return await this.execute(async () => {
|
||||||
|
const response = await fetch(`${this.config.endpoint}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers(),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
await this.ensureSuccess(response);
|
||||||
|
return await response.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async delete(path: string): Promise<void> {
|
||||||
|
return await this.execute(async () => {
|
||||||
|
const response = await fetch(`${this.config.endpoint}${path}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.headers(),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
await this.ensureSuccess(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private headers(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${this.config.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-SDK-Version': 'js/0.1.0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async execute<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
|
if (this.closed) {
|
||||||
|
throw new EconomicsError('Client has been closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastError: Error | undefined;
|
||||||
|
for (let attempt = 0; attempt < this.config.retries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error;
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log(`Attempt ${attempt + 1} failed:`, error);
|
||||||
|
}
|
||||||
|
if (attempt < this.config.retries - 1) {
|
||||||
|
await this.sleep(1000 * Math.pow(2, attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureSuccess(response: Response): Promise<void> {
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
let message = `HTTP ${response.status}`;
|
||||||
|
let code: string | undefined;
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(body);
|
||||||
|
message = error.message ?? message;
|
||||||
|
code = error.code;
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
throw new EconomicsError(message, code, response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Economics SDK Error
|
||||||
|
*/
|
||||||
|
export class EconomicsError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly code?: string,
|
||||||
|
public readonly statusCode?: number
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'EconomicsError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export * from './types';
|
||||||
7
sdk/js/src/economics/index.ts
Normal file
7
sdk/js/src/economics/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Synor Economics SDK
|
||||||
|
* Pricing, billing, staking, and discount management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SynorEconomics, EconomicsError } from './client';
|
||||||
|
export * from './types';
|
||||||
258
sdk/js/src/economics/types.ts
Normal file
258
sdk/js/src/economics/types.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
/**
|
||||||
|
* Synor Economics SDK Types
|
||||||
|
* Pricing, billing, staking, and discount management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Service types for pricing
|
||||||
|
export type ServiceType =
|
||||||
|
| 'compute'
|
||||||
|
| 'storage'
|
||||||
|
| 'database'
|
||||||
|
| 'hosting'
|
||||||
|
| 'bridge'
|
||||||
|
| 'rpc';
|
||||||
|
|
||||||
|
// Billing period
|
||||||
|
export type BillingPeriod = 'hourly' | 'daily' | 'weekly' | 'monthly';
|
||||||
|
|
||||||
|
// Stake status
|
||||||
|
export type StakeStatus = 'active' | 'unstaking' | 'withdrawn' | 'slashed';
|
||||||
|
|
||||||
|
// Discount type
|
||||||
|
export type DiscountType = 'percentage' | 'fixed' | 'volume' | 'referral';
|
||||||
|
|
||||||
|
// Invoice status
|
||||||
|
export type InvoiceStatus = 'pending' | 'paid' | 'overdue' | 'cancelled';
|
||||||
|
|
||||||
|
// Usage metrics for pricing
|
||||||
|
export interface UsageMetrics {
|
||||||
|
computeHours?: number;
|
||||||
|
storageBytes?: number;
|
||||||
|
databaseOps?: number;
|
||||||
|
hostingRequests?: number;
|
||||||
|
bridgeTransfers?: number;
|
||||||
|
rpcCalls?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage plan for cost estimation
|
||||||
|
export interface UsagePlan {
|
||||||
|
services: ServiceUsage[];
|
||||||
|
period: BillingPeriod;
|
||||||
|
startDate?: number;
|
||||||
|
endDate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service usage in plan
|
||||||
|
export interface ServiceUsage {
|
||||||
|
service: ServiceType;
|
||||||
|
metrics: UsageMetrics;
|
||||||
|
tier?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price breakdown
|
||||||
|
export interface Price {
|
||||||
|
service: ServiceType;
|
||||||
|
basePrice: string;
|
||||||
|
quantity: string;
|
||||||
|
unit: string;
|
||||||
|
subtotal: string;
|
||||||
|
discounts: AppliedDiscount[];
|
||||||
|
total: string;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cost estimate
|
||||||
|
export interface CostEstimate {
|
||||||
|
services: Price[];
|
||||||
|
subtotal: string;
|
||||||
|
discounts: AppliedDiscount[];
|
||||||
|
taxes: TaxAmount[];
|
||||||
|
total: string;
|
||||||
|
currency: string;
|
||||||
|
validUntil: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tax amount
|
||||||
|
export interface TaxAmount {
|
||||||
|
name: string;
|
||||||
|
rate: number;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applied discount
|
||||||
|
export interface AppliedDiscount {
|
||||||
|
code: string;
|
||||||
|
type: DiscountType;
|
||||||
|
value: string;
|
||||||
|
savings: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage record
|
||||||
|
export interface Usage {
|
||||||
|
period: BillingPeriod;
|
||||||
|
startDate: number;
|
||||||
|
endDate: number;
|
||||||
|
services: ServiceUsageRecord[];
|
||||||
|
totalCost: string;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service usage record
|
||||||
|
export interface ServiceUsageRecord {
|
||||||
|
service: ServiceType;
|
||||||
|
metrics: UsageMetrics;
|
||||||
|
cost: string;
|
||||||
|
details: UsageDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage detail
|
||||||
|
export interface UsageDetail {
|
||||||
|
timestamp: number;
|
||||||
|
operation: string;
|
||||||
|
quantity: number;
|
||||||
|
unit: string;
|
||||||
|
cost: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoice
|
||||||
|
export interface Invoice {
|
||||||
|
id: string;
|
||||||
|
status: InvoiceStatus;
|
||||||
|
period: BillingPeriod;
|
||||||
|
startDate: number;
|
||||||
|
endDate: number;
|
||||||
|
items: InvoiceItem[];
|
||||||
|
subtotal: string;
|
||||||
|
discounts: AppliedDiscount[];
|
||||||
|
taxes: TaxAmount[];
|
||||||
|
total: string;
|
||||||
|
currency: string;
|
||||||
|
dueDate: number;
|
||||||
|
paidAt?: number;
|
||||||
|
paymentMethod?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoice item
|
||||||
|
export interface InvoiceItem {
|
||||||
|
service: ServiceType;
|
||||||
|
description: string;
|
||||||
|
quantity: string;
|
||||||
|
unitPrice: string;
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account balance
|
||||||
|
export interface AccountBalance {
|
||||||
|
available: string;
|
||||||
|
pending: string;
|
||||||
|
reserved: string;
|
||||||
|
total: string;
|
||||||
|
currency: string;
|
||||||
|
creditLimit?: string;
|
||||||
|
lastUpdated: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stake receipt
|
||||||
|
export interface StakeReceipt {
|
||||||
|
id: string;
|
||||||
|
amount: string;
|
||||||
|
lockDuration: number;
|
||||||
|
startDate: number;
|
||||||
|
endDate: number;
|
||||||
|
apy: string;
|
||||||
|
estimatedRewards: string;
|
||||||
|
txHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unstake receipt
|
||||||
|
export interface UnstakeReceipt {
|
||||||
|
id: string;
|
||||||
|
stakeId: string;
|
||||||
|
amount: string;
|
||||||
|
rewards: string;
|
||||||
|
total: string;
|
||||||
|
unbondingPeriod: number;
|
||||||
|
availableAt: number;
|
||||||
|
txHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stake info
|
||||||
|
export interface StakeInfo {
|
||||||
|
id: string;
|
||||||
|
amount: string;
|
||||||
|
status: StakeStatus;
|
||||||
|
lockDuration: number;
|
||||||
|
startDate: number;
|
||||||
|
endDate: number;
|
||||||
|
apy: string;
|
||||||
|
earnedRewards: string;
|
||||||
|
pendingRewards: string;
|
||||||
|
validator?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Staking rewards
|
||||||
|
export interface StakingRewards {
|
||||||
|
totalEarned: string;
|
||||||
|
pending: string;
|
||||||
|
claimed: string;
|
||||||
|
lastClaimDate?: number;
|
||||||
|
nextClaimAvailable?: number;
|
||||||
|
stakes: StakeRewardDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stake reward detail
|
||||||
|
export interface StakeRewardDetail {
|
||||||
|
stakeId: string;
|
||||||
|
earned: string;
|
||||||
|
pending: string;
|
||||||
|
apy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discount
|
||||||
|
export interface Discount {
|
||||||
|
code: string;
|
||||||
|
type: DiscountType;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
validFrom: number;
|
||||||
|
validUntil: number;
|
||||||
|
minPurchase?: string;
|
||||||
|
maxDiscount?: string;
|
||||||
|
applicableServices: ServiceType[];
|
||||||
|
usageLimit?: number;
|
||||||
|
usedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Staking options
|
||||||
|
export interface StakeOptions {
|
||||||
|
validator?: string;
|
||||||
|
autoCompound?: boolean;
|
||||||
|
lockDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Economics config
|
||||||
|
export interface EconomicsConfig {
|
||||||
|
apiKey: string;
|
||||||
|
endpoint?: string;
|
||||||
|
timeout?: number;
|
||||||
|
retries?: number;
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal response types
|
||||||
|
export interface PriceResponse {
|
||||||
|
price: Price;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvoicesResponse {
|
||||||
|
invoices: Invoice[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StakesResponse {
|
||||||
|
stakes: StakeInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscountsResponse {
|
||||||
|
discounts: Discount[];
|
||||||
|
}
|
||||||
377
sdk/js/src/governance/client.ts
Normal file
377
sdk/js/src/governance/client.ts
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
/**
|
||||||
|
* Synor Governance SDK Client
|
||||||
|
* Proposals, voting, DAOs, and vesting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GovernanceConfig,
|
||||||
|
ProposalDraft,
|
||||||
|
Proposal,
|
||||||
|
ProposalFilter,
|
||||||
|
Vote,
|
||||||
|
VoteReceipt,
|
||||||
|
DelegationReceipt,
|
||||||
|
VotingPower,
|
||||||
|
DaoConfig,
|
||||||
|
Dao,
|
||||||
|
VestingSchedule,
|
||||||
|
VestingContract,
|
||||||
|
ClaimResult,
|
||||||
|
GovernanceTransaction,
|
||||||
|
ProposalsResponse,
|
||||||
|
DaosResponse,
|
||||||
|
VestingContractsResponse,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT = 'https://governance.synor.io/v1';
|
||||||
|
const DEFAULT_TIMEOUT = 30000;
|
||||||
|
const DEFAULT_RETRIES = 3;
|
||||||
|
|
||||||
|
export class SynorGovernance {
|
||||||
|
private config: Required<GovernanceConfig>;
|
||||||
|
private closed = false;
|
||||||
|
|
||||||
|
constructor(config: GovernanceConfig) {
|
||||||
|
this.config = {
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
||||||
|
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
||||||
|
retries: config.retries ?? DEFAULT_RETRIES,
|
||||||
|
debug: config.debug ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Proposal Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new proposal.
|
||||||
|
*/
|
||||||
|
async createProposal(draft: ProposalDraft): Promise<Proposal> {
|
||||||
|
return await this.post<Proposal>('/proposals', draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a proposal by ID.
|
||||||
|
*/
|
||||||
|
async getProposal(proposalId: string): Promise<Proposal> {
|
||||||
|
return await this.get<Proposal>(`/proposals/${encodeURIComponent(proposalId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List proposals with optional filtering.
|
||||||
|
*/
|
||||||
|
async listProposals(filter?: ProposalFilter): Promise<Proposal[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter?.status) params.set('status', filter.status);
|
||||||
|
if (filter?.type) params.set('type', filter.type);
|
||||||
|
if (filter?.proposer) params.set('proposer', filter.proposer);
|
||||||
|
if (filter?.limit) params.set('limit', filter.limit.toString());
|
||||||
|
if (filter?.offset) params.set('offset', filter.offset.toString());
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const response = await this.get<ProposalsResponse>(`/proposals${query}`);
|
||||||
|
return response.proposals ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a proposal.
|
||||||
|
*/
|
||||||
|
async cancelProposal(proposalId: string): Promise<Proposal> {
|
||||||
|
return await this.post<Proposal>(`/proposals/${encodeURIComponent(proposalId)}/cancel`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a passed proposal.
|
||||||
|
*/
|
||||||
|
async executeProposal(proposalId: string): Promise<GovernanceTransaction> {
|
||||||
|
return await this.post<GovernanceTransaction>(
|
||||||
|
`/proposals/${encodeURIComponent(proposalId)}/execute`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Voting Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vote on a proposal.
|
||||||
|
*/
|
||||||
|
async vote(proposalId: string, vote: Vote, weight?: string): Promise<VoteReceipt> {
|
||||||
|
const body: Record<string, unknown> = { ...vote };
|
||||||
|
if (weight) body.weight = weight;
|
||||||
|
return await this.post<VoteReceipt>(
|
||||||
|
`/proposals/${encodeURIComponent(proposalId)}/vote`,
|
||||||
|
body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get votes for a proposal.
|
||||||
|
*/
|
||||||
|
async getVotes(
|
||||||
|
proposalId: string,
|
||||||
|
limit?: number,
|
||||||
|
offset?: number
|
||||||
|
): Promise<VoteReceipt[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (limit) params.set('limit', limit.toString());
|
||||||
|
if (offset) params.set('offset', offset.toString());
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const response = await this.get<{ votes: VoteReceipt[] }>(
|
||||||
|
`/proposals/${encodeURIComponent(proposalId)}/votes${query}`
|
||||||
|
);
|
||||||
|
return response.votes ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate voting power to another address.
|
||||||
|
*/
|
||||||
|
async delegate(to: string, amount?: string): Promise<DelegationReceipt> {
|
||||||
|
const body: Record<string, unknown> = { to };
|
||||||
|
if (amount) body.amount = amount;
|
||||||
|
return await this.post<DelegationReceipt>('/voting/delegate', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undelegate voting power.
|
||||||
|
*/
|
||||||
|
async undelegate(from?: string): Promise<DelegationReceipt> {
|
||||||
|
const body: Record<string, unknown> = {};
|
||||||
|
if (from) body.from = from;
|
||||||
|
return await this.post<DelegationReceipt>('/voting/undelegate', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get voting power for an address.
|
||||||
|
*/
|
||||||
|
async getVotingPower(address: string): Promise<VotingPower> {
|
||||||
|
return await this.get<VotingPower>(`/voting/power/${encodeURIComponent(address)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current voting power for the authenticated user.
|
||||||
|
*/
|
||||||
|
async getMyVotingPower(): Promise<VotingPower> {
|
||||||
|
return await this.get<VotingPower>('/voting/power');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DAO Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DAO.
|
||||||
|
*/
|
||||||
|
async createDao(config: DaoConfig): Promise<Dao> {
|
||||||
|
return await this.post<Dao>('/daos', config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a DAO by ID.
|
||||||
|
*/
|
||||||
|
async getDao(daoId: string): Promise<Dao> {
|
||||||
|
return await this.get<Dao>(`/daos/${encodeURIComponent(daoId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List DAOs.
|
||||||
|
*/
|
||||||
|
async listDaos(limit?: number, offset?: number): Promise<Dao[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (limit) params.set('limit', limit.toString());
|
||||||
|
if (offset) params.set('offset', offset.toString());
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const response = await this.get<DaosResponse>(`/daos${query}`);
|
||||||
|
return response.daos ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get DAO members.
|
||||||
|
*/
|
||||||
|
async getDaoMembers(
|
||||||
|
daoId: string,
|
||||||
|
limit?: number,
|
||||||
|
offset?: number
|
||||||
|
): Promise<{ address: string; power: string }[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (limit) params.set('limit', limit.toString());
|
||||||
|
if (offset) params.set('offset', offset.toString());
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const response = await this.get<{ members: { address: string; power: string }[] }>(
|
||||||
|
`/daos/${encodeURIComponent(daoId)}/members${query}`
|
||||||
|
);
|
||||||
|
return response.members ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Vesting Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a vesting schedule.
|
||||||
|
*/
|
||||||
|
async createVestingSchedule(schedule: VestingSchedule): Promise<VestingContract> {
|
||||||
|
return await this.post<VestingContract>('/vesting', schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a vesting contract.
|
||||||
|
*/
|
||||||
|
async getVestingContract(contractId: string): Promise<VestingContract> {
|
||||||
|
return await this.get<VestingContract>(`/vesting/${encodeURIComponent(contractId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List vesting contracts.
|
||||||
|
*/
|
||||||
|
async listVestingContracts(beneficiary?: string): Promise<VestingContract[]> {
|
||||||
|
const query = beneficiary ? `?beneficiary=${encodeURIComponent(beneficiary)}` : '';
|
||||||
|
const response = await this.get<VestingContractsResponse>(`/vesting${query}`);
|
||||||
|
return response.contracts ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim vested tokens.
|
||||||
|
*/
|
||||||
|
async claimVested(contractId: string): Promise<ClaimResult> {
|
||||||
|
return await this.post<ClaimResult>(
|
||||||
|
`/vesting/${encodeURIComponent(contractId)}/claim`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a vesting contract (if revocable).
|
||||||
|
*/
|
||||||
|
async revokeVesting(contractId: string): Promise<VestingContract> {
|
||||||
|
return await this.post<VestingContract>(
|
||||||
|
`/vesting/${encodeURIComponent(contractId)}/revoke`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get claimable amount for a vesting contract.
|
||||||
|
*/
|
||||||
|
async getClaimableAmount(contractId: string): Promise<{ claimable: string; vested: string }> {
|
||||||
|
return await this.get<{ claimable: string; vested: string }>(
|
||||||
|
`/vesting/${encodeURIComponent(contractId)}/claimable`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check.
|
||||||
|
*/
|
||||||
|
async healthCheck(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.get<{ status: string }>('/health');
|
||||||
|
return response.status === 'healthy';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if client is closed.
|
||||||
|
*/
|
||||||
|
get isClosed(): boolean {
|
||||||
|
return this.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the client.
|
||||||
|
*/
|
||||||
|
close(): void {
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Private HTTP Methods ====================
|
||||||
|
|
||||||
|
private async get<T>(path: string): Promise<T> {
|
||||||
|
return await this.execute(async () => {
|
||||||
|
const response = await fetch(`${this.config.endpoint}${path}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.headers(),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
await this.ensureSuccess(response);
|
||||||
|
return await response.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async post<T>(path: string, body: unknown): Promise<T> {
|
||||||
|
return await this.execute(async () => {
|
||||||
|
const response = await fetch(`${this.config.endpoint}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers(),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
await this.ensureSuccess(response);
|
||||||
|
return await response.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private headers(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${this.config.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-SDK-Version': 'js/0.1.0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async execute<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
|
if (this.closed) {
|
||||||
|
throw new GovernanceError('Client has been closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastError: Error | undefined;
|
||||||
|
for (let attempt = 0; attempt < this.config.retries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error;
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log(`Attempt ${attempt + 1} failed:`, error);
|
||||||
|
}
|
||||||
|
if (attempt < this.config.retries - 1) {
|
||||||
|
await this.sleep(1000 * Math.pow(2, attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureSuccess(response: Response): Promise<void> {
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
let message = `HTTP ${response.status}`;
|
||||||
|
let code: string | undefined;
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(body);
|
||||||
|
message = error.message ?? message;
|
||||||
|
code = error.code;
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
throw new GovernanceError(message, code, response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Governance SDK Error
|
||||||
|
*/
|
||||||
|
export class GovernanceError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly code?: string,
|
||||||
|
public readonly statusCode?: number
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'GovernanceError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export * from './types';
|
||||||
7
sdk/js/src/governance/index.ts
Normal file
7
sdk/js/src/governance/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Synor Governance SDK
|
||||||
|
* Proposals, voting, DAOs, and vesting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SynorGovernance, GovernanceError } from './client';
|
||||||
|
export * from './types';
|
||||||
222
sdk/js/src/governance/types.ts
Normal file
222
sdk/js/src/governance/types.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
/**
|
||||||
|
* Synor Governance SDK Types
|
||||||
|
* Proposals, voting, DAOs, and vesting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Proposal status
|
||||||
|
export type ProposalStatus =
|
||||||
|
| 'draft'
|
||||||
|
| 'active'
|
||||||
|
| 'passed'
|
||||||
|
| 'rejected'
|
||||||
|
| 'executed'
|
||||||
|
| 'cancelled'
|
||||||
|
| 'expired';
|
||||||
|
|
||||||
|
// Vote choice
|
||||||
|
export type VoteChoice = 'for' | 'against' | 'abstain';
|
||||||
|
|
||||||
|
// Proposal type
|
||||||
|
export type ProposalType =
|
||||||
|
| 'parameter_change'
|
||||||
|
| 'treasury_spend'
|
||||||
|
| 'upgrade'
|
||||||
|
| 'text'
|
||||||
|
| 'custom';
|
||||||
|
|
||||||
|
// DAO type
|
||||||
|
export type DaoType = 'token' | 'multisig' | 'hybrid';
|
||||||
|
|
||||||
|
// Vesting status
|
||||||
|
export type VestingStatus = 'active' | 'paused' | 'completed' | 'cancelled';
|
||||||
|
|
||||||
|
// Proposal draft
|
||||||
|
export interface ProposalDraft {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
type: ProposalType;
|
||||||
|
actions?: ProposalAction[];
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
quorum?: string;
|
||||||
|
threshold?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proposal action
|
||||||
|
export interface ProposalAction {
|
||||||
|
target: string;
|
||||||
|
value: string;
|
||||||
|
calldata: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proposal
|
||||||
|
export interface Proposal {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
type: ProposalType;
|
||||||
|
status: ProposalStatus;
|
||||||
|
proposer: string;
|
||||||
|
actions: ProposalAction[];
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
quorum: string;
|
||||||
|
threshold: string;
|
||||||
|
forVotes: string;
|
||||||
|
againstVotes: string;
|
||||||
|
abstainVotes: string;
|
||||||
|
totalVotes: string;
|
||||||
|
createdAt: number;
|
||||||
|
executedAt?: number;
|
||||||
|
executionTxHash?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proposal filter
|
||||||
|
export interface ProposalFilter {
|
||||||
|
status?: ProposalStatus;
|
||||||
|
type?: ProposalType;
|
||||||
|
proposer?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vote
|
||||||
|
export interface Vote {
|
||||||
|
choice: VoteChoice;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vote receipt
|
||||||
|
export interface VoteReceipt {
|
||||||
|
proposalId: string;
|
||||||
|
voter: string;
|
||||||
|
choice: VoteChoice;
|
||||||
|
weight: string;
|
||||||
|
reason?: string;
|
||||||
|
timestamp: number;
|
||||||
|
txHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegation receipt
|
||||||
|
export interface DelegationReceipt {
|
||||||
|
delegator: string;
|
||||||
|
delegatee: string;
|
||||||
|
amount: string;
|
||||||
|
timestamp: number;
|
||||||
|
txHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Voting power
|
||||||
|
export interface VotingPower {
|
||||||
|
address: string;
|
||||||
|
ownPower: string;
|
||||||
|
delegatedPower: string;
|
||||||
|
totalPower: string;
|
||||||
|
delegatedFrom: DelegationInfo[];
|
||||||
|
delegatedTo?: DelegationInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegation info
|
||||||
|
export interface DelegationInfo {
|
||||||
|
address: string;
|
||||||
|
amount: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DAO config
|
||||||
|
export interface DaoConfig {
|
||||||
|
name: string;
|
||||||
|
type: DaoType;
|
||||||
|
tokenAddress?: string;
|
||||||
|
signers?: string[];
|
||||||
|
threshold?: number;
|
||||||
|
votingPeriod: number;
|
||||||
|
quorum: string;
|
||||||
|
proposalThreshold: string;
|
||||||
|
timelockDelay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DAO
|
||||||
|
export interface Dao {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: DaoType;
|
||||||
|
tokenAddress?: string;
|
||||||
|
signers?: string[];
|
||||||
|
threshold?: number;
|
||||||
|
votingPeriod: number;
|
||||||
|
quorum: string;
|
||||||
|
proposalThreshold: string;
|
||||||
|
timelockDelay?: number;
|
||||||
|
treasury: string;
|
||||||
|
totalProposals: number;
|
||||||
|
activeProposals: number;
|
||||||
|
totalMembers: number;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vesting schedule
|
||||||
|
export interface VestingSchedule {
|
||||||
|
beneficiary: string;
|
||||||
|
totalAmount: string;
|
||||||
|
startTime: number;
|
||||||
|
cliffDuration: number;
|
||||||
|
vestingDuration: number;
|
||||||
|
revocable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vesting contract
|
||||||
|
export interface VestingContract {
|
||||||
|
id: string;
|
||||||
|
beneficiary: string;
|
||||||
|
totalAmount: string;
|
||||||
|
releasedAmount: string;
|
||||||
|
vestedAmount: string;
|
||||||
|
startTime: number;
|
||||||
|
cliffTime: number;
|
||||||
|
endTime: number;
|
||||||
|
revocable: boolean;
|
||||||
|
status: VestingStatus;
|
||||||
|
createdAt: number;
|
||||||
|
txHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim result
|
||||||
|
export interface ClaimResult {
|
||||||
|
vestingId: string;
|
||||||
|
amount: string;
|
||||||
|
recipient: string;
|
||||||
|
timestamp: number;
|
||||||
|
txHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction result
|
||||||
|
export interface GovernanceTransaction {
|
||||||
|
txHash: string;
|
||||||
|
timestamp: number;
|
||||||
|
blockNumber: number;
|
||||||
|
status: 'pending' | 'confirmed' | 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Governance config
|
||||||
|
export interface GovernanceConfig {
|
||||||
|
apiKey: string;
|
||||||
|
endpoint?: string;
|
||||||
|
timeout?: number;
|
||||||
|
retries?: number;
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal response types
|
||||||
|
export interface ProposalsResponse {
|
||||||
|
proposals: Proposal[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DaosResponse {
|
||||||
|
daos: Dao[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VestingContractsResponse {
|
||||||
|
contracts: VestingContract[];
|
||||||
|
}
|
||||||
409
sdk/js/src/mining/client.ts
Normal file
409
sdk/js/src/mining/client.ts
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
/**
|
||||||
|
* Synor Mining SDK Client
|
||||||
|
* Pool connections, block templates, hashrate stats, and GPU management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
MiningConfig,
|
||||||
|
PoolConfig,
|
||||||
|
StratumConnection,
|
||||||
|
BlockTemplate,
|
||||||
|
MinedWork,
|
||||||
|
SubmitResult,
|
||||||
|
Hashrate,
|
||||||
|
MiningStats,
|
||||||
|
TimePeriod,
|
||||||
|
Earnings,
|
||||||
|
MiningDevice,
|
||||||
|
DeviceConfig,
|
||||||
|
WorkerInfo,
|
||||||
|
PoolStats,
|
||||||
|
MiningAlgorithm,
|
||||||
|
DevicesResponse,
|
||||||
|
WorkersResponse,
|
||||||
|
AlgorithmsResponse,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT = 'https://mining.synor.io/v1';
|
||||||
|
const DEFAULT_TIMEOUT = 30000;
|
||||||
|
const DEFAULT_RETRIES = 3;
|
||||||
|
|
||||||
|
export class SynorMining {
|
||||||
|
private config: Required<MiningConfig>;
|
||||||
|
private closed = false;
|
||||||
|
private activeConnection?: StratumConnection;
|
||||||
|
|
||||||
|
constructor(config: MiningConfig) {
|
||||||
|
this.config = {
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
||||||
|
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
||||||
|
retries: config.retries ?? DEFAULT_RETRIES,
|
||||||
|
debug: config.debug ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Pool Connection ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to a mining pool.
|
||||||
|
*/
|
||||||
|
async connect(pool: PoolConfig): Promise<StratumConnection> {
|
||||||
|
const connection = await this.post<StratumConnection>('/pool/connect', pool);
|
||||||
|
this.activeConnection = connection;
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the current pool.
|
||||||
|
*/
|
||||||
|
async disconnect(): Promise<void> {
|
||||||
|
if (this.activeConnection) {
|
||||||
|
await this.post<void>(`/pool/disconnect/${this.activeConnection.id}`, {});
|
||||||
|
this.activeConnection = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current connection status.
|
||||||
|
*/
|
||||||
|
async getConnectionStatus(): Promise<StratumConnection | null> {
|
||||||
|
if (!this.activeConnection) return null;
|
||||||
|
try {
|
||||||
|
return await this.get<StratumConnection>(`/pool/status/${this.activeConnection.id}`);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconnect to the pool.
|
||||||
|
*/
|
||||||
|
async reconnect(): Promise<StratumConnection> {
|
||||||
|
if (!this.activeConnection) {
|
||||||
|
throw new MiningError('No active connection to reconnect');
|
||||||
|
}
|
||||||
|
return await this.post<StratumConnection>(`/pool/reconnect/${this.activeConnection.id}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Mining Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current block template.
|
||||||
|
*/
|
||||||
|
async getBlockTemplate(): Promise<BlockTemplate> {
|
||||||
|
return await this.get<BlockTemplate>('/mining/template');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit mined work.
|
||||||
|
*/
|
||||||
|
async submitWork(work: MinedWork): Promise<SubmitResult> {
|
||||||
|
return await this.post<SubmitResult>('/mining/submit', work);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get work from pool (stratum getwork).
|
||||||
|
*/
|
||||||
|
async getWork(): Promise<{ work: string; target: string; algorithm: string }> {
|
||||||
|
return await this.get<{ work: string; target: string; algorithm: string }>('/mining/getwork');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start mining on all enabled devices.
|
||||||
|
*/
|
||||||
|
async startMining(algorithm?: string): Promise<{ started: boolean; devices: string[] }> {
|
||||||
|
const body = algorithm ? { algorithm } : {};
|
||||||
|
return await this.post<{ started: boolean; devices: string[] }>('/mining/start', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop mining on all devices.
|
||||||
|
*/
|
||||||
|
async stopMining(): Promise<{ stopped: boolean }> {
|
||||||
|
return await this.post<{ stopped: boolean }>('/mining/stop', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Stats ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current hashrate.
|
||||||
|
*/
|
||||||
|
async getHashrate(): Promise<Hashrate> {
|
||||||
|
return await this.get<Hashrate>('/stats/hashrate');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mining stats.
|
||||||
|
*/
|
||||||
|
async getStats(): Promise<MiningStats> {
|
||||||
|
return await this.get<MiningStats>('/stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get earnings for a time period.
|
||||||
|
*/
|
||||||
|
async getEarnings(period?: TimePeriod): Promise<Earnings> {
|
||||||
|
const query = period ? `?period=${period}` : '';
|
||||||
|
return await this.get<Earnings>(`/stats/earnings${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get earnings history.
|
||||||
|
*/
|
||||||
|
async getEarningsHistory(limit?: number, offset?: number): Promise<Earnings[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (limit) params.set('limit', limit.toString());
|
||||||
|
if (offset) params.set('offset', offset.toString());
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
const response = await this.get<{ earnings: Earnings[] }>(`/stats/earnings/history${query}`);
|
||||||
|
return response.earnings ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pool stats.
|
||||||
|
*/
|
||||||
|
async getPoolStats(): Promise<PoolStats> {
|
||||||
|
return await this.get<PoolStats>('/pool/stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GPU Management ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all mining devices.
|
||||||
|
*/
|
||||||
|
async listDevices(): Promise<MiningDevice[]> {
|
||||||
|
const response = await this.get<DevicesResponse>('/devices');
|
||||||
|
return response.devices ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device details.
|
||||||
|
*/
|
||||||
|
async getDevice(deviceId: string): Promise<MiningDevice> {
|
||||||
|
return await this.get<MiningDevice>(`/devices/${encodeURIComponent(deviceId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set device configuration.
|
||||||
|
*/
|
||||||
|
async setDeviceConfig(deviceId: string, config: DeviceConfig): Promise<MiningDevice> {
|
||||||
|
return await this.post<MiningDevice>(
|
||||||
|
`/devices/${encodeURIComponent(deviceId)}/config`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable a device for mining.
|
||||||
|
*/
|
||||||
|
async enableDevice(deviceId: string): Promise<MiningDevice> {
|
||||||
|
return await this.post<MiningDevice>(`/devices/${encodeURIComponent(deviceId)}/enable`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable a device.
|
||||||
|
*/
|
||||||
|
async disableDevice(deviceId: string): Promise<MiningDevice> {
|
||||||
|
return await this.post<MiningDevice>(`/devices/${encodeURIComponent(deviceId)}/disable`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset device to default settings.
|
||||||
|
*/
|
||||||
|
async resetDevice(deviceId: string): Promise<MiningDevice> {
|
||||||
|
return await this.post<MiningDevice>(`/devices/${encodeURIComponent(deviceId)}/reset`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Workers ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all workers.
|
||||||
|
*/
|
||||||
|
async listWorkers(): Promise<WorkerInfo[]> {
|
||||||
|
const response = await this.get<WorkersResponse>('/workers');
|
||||||
|
return response.workers ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get worker details.
|
||||||
|
*/
|
||||||
|
async getWorker(workerId: string): Promise<WorkerInfo> {
|
||||||
|
return await this.get<WorkerInfo>(`/workers/${encodeURIComponent(workerId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new worker.
|
||||||
|
*/
|
||||||
|
async createWorker(name: string): Promise<WorkerInfo> {
|
||||||
|
return await this.post<WorkerInfo>('/workers', { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a worker.
|
||||||
|
*/
|
||||||
|
async deleteWorker(workerId: string): Promise<void> {
|
||||||
|
await this.delete(`/workers/${encodeURIComponent(workerId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Algorithms ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get supported mining algorithms.
|
||||||
|
*/
|
||||||
|
async getSupportedAlgorithms(): Promise<MiningAlgorithm[]> {
|
||||||
|
const response = await this.get<AlgorithmsResponse>('/algorithms');
|
||||||
|
return response.algorithms ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current algorithm.
|
||||||
|
*/
|
||||||
|
async getCurrentAlgorithm(): Promise<MiningAlgorithm> {
|
||||||
|
return await this.get<MiningAlgorithm>('/algorithms/current');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different algorithm.
|
||||||
|
*/
|
||||||
|
async switchAlgorithm(algorithm: string): Promise<{ switched: boolean }> {
|
||||||
|
return await this.post<{ switched: boolean }>('/algorithms/switch', { algorithm });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check.
|
||||||
|
*/
|
||||||
|
async healthCheck(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.get<{ status: string }>('/health');
|
||||||
|
return response.status === 'healthy';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if client is closed.
|
||||||
|
*/
|
||||||
|
get isClosed(): boolean {
|
||||||
|
return this.closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the client.
|
||||||
|
*/
|
||||||
|
async close(): Promise<void> {
|
||||||
|
if (this.activeConnection) {
|
||||||
|
await this.disconnect();
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Private HTTP Methods ====================
|
||||||
|
|
||||||
|
private async get<T>(path: string): Promise<T> {
|
||||||
|
return await this.execute(async () => {
|
||||||
|
const response = await fetch(`${this.config.endpoint}${path}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.headers(),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
await this.ensureSuccess(response);
|
||||||
|
return await response.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async post<T>(path: string, body: unknown): Promise<T> {
|
||||||
|
return await this.execute(async () => {
|
||||||
|
const response = await fetch(`${this.config.endpoint}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers(),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
await this.ensureSuccess(response);
|
||||||
|
const text = await response.text();
|
||||||
|
return text ? JSON.parse(text) : undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async delete(path: string): Promise<void> {
|
||||||
|
return await this.execute(async () => {
|
||||||
|
const response = await fetch(`${this.config.endpoint}${path}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.headers(),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
await this.ensureSuccess(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private headers(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${this.config.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-SDK-Version': 'js/0.1.0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async execute<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
|
if (this.closed) {
|
||||||
|
throw new MiningError('Client has been closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastError: Error | undefined;
|
||||||
|
for (let attempt = 0; attempt < this.config.retries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error;
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log(`Attempt ${attempt + 1} failed:`, error);
|
||||||
|
}
|
||||||
|
if (attempt < this.config.retries - 1) {
|
||||||
|
await this.sleep(1000 * Math.pow(2, attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureSuccess(response: Response): Promise<void> {
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text();
|
||||||
|
let message = `HTTP ${response.status}`;
|
||||||
|
let code: string | undefined;
|
||||||
|
try {
|
||||||
|
const error = JSON.parse(body);
|
||||||
|
message = error.message ?? message;
|
||||||
|
code = error.code;
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
throw new MiningError(message, code, response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mining SDK Error
|
||||||
|
*/
|
||||||
|
export class MiningError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly code?: string,
|
||||||
|
public readonly statusCode?: number
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'MiningError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export * from './types';
|
||||||
7
sdk/js/src/mining/index.ts
Normal file
7
sdk/js/src/mining/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Synor Mining SDK
|
||||||
|
* Pool connections, block templates, hashrate stats, and GPU management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SynorMining, MiningError } from './client';
|
||||||
|
export * from './types';
|
||||||
242
sdk/js/src/mining/types.ts
Normal file
242
sdk/js/src/mining/types.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
/**
|
||||||
|
* Synor Mining SDK Types
|
||||||
|
* Pool connections, block templates, hashrate stats, and GPU management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Device type
|
||||||
|
export type DeviceType = 'cpu' | 'gpu_nvidia' | 'gpu_amd' | 'asic';
|
||||||
|
|
||||||
|
// Device status
|
||||||
|
export type DeviceStatus = 'idle' | 'mining' | 'error' | 'offline';
|
||||||
|
|
||||||
|
// Connection status
|
||||||
|
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
|
||||||
|
|
||||||
|
// Time period for stats
|
||||||
|
export type TimePeriod = 'hour' | 'day' | 'week' | 'month' | 'all';
|
||||||
|
|
||||||
|
// Submit result status
|
||||||
|
export type SubmitResultStatus = 'accepted' | 'rejected' | 'stale';
|
||||||
|
|
||||||
|
// Pool configuration
|
||||||
|
export interface PoolConfig {
|
||||||
|
url: string;
|
||||||
|
user: string;
|
||||||
|
password?: string;
|
||||||
|
algorithm?: string;
|
||||||
|
difficulty?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stratum connection
|
||||||
|
export interface StratumConnection {
|
||||||
|
id: string;
|
||||||
|
pool: string;
|
||||||
|
status: ConnectionStatus;
|
||||||
|
algorithm: string;
|
||||||
|
difficulty: number;
|
||||||
|
connectedAt: number;
|
||||||
|
lastShareAt?: number;
|
||||||
|
acceptedShares: number;
|
||||||
|
rejectedShares: number;
|
||||||
|
staleShares: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block template
|
||||||
|
export interface BlockTemplate {
|
||||||
|
id: string;
|
||||||
|
previousBlockHash: string;
|
||||||
|
merkleRoot: string;
|
||||||
|
timestamp: number;
|
||||||
|
bits: string;
|
||||||
|
height: number;
|
||||||
|
coinbaseValue: string;
|
||||||
|
transactions: TemplateTransaction[];
|
||||||
|
target: string;
|
||||||
|
algorithm: string;
|
||||||
|
extraNonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template transaction
|
||||||
|
export interface TemplateTransaction {
|
||||||
|
txid: string;
|
||||||
|
data: string;
|
||||||
|
fee: string;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mined work
|
||||||
|
export interface MinedWork {
|
||||||
|
templateId: string;
|
||||||
|
nonce: string;
|
||||||
|
extraNonce: string;
|
||||||
|
timestamp: number;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit result
|
||||||
|
export interface SubmitResult {
|
||||||
|
status: SubmitResultStatus;
|
||||||
|
reason?: string;
|
||||||
|
share: ShareInfo;
|
||||||
|
blockFound: boolean;
|
||||||
|
blockHash?: string;
|
||||||
|
reward?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share info
|
||||||
|
export interface ShareInfo {
|
||||||
|
hash: string;
|
||||||
|
difficulty: number;
|
||||||
|
timestamp: number;
|
||||||
|
accepted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hashrate
|
||||||
|
export interface Hashrate {
|
||||||
|
current: number;
|
||||||
|
average1h: number;
|
||||||
|
average24h: number;
|
||||||
|
peak: number;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mining stats
|
||||||
|
export interface MiningStats {
|
||||||
|
hashrate: Hashrate;
|
||||||
|
shares: ShareStats;
|
||||||
|
uptime: number;
|
||||||
|
efficiency: number;
|
||||||
|
powerConsumption?: number;
|
||||||
|
temperature?: DeviceTemperature;
|
||||||
|
earnings: EarningsSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share stats
|
||||||
|
export interface ShareStats {
|
||||||
|
accepted: number;
|
||||||
|
rejected: number;
|
||||||
|
stale: number;
|
||||||
|
total: number;
|
||||||
|
acceptRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device temperature
|
||||||
|
export interface DeviceTemperature {
|
||||||
|
current: number;
|
||||||
|
max: number;
|
||||||
|
throttling: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Earnings snapshot
|
||||||
|
export interface EarningsSnapshot {
|
||||||
|
today: string;
|
||||||
|
yesterday: string;
|
||||||
|
thisWeek: string;
|
||||||
|
thisMonth: string;
|
||||||
|
total: string;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed earnings
|
||||||
|
export interface Earnings {
|
||||||
|
period: TimePeriod;
|
||||||
|
startDate: number;
|
||||||
|
endDate: number;
|
||||||
|
amount: string;
|
||||||
|
blocks: number;
|
||||||
|
shares: number;
|
||||||
|
averageHashrate: number;
|
||||||
|
currency: string;
|
||||||
|
breakdown: EarningsBreakdown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Earnings breakdown
|
||||||
|
export interface EarningsBreakdown {
|
||||||
|
date: number;
|
||||||
|
amount: string;
|
||||||
|
blocks: number;
|
||||||
|
shares: number;
|
||||||
|
hashrate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mining device
|
||||||
|
export interface MiningDevice {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: DeviceType;
|
||||||
|
status: DeviceStatus;
|
||||||
|
hashrate: number;
|
||||||
|
temperature: number;
|
||||||
|
fanSpeed: number;
|
||||||
|
powerDraw: number;
|
||||||
|
memoryUsed: number;
|
||||||
|
memoryTotal: number;
|
||||||
|
driver?: string;
|
||||||
|
firmware?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device config
|
||||||
|
export interface DeviceConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
intensity?: number;
|
||||||
|
powerLimit?: number;
|
||||||
|
coreClockOffset?: number;
|
||||||
|
memoryClockOffset?: number;
|
||||||
|
fanSpeed?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker info
|
||||||
|
export interface WorkerInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: ConnectionStatus;
|
||||||
|
hashrate: Hashrate;
|
||||||
|
shares: ShareStats;
|
||||||
|
devices: MiningDevice[];
|
||||||
|
lastSeen: number;
|
||||||
|
uptime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool stats
|
||||||
|
export interface PoolStats {
|
||||||
|
url: string;
|
||||||
|
workers: number;
|
||||||
|
hashrate: number;
|
||||||
|
difficulty: number;
|
||||||
|
lastBlock: number;
|
||||||
|
blocksFound24h: number;
|
||||||
|
luck: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mining algorithm
|
||||||
|
export interface MiningAlgorithm {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
hashUnit: string;
|
||||||
|
profitability: string;
|
||||||
|
difficulty: number;
|
||||||
|
blockReward: string;
|
||||||
|
blockTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mining config
|
||||||
|
export interface MiningConfig {
|
||||||
|
apiKey: string;
|
||||||
|
endpoint?: string;
|
||||||
|
timeout?: number;
|
||||||
|
retries?: number;
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal response types
|
||||||
|
export interface DevicesResponse {
|
||||||
|
devices: MiningDevice[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkersResponse {
|
||||||
|
workers: WorkerInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlgorithmsResponse {
|
||||||
|
algorithms: MiningAlgorithm[];
|
||||||
|
}
|
||||||
66
sdk/python/src/synor_economics/__init__.py
Normal file
66
sdk/python/src/synor_economics/__init__.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
"""
|
||||||
|
Synor Economics SDK for Python.
|
||||||
|
Pricing, billing, staking, and discount management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import SynorEconomics, EconomicsError
|
||||||
|
from .types import (
|
||||||
|
ServiceType,
|
||||||
|
BillingPeriod,
|
||||||
|
StakeStatus,
|
||||||
|
DiscountType,
|
||||||
|
InvoiceStatus,
|
||||||
|
UsageMetrics,
|
||||||
|
UsagePlan,
|
||||||
|
ServiceUsage,
|
||||||
|
Price,
|
||||||
|
CostEstimate,
|
||||||
|
TaxAmount,
|
||||||
|
AppliedDiscount,
|
||||||
|
Usage,
|
||||||
|
ServiceUsageRecord,
|
||||||
|
UsageDetail,
|
||||||
|
Invoice,
|
||||||
|
InvoiceItem,
|
||||||
|
AccountBalance,
|
||||||
|
StakeReceipt,
|
||||||
|
UnstakeReceipt,
|
||||||
|
StakeInfo,
|
||||||
|
StakingRewards,
|
||||||
|
StakeRewardDetail,
|
||||||
|
Discount,
|
||||||
|
StakeOptions,
|
||||||
|
EconomicsConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__all__ = [
|
||||||
|
"SynorEconomics",
|
||||||
|
"EconomicsError",
|
||||||
|
"ServiceType",
|
||||||
|
"BillingPeriod",
|
||||||
|
"StakeStatus",
|
||||||
|
"DiscountType",
|
||||||
|
"InvoiceStatus",
|
||||||
|
"UsageMetrics",
|
||||||
|
"UsagePlan",
|
||||||
|
"ServiceUsage",
|
||||||
|
"Price",
|
||||||
|
"CostEstimate",
|
||||||
|
"TaxAmount",
|
||||||
|
"AppliedDiscount",
|
||||||
|
"Usage",
|
||||||
|
"ServiceUsageRecord",
|
||||||
|
"UsageDetail",
|
||||||
|
"Invoice",
|
||||||
|
"InvoiceItem",
|
||||||
|
"AccountBalance",
|
||||||
|
"StakeReceipt",
|
||||||
|
"UnstakeReceipt",
|
||||||
|
"StakeInfo",
|
||||||
|
"StakingRewards",
|
||||||
|
"StakeRewardDetail",
|
||||||
|
"Discount",
|
||||||
|
"StakeOptions",
|
||||||
|
"EconomicsConfig",
|
||||||
|
]
|
||||||
499
sdk/python/src/synor_economics/client.py
Normal file
499
sdk/python/src/synor_economics/client.py
Normal file
|
|
@ -0,0 +1,499 @@
|
||||||
|
"""
|
||||||
|
Synor Economics SDK Client.
|
||||||
|
Pricing, billing, staking, and discount management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from urllib.parse import urlencode, quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .types import (
|
||||||
|
EconomicsConfig,
|
||||||
|
ServiceType,
|
||||||
|
UsageMetrics,
|
||||||
|
Price,
|
||||||
|
UsagePlan,
|
||||||
|
CostEstimate,
|
||||||
|
BillingPeriod,
|
||||||
|
Usage,
|
||||||
|
Invoice,
|
||||||
|
AccountBalance,
|
||||||
|
StakeReceipt,
|
||||||
|
UnstakeReceipt,
|
||||||
|
StakingRewards,
|
||||||
|
StakeInfo,
|
||||||
|
StakeOptions,
|
||||||
|
Discount,
|
||||||
|
AppliedDiscount,
|
||||||
|
TaxAmount,
|
||||||
|
ServiceUsageRecord,
|
||||||
|
UsageDetail,
|
||||||
|
InvoiceItem,
|
||||||
|
InvoiceStatus,
|
||||||
|
StakeStatus,
|
||||||
|
DiscountType,
|
||||||
|
StakeRewardDetail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EconomicsError(Exception):
|
||||||
|
"""Economics SDK Error."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, code: Optional[str] = None, status_code: int = 0):
|
||||||
|
super().__init__(message)
|
||||||
|
self.code = code
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class SynorEconomics:
|
||||||
|
"""
|
||||||
|
Synor Economics SDK client for Python.
|
||||||
|
Pricing, billing, staking, and discount management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: EconomicsConfig):
|
||||||
|
self._config = config
|
||||||
|
self._closed = False
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=config.endpoint,
|
||||||
|
timeout=config.timeout,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {config.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-SDK-Version": "python/0.1.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== Pricing Operations ====================
|
||||||
|
|
||||||
|
async def get_price(self, service: ServiceType, usage: UsageMetrics) -> Price:
|
||||||
|
"""Get price for a service based on usage."""
|
||||||
|
response = await self._post(
|
||||||
|
"/pricing/calculate",
|
||||||
|
{"service": service.value, "usage": self._usage_to_dict(usage)},
|
||||||
|
)
|
||||||
|
return self._parse_price(response["price"])
|
||||||
|
|
||||||
|
async def estimate_cost(self, plan: UsagePlan) -> CostEstimate:
|
||||||
|
"""Estimate cost for a usage plan."""
|
||||||
|
response = await self._post("/pricing/estimate", self._plan_to_dict(plan))
|
||||||
|
return self._parse_cost_estimate(response)
|
||||||
|
|
||||||
|
async def get_pricing_tiers(self, service: ServiceType) -> List[Price]:
|
||||||
|
"""Get pricing tiers for a service."""
|
||||||
|
response = await self._get(f"/pricing/{service.value}/tiers")
|
||||||
|
return [self._parse_price(p) for p in response.get("tiers", [])]
|
||||||
|
|
||||||
|
# ==================== Usage & Billing Operations ====================
|
||||||
|
|
||||||
|
async def get_usage(self, period: Optional[BillingPeriod] = None) -> Usage:
|
||||||
|
"""Get usage for a billing period."""
|
||||||
|
params = f"?period={period.value}" if period else ""
|
||||||
|
response = await self._get(f"/usage{params}")
|
||||||
|
return self._parse_usage(response)
|
||||||
|
|
||||||
|
async def get_usage_history(
|
||||||
|
self, limit: Optional[int] = None, offset: Optional[int] = None
|
||||||
|
) -> List[Usage]:
|
||||||
|
"""Get usage history."""
|
||||||
|
params = {}
|
||||||
|
if limit:
|
||||||
|
params["limit"] = str(limit)
|
||||||
|
if offset:
|
||||||
|
params["offset"] = str(offset)
|
||||||
|
query = f"?{urlencode(params)}" if params else ""
|
||||||
|
response = await self._get(f"/usage/history{query}")
|
||||||
|
return [self._parse_usage(u) for u in response.get("usage", [])]
|
||||||
|
|
||||||
|
async def get_invoices(self) -> List[Invoice]:
|
||||||
|
"""Get invoices."""
|
||||||
|
response = await self._get("/invoices")
|
||||||
|
return [self._parse_invoice(i) for i in response.get("invoices", [])]
|
||||||
|
|
||||||
|
async def get_invoice(self, invoice_id: str) -> Invoice:
|
||||||
|
"""Get a specific invoice."""
|
||||||
|
response = await self._get(f"/invoices/{quote(invoice_id)}")
|
||||||
|
return self._parse_invoice(response)
|
||||||
|
|
||||||
|
async def pay_invoice(
|
||||||
|
self, invoice_id: str, payment_method: Optional[str] = None
|
||||||
|
) -> Invoice:
|
||||||
|
"""Pay an invoice."""
|
||||||
|
response = await self._post(
|
||||||
|
f"/invoices/{quote(invoice_id)}/pay",
|
||||||
|
{"payment_method": payment_method} if payment_method else {},
|
||||||
|
)
|
||||||
|
return self._parse_invoice(response)
|
||||||
|
|
||||||
|
async def get_balance(self) -> AccountBalance:
|
||||||
|
"""Get account balance."""
|
||||||
|
response = await self._get("/balance")
|
||||||
|
return self._parse_balance(response)
|
||||||
|
|
||||||
|
async def add_funds(self, amount: str, payment_method: str) -> AccountBalance:
|
||||||
|
"""Add funds to account."""
|
||||||
|
response = await self._post(
|
||||||
|
"/balance/deposit", {"amount": amount, "payment_method": payment_method}
|
||||||
|
)
|
||||||
|
return self._parse_balance(response)
|
||||||
|
|
||||||
|
# ==================== Staking Operations ====================
|
||||||
|
|
||||||
|
async def stake(
|
||||||
|
self, amount: str, options: Optional[StakeOptions] = None
|
||||||
|
) -> StakeReceipt:
|
||||||
|
"""Stake tokens."""
|
||||||
|
body: Dict[str, Any] = {"amount": amount}
|
||||||
|
if options:
|
||||||
|
if options.validator:
|
||||||
|
body["validator"] = options.validator
|
||||||
|
if options.auto_compound is not None:
|
||||||
|
body["auto_compound"] = options.auto_compound
|
||||||
|
if options.lock_duration:
|
||||||
|
body["lock_duration"] = options.lock_duration
|
||||||
|
response = await self._post("/staking/stake", body)
|
||||||
|
return self._parse_stake_receipt(response)
|
||||||
|
|
||||||
|
async def unstake(self, stake_id: str) -> UnstakeReceipt:
|
||||||
|
"""Unstake tokens."""
|
||||||
|
response = await self._post(f"/staking/stakes/{quote(stake_id)}/unstake", {})
|
||||||
|
return self._parse_unstake_receipt(response)
|
||||||
|
|
||||||
|
async def get_staking_rewards(self) -> StakingRewards:
|
||||||
|
"""Get staking rewards."""
|
||||||
|
response = await self._get("/staking/rewards")
|
||||||
|
return self._parse_staking_rewards(response)
|
||||||
|
|
||||||
|
async def claim_rewards(self, stake_id: Optional[str] = None) -> StakingRewards:
|
||||||
|
"""Claim staking rewards."""
|
||||||
|
path = (
|
||||||
|
f"/staking/stakes/{quote(stake_id)}/claim"
|
||||||
|
if stake_id
|
||||||
|
else "/staking/rewards/claim"
|
||||||
|
)
|
||||||
|
response = await self._post(path, {})
|
||||||
|
return self._parse_staking_rewards(response)
|
||||||
|
|
||||||
|
async def list_stakes(self) -> List[StakeInfo]:
|
||||||
|
"""List active stakes."""
|
||||||
|
response = await self._get("/staking/stakes")
|
||||||
|
return [self._parse_stake_info(s) for s in response.get("stakes", [])]
|
||||||
|
|
||||||
|
async def get_stake(self, stake_id: str) -> StakeInfo:
|
||||||
|
"""Get stake details."""
|
||||||
|
response = await self._get(f"/staking/stakes/{quote(stake_id)}")
|
||||||
|
return self._parse_stake_info(response)
|
||||||
|
|
||||||
|
async def get_staking_apy(self) -> Dict[str, Any]:
|
||||||
|
"""Get current APY for staking."""
|
||||||
|
return await self._get("/staking/apy")
|
||||||
|
|
||||||
|
# ==================== Discount Operations ====================
|
||||||
|
|
||||||
|
async def apply_discount(self, code: str) -> Discount:
|
||||||
|
"""Apply a discount code."""
|
||||||
|
response = await self._post("/discounts/apply", {"code": code})
|
||||||
|
return self._parse_discount(response)
|
||||||
|
|
||||||
|
async def get_available_discounts(self) -> List[Discount]:
|
||||||
|
"""Get available discounts."""
|
||||||
|
response = await self._get("/discounts")
|
||||||
|
return [self._parse_discount(d) for d in response.get("discounts", [])]
|
||||||
|
|
||||||
|
async def get_active_discounts(self) -> List[Discount]:
|
||||||
|
"""Get active discounts on account."""
|
||||||
|
response = await self._get("/discounts/active")
|
||||||
|
return [self._parse_discount(d) for d in response.get("discounts", [])]
|
||||||
|
|
||||||
|
async def remove_discount(self, code: str) -> None:
|
||||||
|
"""Remove a discount."""
|
||||||
|
await self._delete(f"/discounts/{quote(code)}")
|
||||||
|
|
||||||
|
# ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
async def health_check(self) -> bool:
|
||||||
|
"""Health check."""
|
||||||
|
try:
|
||||||
|
response = await self._get("/health")
|
||||||
|
return response.get("status") == "healthy"
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self) -> bool:
|
||||||
|
"""Check if client is closed."""
|
||||||
|
return self._closed
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""Close the client."""
|
||||||
|
self._closed = True
|
||||||
|
await self._client.aclose()
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "SynorEconomics":
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args) -> None:
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
# ==================== Private Methods ====================
|
||||||
|
|
||||||
|
async def _get(self, path: str) -> Dict[str, Any]:
|
||||||
|
return await self._execute(lambda: self._client.get(path))
|
||||||
|
|
||||||
|
async def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
return await self._execute(lambda: self._client.post(path, json=body))
|
||||||
|
|
||||||
|
async def _delete(self, path: str) -> None:
|
||||||
|
await self._execute(lambda: self._client.delete(path))
|
||||||
|
|
||||||
|
async def _execute(self, operation) -> Dict[str, Any]:
|
||||||
|
if self._closed:
|
||||||
|
raise EconomicsError("Client has been closed")
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for attempt in range(self._config.retries):
|
||||||
|
try:
|
||||||
|
response = await operation()
|
||||||
|
self._ensure_success(response)
|
||||||
|
if response.content:
|
||||||
|
return response.json()
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
if self._config.debug:
|
||||||
|
print(f"Attempt {attempt + 1} failed: {e}")
|
||||||
|
if attempt < self._config.retries - 1:
|
||||||
|
await asyncio.sleep(2**attempt)
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
def _ensure_success(self, response: httpx.Response) -> None:
|
||||||
|
if response.status_code >= 400:
|
||||||
|
message = f"HTTP {response.status_code}"
|
||||||
|
code = None
|
||||||
|
try:
|
||||||
|
error = response.json()
|
||||||
|
message = error.get("message", message)
|
||||||
|
code = error.get("code")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise EconomicsError(message, code, response.status_code)
|
||||||
|
|
||||||
|
# ==================== Parsing Methods ====================
|
||||||
|
|
||||||
|
def _usage_to_dict(self, usage: UsageMetrics) -> Dict[str, Any]:
|
||||||
|
result = {}
|
||||||
|
if usage.compute_hours is not None:
|
||||||
|
result["compute_hours"] = usage.compute_hours
|
||||||
|
if usage.storage_bytes is not None:
|
||||||
|
result["storage_bytes"] = usage.storage_bytes
|
||||||
|
if usage.database_ops is not None:
|
||||||
|
result["database_ops"] = usage.database_ops
|
||||||
|
if usage.hosting_requests is not None:
|
||||||
|
result["hosting_requests"] = usage.hosting_requests
|
||||||
|
if usage.bridge_transfers is not None:
|
||||||
|
result["bridge_transfers"] = usage.bridge_transfers
|
||||||
|
if usage.rpc_calls is not None:
|
||||||
|
result["rpc_calls"] = usage.rpc_calls
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _plan_to_dict(self, plan: UsagePlan) -> Dict[str, Any]:
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"service": s.service.value,
|
||||||
|
"metrics": self._usage_to_dict(s.metrics),
|
||||||
|
"tier": s.tier,
|
||||||
|
}
|
||||||
|
for s in plan.services
|
||||||
|
],
|
||||||
|
"period": plan.period.value,
|
||||||
|
}
|
||||||
|
if plan.start_date:
|
||||||
|
result["start_date"] = plan.start_date
|
||||||
|
if plan.end_date:
|
||||||
|
result["end_date"] = plan.end_date
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _parse_price(self, data: Dict[str, Any]) -> Price:
|
||||||
|
return Price(
|
||||||
|
service=ServiceType(data["service"]),
|
||||||
|
base_price=data["base_price"],
|
||||||
|
quantity=data["quantity"],
|
||||||
|
unit=data["unit"],
|
||||||
|
subtotal=data["subtotal"],
|
||||||
|
discounts=[self._parse_applied_discount(d) for d in data.get("discounts", [])],
|
||||||
|
total=data["total"],
|
||||||
|
currency=data["currency"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_applied_discount(self, data: Dict[str, Any]) -> AppliedDiscount:
|
||||||
|
return AppliedDiscount(
|
||||||
|
code=data["code"],
|
||||||
|
type=DiscountType(data["type"]),
|
||||||
|
value=data["value"],
|
||||||
|
savings=data["savings"],
|
||||||
|
description=data.get("description"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_cost_estimate(self, data: Dict[str, Any]) -> CostEstimate:
|
||||||
|
return CostEstimate(
|
||||||
|
services=[self._parse_price(p) for p in data.get("services", [])],
|
||||||
|
subtotal=data["subtotal"],
|
||||||
|
discounts=[self._parse_applied_discount(d) for d in data.get("discounts", [])],
|
||||||
|
taxes=[
|
||||||
|
TaxAmount(name=t["name"], rate=t["rate"], amount=t["amount"])
|
||||||
|
for t in data.get("taxes", [])
|
||||||
|
],
|
||||||
|
total=data["total"],
|
||||||
|
currency=data["currency"],
|
||||||
|
valid_until=data["valid_until"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_usage(self, data: Dict[str, Any]) -> Usage:
|
||||||
|
return Usage(
|
||||||
|
period=BillingPeriod(data["period"]),
|
||||||
|
start_date=data["start_date"],
|
||||||
|
end_date=data["end_date"],
|
||||||
|
services=[self._parse_service_usage(s) for s in data.get("services", [])],
|
||||||
|
total_cost=data["total_cost"],
|
||||||
|
currency=data["currency"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_service_usage(self, data: Dict[str, Any]) -> ServiceUsageRecord:
|
||||||
|
metrics_data = data.get("metrics", {})
|
||||||
|
return ServiceUsageRecord(
|
||||||
|
service=ServiceType(data["service"]),
|
||||||
|
metrics=UsageMetrics(
|
||||||
|
compute_hours=metrics_data.get("compute_hours"),
|
||||||
|
storage_bytes=metrics_data.get("storage_bytes"),
|
||||||
|
database_ops=metrics_data.get("database_ops"),
|
||||||
|
hosting_requests=metrics_data.get("hosting_requests"),
|
||||||
|
bridge_transfers=metrics_data.get("bridge_transfers"),
|
||||||
|
rpc_calls=metrics_data.get("rpc_calls"),
|
||||||
|
),
|
||||||
|
cost=data["cost"],
|
||||||
|
details=[
|
||||||
|
UsageDetail(
|
||||||
|
timestamp=d["timestamp"],
|
||||||
|
operation=d["operation"],
|
||||||
|
quantity=d["quantity"],
|
||||||
|
unit=d["unit"],
|
||||||
|
cost=d["cost"],
|
||||||
|
)
|
||||||
|
for d in data.get("details", [])
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_invoice(self, data: Dict[str, Any]) -> Invoice:
|
||||||
|
return Invoice(
|
||||||
|
id=data["id"],
|
||||||
|
status=InvoiceStatus(data["status"]),
|
||||||
|
period=BillingPeriod(data["period"]),
|
||||||
|
start_date=data["start_date"],
|
||||||
|
end_date=data["end_date"],
|
||||||
|
items=[
|
||||||
|
InvoiceItem(
|
||||||
|
service=ServiceType(i["service"]),
|
||||||
|
description=i["description"],
|
||||||
|
quantity=i["quantity"],
|
||||||
|
unit_price=i["unit_price"],
|
||||||
|
total=i["total"],
|
||||||
|
)
|
||||||
|
for i in data.get("items", [])
|
||||||
|
],
|
||||||
|
subtotal=data["subtotal"],
|
||||||
|
discounts=[self._parse_applied_discount(d) for d in data.get("discounts", [])],
|
||||||
|
taxes=[
|
||||||
|
TaxAmount(name=t["name"], rate=t["rate"], amount=t["amount"])
|
||||||
|
for t in data.get("taxes", [])
|
||||||
|
],
|
||||||
|
total=data["total"],
|
||||||
|
currency=data["currency"],
|
||||||
|
due_date=data["due_date"],
|
||||||
|
paid_at=data.get("paid_at"),
|
||||||
|
payment_method=data.get("payment_method"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_balance(self, data: Dict[str, Any]) -> AccountBalance:
|
||||||
|
return AccountBalance(
|
||||||
|
available=data["available"],
|
||||||
|
pending=data["pending"],
|
||||||
|
reserved=data["reserved"],
|
||||||
|
total=data["total"],
|
||||||
|
currency=data["currency"],
|
||||||
|
last_updated=data["last_updated"],
|
||||||
|
credit_limit=data.get("credit_limit"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_stake_receipt(self, data: Dict[str, Any]) -> StakeReceipt:
|
||||||
|
return StakeReceipt(
|
||||||
|
id=data["id"],
|
||||||
|
amount=data["amount"],
|
||||||
|
lock_duration=data["lock_duration"],
|
||||||
|
start_date=data["start_date"],
|
||||||
|
end_date=data["end_date"],
|
||||||
|
apy=data["apy"],
|
||||||
|
estimated_rewards=data["estimated_rewards"],
|
||||||
|
tx_hash=data["tx_hash"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_unstake_receipt(self, data: Dict[str, Any]) -> UnstakeReceipt:
|
||||||
|
return UnstakeReceipt(
|
||||||
|
id=data["id"],
|
||||||
|
stake_id=data["stake_id"],
|
||||||
|
amount=data["amount"],
|
||||||
|
rewards=data["rewards"],
|
||||||
|
total=data["total"],
|
||||||
|
unbonding_period=data["unbonding_period"],
|
||||||
|
available_at=data["available_at"],
|
||||||
|
tx_hash=data["tx_hash"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_stake_info(self, data: Dict[str, Any]) -> StakeInfo:
|
||||||
|
return StakeInfo(
|
||||||
|
id=data["id"],
|
||||||
|
amount=data["amount"],
|
||||||
|
status=StakeStatus(data["status"]),
|
||||||
|
lock_duration=data["lock_duration"],
|
||||||
|
start_date=data["start_date"],
|
||||||
|
end_date=data["end_date"],
|
||||||
|
apy=data["apy"],
|
||||||
|
earned_rewards=data["earned_rewards"],
|
||||||
|
pending_rewards=data["pending_rewards"],
|
||||||
|
validator=data.get("validator"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_staking_rewards(self, data: Dict[str, Any]) -> StakingRewards:
|
||||||
|
return StakingRewards(
|
||||||
|
total_earned=data["total_earned"],
|
||||||
|
pending=data["pending"],
|
||||||
|
claimed=data["claimed"],
|
||||||
|
stakes=[
|
||||||
|
StakeRewardDetail(
|
||||||
|
stake_id=s["stake_id"],
|
||||||
|
earned=s["earned"],
|
||||||
|
pending=s["pending"],
|
||||||
|
apy=s["apy"],
|
||||||
|
)
|
||||||
|
for s in data.get("stakes", [])
|
||||||
|
],
|
||||||
|
last_claim_date=data.get("last_claim_date"),
|
||||||
|
next_claim_available=data.get("next_claim_available"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_discount(self, data: Dict[str, Any]) -> Discount:
|
||||||
|
return Discount(
|
||||||
|
code=data["code"],
|
||||||
|
type=DiscountType(data["type"]),
|
||||||
|
value=data["value"],
|
||||||
|
description=data["description"],
|
||||||
|
valid_from=data["valid_from"],
|
||||||
|
valid_until=data["valid_until"],
|
||||||
|
applicable_services=[ServiceType(s) for s in data.get("applicable_services", [])],
|
||||||
|
used_count=data["used_count"],
|
||||||
|
min_purchase=data.get("min_purchase"),
|
||||||
|
max_discount=data.get("max_discount"),
|
||||||
|
usage_limit=data.get("usage_limit"),
|
||||||
|
)
|
||||||
286
sdk/python/src/synor_economics/types.py
Normal file
286
sdk/python/src/synor_economics/types.py
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
"""
|
||||||
|
Synor Economics SDK Types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceType(str, Enum):
|
||||||
|
"""Service types for pricing."""
|
||||||
|
COMPUTE = "compute"
|
||||||
|
STORAGE = "storage"
|
||||||
|
DATABASE = "database"
|
||||||
|
HOSTING = "hosting"
|
||||||
|
BRIDGE = "bridge"
|
||||||
|
RPC = "rpc"
|
||||||
|
|
||||||
|
|
||||||
|
class BillingPeriod(str, Enum):
|
||||||
|
"""Billing period."""
|
||||||
|
HOURLY = "hourly"
|
||||||
|
DAILY = "daily"
|
||||||
|
WEEKLY = "weekly"
|
||||||
|
MONTHLY = "monthly"
|
||||||
|
|
||||||
|
|
||||||
|
class StakeStatus(str, Enum):
|
||||||
|
"""Stake status."""
|
||||||
|
ACTIVE = "active"
|
||||||
|
UNSTAKING = "unstaking"
|
||||||
|
WITHDRAWN = "withdrawn"
|
||||||
|
SLASHED = "slashed"
|
||||||
|
|
||||||
|
|
||||||
|
class DiscountType(str, Enum):
|
||||||
|
"""Discount type."""
|
||||||
|
PERCENTAGE = "percentage"
|
||||||
|
FIXED = "fixed"
|
||||||
|
VOLUME = "volume"
|
||||||
|
REFERRAL = "referral"
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceStatus(str, Enum):
|
||||||
|
"""Invoice status."""
|
||||||
|
PENDING = "pending"
|
||||||
|
PAID = "paid"
|
||||||
|
OVERDUE = "overdue"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UsageMetrics:
|
||||||
|
"""Usage metrics for pricing."""
|
||||||
|
compute_hours: Optional[float] = None
|
||||||
|
storage_bytes: Optional[int] = None
|
||||||
|
database_ops: Optional[int] = None
|
||||||
|
hosting_requests: Optional[int] = None
|
||||||
|
bridge_transfers: Optional[int] = None
|
||||||
|
rpc_calls: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ServiceUsage:
|
||||||
|
"""Service usage in plan."""
|
||||||
|
service: ServiceType
|
||||||
|
metrics: UsageMetrics
|
||||||
|
tier: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UsagePlan:
|
||||||
|
"""Usage plan for cost estimation."""
|
||||||
|
services: List[ServiceUsage]
|
||||||
|
period: BillingPeriod
|
||||||
|
start_date: Optional[int] = None
|
||||||
|
end_date: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppliedDiscount:
|
||||||
|
"""Applied discount."""
|
||||||
|
code: str
|
||||||
|
type: DiscountType
|
||||||
|
value: str
|
||||||
|
savings: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaxAmount:
|
||||||
|
"""Tax amount."""
|
||||||
|
name: str
|
||||||
|
rate: float
|
||||||
|
amount: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Price:
|
||||||
|
"""Price breakdown."""
|
||||||
|
service: ServiceType
|
||||||
|
base_price: str
|
||||||
|
quantity: str
|
||||||
|
unit: str
|
||||||
|
subtotal: str
|
||||||
|
discounts: List[AppliedDiscount]
|
||||||
|
total: str
|
||||||
|
currency: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CostEstimate:
|
||||||
|
"""Cost estimate."""
|
||||||
|
services: List[Price]
|
||||||
|
subtotal: str
|
||||||
|
discounts: List[AppliedDiscount]
|
||||||
|
taxes: List[TaxAmount]
|
||||||
|
total: str
|
||||||
|
currency: str
|
||||||
|
valid_until: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UsageDetail:
|
||||||
|
"""Usage detail."""
|
||||||
|
timestamp: int
|
||||||
|
operation: str
|
||||||
|
quantity: float
|
||||||
|
unit: str
|
||||||
|
cost: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ServiceUsageRecord:
|
||||||
|
"""Service usage record."""
|
||||||
|
service: ServiceType
|
||||||
|
metrics: UsageMetrics
|
||||||
|
cost: str
|
||||||
|
details: List[UsageDetail]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Usage:
|
||||||
|
"""Usage record."""
|
||||||
|
period: BillingPeriod
|
||||||
|
start_date: int
|
||||||
|
end_date: int
|
||||||
|
services: List[ServiceUsageRecord]
|
||||||
|
total_cost: str
|
||||||
|
currency: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InvoiceItem:
|
||||||
|
"""Invoice item."""
|
||||||
|
service: ServiceType
|
||||||
|
description: str
|
||||||
|
quantity: str
|
||||||
|
unit_price: str
|
||||||
|
total: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Invoice:
|
||||||
|
"""Invoice."""
|
||||||
|
id: str
|
||||||
|
status: InvoiceStatus
|
||||||
|
period: BillingPeriod
|
||||||
|
start_date: int
|
||||||
|
end_date: int
|
||||||
|
items: List[InvoiceItem]
|
||||||
|
subtotal: str
|
||||||
|
discounts: List[AppliedDiscount]
|
||||||
|
taxes: List[TaxAmount]
|
||||||
|
total: str
|
||||||
|
currency: str
|
||||||
|
due_date: int
|
||||||
|
paid_at: Optional[int] = None
|
||||||
|
payment_method: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AccountBalance:
|
||||||
|
"""Account balance."""
|
||||||
|
available: str
|
||||||
|
pending: str
|
||||||
|
reserved: str
|
||||||
|
total: str
|
||||||
|
currency: str
|
||||||
|
last_updated: int
|
||||||
|
credit_limit: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StakeReceipt:
|
||||||
|
"""Stake receipt."""
|
||||||
|
id: str
|
||||||
|
amount: str
|
||||||
|
lock_duration: int
|
||||||
|
start_date: int
|
||||||
|
end_date: int
|
||||||
|
apy: str
|
||||||
|
estimated_rewards: str
|
||||||
|
tx_hash: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UnstakeReceipt:
|
||||||
|
"""Unstake receipt."""
|
||||||
|
id: str
|
||||||
|
stake_id: str
|
||||||
|
amount: str
|
||||||
|
rewards: str
|
||||||
|
total: str
|
||||||
|
unbonding_period: int
|
||||||
|
available_at: int
|
||||||
|
tx_hash: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StakeInfo:
|
||||||
|
"""Stake info."""
|
||||||
|
id: str
|
||||||
|
amount: str
|
||||||
|
status: StakeStatus
|
||||||
|
lock_duration: int
|
||||||
|
start_date: int
|
||||||
|
end_date: int
|
||||||
|
apy: str
|
||||||
|
earned_rewards: str
|
||||||
|
pending_rewards: str
|
||||||
|
validator: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StakeRewardDetail:
|
||||||
|
"""Stake reward detail."""
|
||||||
|
stake_id: str
|
||||||
|
earned: str
|
||||||
|
pending: str
|
||||||
|
apy: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StakingRewards:
|
||||||
|
"""Staking rewards."""
|
||||||
|
total_earned: str
|
||||||
|
pending: str
|
||||||
|
claimed: str
|
||||||
|
stakes: List[StakeRewardDetail]
|
||||||
|
last_claim_date: Optional[int] = None
|
||||||
|
next_claim_available: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Discount:
|
||||||
|
"""Discount."""
|
||||||
|
code: str
|
||||||
|
type: DiscountType
|
||||||
|
value: str
|
||||||
|
description: str
|
||||||
|
valid_from: int
|
||||||
|
valid_until: int
|
||||||
|
applicable_services: List[ServiceType]
|
||||||
|
used_count: int
|
||||||
|
min_purchase: Optional[str] = None
|
||||||
|
max_discount: Optional[str] = None
|
||||||
|
usage_limit: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StakeOptions:
|
||||||
|
"""Staking options."""
|
||||||
|
validator: Optional[str] = None
|
||||||
|
auto_compound: Optional[bool] = None
|
||||||
|
lock_duration: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EconomicsConfig:
|
||||||
|
"""Economics SDK configuration."""
|
||||||
|
api_key: str
|
||||||
|
endpoint: str = "https://economics.synor.io/v1"
|
||||||
|
timeout: float = 30.0
|
||||||
|
retries: int = 3
|
||||||
|
debug: bool = False
|
||||||
56
sdk/python/src/synor_governance/__init__.py
Normal file
56
sdk/python/src/synor_governance/__init__.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
"""
|
||||||
|
Synor Governance SDK for Python.
|
||||||
|
Proposals, voting, DAOs, and vesting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import SynorGovernance, GovernanceError
|
||||||
|
from .types import (
|
||||||
|
ProposalStatus,
|
||||||
|
VoteChoice,
|
||||||
|
ProposalType,
|
||||||
|
DaoType,
|
||||||
|
VestingStatus,
|
||||||
|
ProposalDraft,
|
||||||
|
ProposalAction,
|
||||||
|
Proposal,
|
||||||
|
ProposalFilter,
|
||||||
|
Vote,
|
||||||
|
VoteReceipt,
|
||||||
|
DelegationReceipt,
|
||||||
|
VotingPower,
|
||||||
|
DelegationInfo,
|
||||||
|
DaoConfig,
|
||||||
|
Dao,
|
||||||
|
VestingSchedule,
|
||||||
|
VestingContract,
|
||||||
|
ClaimResult,
|
||||||
|
GovernanceTransaction,
|
||||||
|
GovernanceConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__all__ = [
|
||||||
|
"SynorGovernance",
|
||||||
|
"GovernanceError",
|
||||||
|
"ProposalStatus",
|
||||||
|
"VoteChoice",
|
||||||
|
"ProposalType",
|
||||||
|
"DaoType",
|
||||||
|
"VestingStatus",
|
||||||
|
"ProposalDraft",
|
||||||
|
"ProposalAction",
|
||||||
|
"Proposal",
|
||||||
|
"ProposalFilter",
|
||||||
|
"Vote",
|
||||||
|
"VoteReceipt",
|
||||||
|
"DelegationReceipt",
|
||||||
|
"VotingPower",
|
||||||
|
"DelegationInfo",
|
||||||
|
"DaoConfig",
|
||||||
|
"Dao",
|
||||||
|
"VestingSchedule",
|
||||||
|
"VestingContract",
|
||||||
|
"ClaimResult",
|
||||||
|
"GovernanceTransaction",
|
||||||
|
"GovernanceConfig",
|
||||||
|
]
|
||||||
478
sdk/python/src/synor_governance/client.py
Normal file
478
sdk/python/src/synor_governance/client.py
Normal file
|
|
@ -0,0 +1,478 @@
|
||||||
|
"""
|
||||||
|
Synor Governance SDK Client.
|
||||||
|
Proposals, voting, DAOs, and vesting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from urllib.parse import urlencode, quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .types import (
|
||||||
|
GovernanceConfig,
|
||||||
|
ProposalDraft,
|
||||||
|
Proposal,
|
||||||
|
ProposalFilter,
|
||||||
|
ProposalAction,
|
||||||
|
ProposalStatus,
|
||||||
|
ProposalType,
|
||||||
|
Vote,
|
||||||
|
VoteReceipt,
|
||||||
|
VoteChoice,
|
||||||
|
DelegationReceipt,
|
||||||
|
VotingPower,
|
||||||
|
DelegationInfo,
|
||||||
|
DaoConfig,
|
||||||
|
Dao,
|
||||||
|
DaoType,
|
||||||
|
VestingSchedule,
|
||||||
|
VestingContract,
|
||||||
|
VestingStatus,
|
||||||
|
ClaimResult,
|
||||||
|
GovernanceTransaction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GovernanceError(Exception):
|
||||||
|
"""Governance SDK Error."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, code: Optional[str] = None, status_code: int = 0):
|
||||||
|
super().__init__(message)
|
||||||
|
self.code = code
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class SynorGovernance:
|
||||||
|
"""
|
||||||
|
Synor Governance SDK client for Python.
|
||||||
|
Proposals, voting, DAOs, and vesting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: GovernanceConfig):
|
||||||
|
self._config = config
|
||||||
|
self._closed = False
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=config.endpoint,
|
||||||
|
timeout=config.timeout,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {config.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-SDK-Version": "python/0.1.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== Proposal Operations ====================
|
||||||
|
|
||||||
|
async def create_proposal(self, draft: ProposalDraft) -> Proposal:
|
||||||
|
"""Create a new proposal."""
|
||||||
|
body = self._draft_to_dict(draft)
|
||||||
|
response = await self._post("/proposals", body)
|
||||||
|
return self._parse_proposal(response)
|
||||||
|
|
||||||
|
async def get_proposal(self, proposal_id: str) -> Proposal:
|
||||||
|
"""Get a proposal by ID."""
|
||||||
|
response = await self._get(f"/proposals/{quote(proposal_id)}")
|
||||||
|
return self._parse_proposal(response)
|
||||||
|
|
||||||
|
async def list_proposals(self, filter: Optional[ProposalFilter] = None) -> List[Proposal]:
|
||||||
|
"""List proposals with optional filtering."""
|
||||||
|
params = {}
|
||||||
|
if filter:
|
||||||
|
if filter.status:
|
||||||
|
params["status"] = filter.status.value
|
||||||
|
if filter.type:
|
||||||
|
params["type"] = filter.type.value
|
||||||
|
if filter.proposer:
|
||||||
|
params["proposer"] = filter.proposer
|
||||||
|
if filter.limit:
|
||||||
|
params["limit"] = str(filter.limit)
|
||||||
|
if filter.offset:
|
||||||
|
params["offset"] = str(filter.offset)
|
||||||
|
query = f"?{urlencode(params)}" if params else ""
|
||||||
|
response = await self._get(f"/proposals{query}")
|
||||||
|
return [self._parse_proposal(p) for p in response.get("proposals", [])]
|
||||||
|
|
||||||
|
async def cancel_proposal(self, proposal_id: str) -> Proposal:
|
||||||
|
"""Cancel a proposal."""
|
||||||
|
response = await self._post(f"/proposals/{quote(proposal_id)}/cancel", {})
|
||||||
|
return self._parse_proposal(response)
|
||||||
|
|
||||||
|
async def execute_proposal(self, proposal_id: str) -> GovernanceTransaction:
|
||||||
|
"""Execute a passed proposal."""
|
||||||
|
response = await self._post(f"/proposals/{quote(proposal_id)}/execute", {})
|
||||||
|
return GovernanceTransaction(
|
||||||
|
tx_hash=response["tx_hash"],
|
||||||
|
timestamp=response["timestamp"],
|
||||||
|
block_number=response["block_number"],
|
||||||
|
status=response["status"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== Voting Operations ====================
|
||||||
|
|
||||||
|
async def vote(
|
||||||
|
self, proposal_id: str, vote: Vote, weight: Optional[str] = None
|
||||||
|
) -> VoteReceipt:
|
||||||
|
"""Vote on a proposal."""
|
||||||
|
body: Dict[str, Any] = {"choice": vote.choice.value}
|
||||||
|
if vote.reason:
|
||||||
|
body["reason"] = vote.reason
|
||||||
|
if weight:
|
||||||
|
body["weight"] = weight
|
||||||
|
response = await self._post(f"/proposals/{quote(proposal_id)}/vote", body)
|
||||||
|
return self._parse_vote_receipt(response)
|
||||||
|
|
||||||
|
async def get_votes(
|
||||||
|
self,
|
||||||
|
proposal_id: str,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
offset: Optional[int] = None,
|
||||||
|
) -> List[VoteReceipt]:
|
||||||
|
"""Get votes for a proposal."""
|
||||||
|
params = {}
|
||||||
|
if limit:
|
||||||
|
params["limit"] = str(limit)
|
||||||
|
if offset:
|
||||||
|
params["offset"] = str(offset)
|
||||||
|
query = f"?{urlencode(params)}" if params else ""
|
||||||
|
response = await self._get(f"/proposals/{quote(proposal_id)}/votes{query}")
|
||||||
|
return [self._parse_vote_receipt(v) for v in response.get("votes", [])]
|
||||||
|
|
||||||
|
async def delegate(
|
||||||
|
self, to: str, amount: Optional[str] = None
|
||||||
|
) -> DelegationReceipt:
|
||||||
|
"""Delegate voting power to another address."""
|
||||||
|
body: Dict[str, Any] = {"to": to}
|
||||||
|
if amount:
|
||||||
|
body["amount"] = amount
|
||||||
|
response = await self._post("/voting/delegate", body)
|
||||||
|
return self._parse_delegation_receipt(response)
|
||||||
|
|
||||||
|
async def undelegate(self, from_addr: Optional[str] = None) -> DelegationReceipt:
|
||||||
|
"""Undelegate voting power."""
|
||||||
|
body: Dict[str, Any] = {}
|
||||||
|
if from_addr:
|
||||||
|
body["from"] = from_addr
|
||||||
|
response = await self._post("/voting/undelegate", body)
|
||||||
|
return self._parse_delegation_receipt(response)
|
||||||
|
|
||||||
|
async def get_voting_power(self, address: str) -> VotingPower:
|
||||||
|
"""Get voting power for an address."""
|
||||||
|
response = await self._get(f"/voting/power/{quote(address)}")
|
||||||
|
return self._parse_voting_power(response)
|
||||||
|
|
||||||
|
async def get_my_voting_power(self) -> VotingPower:
|
||||||
|
"""Get current voting power for the authenticated user."""
|
||||||
|
response = await self._get("/voting/power")
|
||||||
|
return self._parse_voting_power(response)
|
||||||
|
|
||||||
|
# ==================== DAO Operations ====================
|
||||||
|
|
||||||
|
async def create_dao(self, config: DaoConfig) -> Dao:
|
||||||
|
"""Create a new DAO."""
|
||||||
|
body = self._dao_config_to_dict(config)
|
||||||
|
response = await self._post("/daos", body)
|
||||||
|
return self._parse_dao(response)
|
||||||
|
|
||||||
|
async def get_dao(self, dao_id: str) -> Dao:
|
||||||
|
"""Get a DAO by ID."""
|
||||||
|
response = await self._get(f"/daos/{quote(dao_id)}")
|
||||||
|
return self._parse_dao(response)
|
||||||
|
|
||||||
|
async def list_daos(
|
||||||
|
self, limit: Optional[int] = None, offset: Optional[int] = None
|
||||||
|
) -> List[Dao]:
|
||||||
|
"""List DAOs."""
|
||||||
|
params = {}
|
||||||
|
if limit:
|
||||||
|
params["limit"] = str(limit)
|
||||||
|
if offset:
|
||||||
|
params["offset"] = str(offset)
|
||||||
|
query = f"?{urlencode(params)}" if params else ""
|
||||||
|
response = await self._get(f"/daos{query}")
|
||||||
|
return [self._parse_dao(d) for d in response.get("daos", [])]
|
||||||
|
|
||||||
|
async def get_dao_members(
|
||||||
|
self,
|
||||||
|
dao_id: str,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
offset: Optional[int] = None,
|
||||||
|
) -> List[Dict[str, str]]:
|
||||||
|
"""Get DAO members."""
|
||||||
|
params = {}
|
||||||
|
if limit:
|
||||||
|
params["limit"] = str(limit)
|
||||||
|
if offset:
|
||||||
|
params["offset"] = str(offset)
|
||||||
|
query = f"?{urlencode(params)}" if params else ""
|
||||||
|
response = await self._get(f"/daos/{quote(dao_id)}/members{query}")
|
||||||
|
return response.get("members", [])
|
||||||
|
|
||||||
|
# ==================== Vesting Operations ====================
|
||||||
|
|
||||||
|
async def create_vesting_schedule(self, schedule: VestingSchedule) -> VestingContract:
|
||||||
|
"""Create a vesting schedule."""
|
||||||
|
body = {
|
||||||
|
"beneficiary": schedule.beneficiary,
|
||||||
|
"total_amount": schedule.total_amount,
|
||||||
|
"start_time": schedule.start_time,
|
||||||
|
"cliff_duration": schedule.cliff_duration,
|
||||||
|
"vesting_duration": schedule.vesting_duration,
|
||||||
|
"revocable": schedule.revocable,
|
||||||
|
}
|
||||||
|
response = await self._post("/vesting", body)
|
||||||
|
return self._parse_vesting_contract(response)
|
||||||
|
|
||||||
|
async def get_vesting_contract(self, contract_id: str) -> VestingContract:
|
||||||
|
"""Get a vesting contract."""
|
||||||
|
response = await self._get(f"/vesting/{quote(contract_id)}")
|
||||||
|
return self._parse_vesting_contract(response)
|
||||||
|
|
||||||
|
async def list_vesting_contracts(
|
||||||
|
self, beneficiary: Optional[str] = None
|
||||||
|
) -> List[VestingContract]:
|
||||||
|
"""List vesting contracts."""
|
||||||
|
query = f"?beneficiary={quote(beneficiary)}" if beneficiary else ""
|
||||||
|
response = await self._get(f"/vesting{query}")
|
||||||
|
return [self._parse_vesting_contract(c) for c in response.get("contracts", [])]
|
||||||
|
|
||||||
|
async def claim_vested(self, contract_id: str) -> ClaimResult:
|
||||||
|
"""Claim vested tokens."""
|
||||||
|
response = await self._post(f"/vesting/{quote(contract_id)}/claim", {})
|
||||||
|
return ClaimResult(
|
||||||
|
vesting_id=response["vesting_id"],
|
||||||
|
amount=response["amount"],
|
||||||
|
recipient=response["recipient"],
|
||||||
|
timestamp=response["timestamp"],
|
||||||
|
tx_hash=response["tx_hash"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def revoke_vesting(self, contract_id: str) -> VestingContract:
|
||||||
|
"""Revoke a vesting contract (if revocable)."""
|
||||||
|
response = await self._post(f"/vesting/{quote(contract_id)}/revoke", {})
|
||||||
|
return self._parse_vesting_contract(response)
|
||||||
|
|
||||||
|
async def get_claimable_amount(self, contract_id: str) -> Dict[str, str]:
|
||||||
|
"""Get claimable amount for a vesting contract."""
|
||||||
|
return await self._get(f"/vesting/{quote(contract_id)}/claimable")
|
||||||
|
|
||||||
|
# ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
async def health_check(self) -> bool:
|
||||||
|
"""Health check."""
|
||||||
|
try:
|
||||||
|
response = await self._get("/health")
|
||||||
|
return response.get("status") == "healthy"
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self) -> bool:
|
||||||
|
"""Check if client is closed."""
|
||||||
|
return self._closed
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""Close the client."""
|
||||||
|
self._closed = True
|
||||||
|
await self._client.aclose()
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "SynorGovernance":
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args) -> None:
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
# ==================== Private Methods ====================
|
||||||
|
|
||||||
|
async def _get(self, path: str) -> Dict[str, Any]:
|
||||||
|
return await self._execute(lambda: self._client.get(path))
|
||||||
|
|
||||||
|
async def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
return await self._execute(lambda: self._client.post(path, json=body))
|
||||||
|
|
||||||
|
async def _execute(self, operation) -> Dict[str, Any]:
|
||||||
|
if self._closed:
|
||||||
|
raise GovernanceError("Client has been closed")
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for attempt in range(self._config.retries):
|
||||||
|
try:
|
||||||
|
response = await operation()
|
||||||
|
self._ensure_success(response)
|
||||||
|
if response.content:
|
||||||
|
return response.json()
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
if self._config.debug:
|
||||||
|
print(f"Attempt {attempt + 1} failed: {e}")
|
||||||
|
if attempt < self._config.retries - 1:
|
||||||
|
await asyncio.sleep(2**attempt)
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
def _ensure_success(self, response: httpx.Response) -> None:
|
||||||
|
if response.status_code >= 400:
|
||||||
|
message = f"HTTP {response.status_code}"
|
||||||
|
code = None
|
||||||
|
try:
|
||||||
|
error = response.json()
|
||||||
|
message = error.get("message", message)
|
||||||
|
code = error.get("code")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise GovernanceError(message, code, response.status_code)
|
||||||
|
|
||||||
|
# ==================== Parsing Methods ====================
|
||||||
|
|
||||||
|
def _draft_to_dict(self, draft: ProposalDraft) -> Dict[str, Any]:
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"title": draft.title,
|
||||||
|
"description": draft.description,
|
||||||
|
"type": draft.type.value,
|
||||||
|
}
|
||||||
|
if draft.actions:
|
||||||
|
result["actions"] = [
|
||||||
|
{
|
||||||
|
"target": a.target,
|
||||||
|
"value": a.value,
|
||||||
|
"calldata": a.calldata,
|
||||||
|
"description": a.description,
|
||||||
|
}
|
||||||
|
for a in draft.actions
|
||||||
|
]
|
||||||
|
if draft.start_time:
|
||||||
|
result["start_time"] = draft.start_time
|
||||||
|
if draft.end_time:
|
||||||
|
result["end_time"] = draft.end_time
|
||||||
|
if draft.quorum:
|
||||||
|
result["quorum"] = draft.quorum
|
||||||
|
if draft.threshold:
|
||||||
|
result["threshold"] = draft.threshold
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _dao_config_to_dict(self, config: DaoConfig) -> Dict[str, Any]:
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"name": config.name,
|
||||||
|
"type": config.type.value,
|
||||||
|
"voting_period": config.voting_period,
|
||||||
|
"quorum": config.quorum,
|
||||||
|
"proposal_threshold": config.proposal_threshold,
|
||||||
|
}
|
||||||
|
if config.token_address:
|
||||||
|
result["token_address"] = config.token_address
|
||||||
|
if config.signers:
|
||||||
|
result["signers"] = config.signers
|
||||||
|
if config.threshold:
|
||||||
|
result["threshold"] = config.threshold
|
||||||
|
if config.timelock_delay:
|
||||||
|
result["timelock_delay"] = config.timelock_delay
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _parse_proposal(self, data: Dict[str, Any]) -> Proposal:
|
||||||
|
return Proposal(
|
||||||
|
id=data["id"],
|
||||||
|
title=data["title"],
|
||||||
|
description=data["description"],
|
||||||
|
type=ProposalType(data["type"]),
|
||||||
|
status=ProposalStatus(data["status"]),
|
||||||
|
proposer=data["proposer"],
|
||||||
|
actions=[
|
||||||
|
ProposalAction(
|
||||||
|
target=a["target"],
|
||||||
|
value=a["value"],
|
||||||
|
calldata=a["calldata"],
|
||||||
|
description=a.get("description"),
|
||||||
|
)
|
||||||
|
for a in data.get("actions", [])
|
||||||
|
],
|
||||||
|
start_time=data["start_time"],
|
||||||
|
end_time=data["end_time"],
|
||||||
|
quorum=data["quorum"],
|
||||||
|
threshold=data["threshold"],
|
||||||
|
for_votes=data["for_votes"],
|
||||||
|
against_votes=data["against_votes"],
|
||||||
|
abstain_votes=data["abstain_votes"],
|
||||||
|
total_votes=data["total_votes"],
|
||||||
|
created_at=data["created_at"],
|
||||||
|
executed_at=data.get("executed_at"),
|
||||||
|
execution_tx_hash=data.get("execution_tx_hash"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_vote_receipt(self, data: Dict[str, Any]) -> VoteReceipt:
|
||||||
|
return VoteReceipt(
|
||||||
|
proposal_id=data["proposal_id"],
|
||||||
|
voter=data["voter"],
|
||||||
|
choice=VoteChoice(data["choice"]),
|
||||||
|
weight=data["weight"],
|
||||||
|
timestamp=data["timestamp"],
|
||||||
|
tx_hash=data["tx_hash"],
|
||||||
|
reason=data.get("reason"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_delegation_receipt(self, data: Dict[str, Any]) -> DelegationReceipt:
|
||||||
|
return DelegationReceipt(
|
||||||
|
delegator=data["delegator"],
|
||||||
|
delegatee=data["delegatee"],
|
||||||
|
amount=data["amount"],
|
||||||
|
timestamp=data["timestamp"],
|
||||||
|
tx_hash=data["tx_hash"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_voting_power(self, data: Dict[str, Any]) -> VotingPower:
|
||||||
|
delegated_to = None
|
||||||
|
if data.get("delegated_to"):
|
||||||
|
delegated_to = DelegationInfo(
|
||||||
|
address=data["delegated_to"]["address"],
|
||||||
|
amount=data["delegated_to"]["amount"],
|
||||||
|
timestamp=data["delegated_to"]["timestamp"],
|
||||||
|
)
|
||||||
|
return VotingPower(
|
||||||
|
address=data["address"],
|
||||||
|
own_power=data["own_power"],
|
||||||
|
delegated_power=data["delegated_power"],
|
||||||
|
total_power=data["total_power"],
|
||||||
|
delegated_from=[
|
||||||
|
DelegationInfo(
|
||||||
|
address=d["address"],
|
||||||
|
amount=d["amount"],
|
||||||
|
timestamp=d["timestamp"],
|
||||||
|
)
|
||||||
|
for d in data.get("delegated_from", [])
|
||||||
|
],
|
||||||
|
delegated_to=delegated_to,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_dao(self, data: Dict[str, Any]) -> Dao:
|
||||||
|
return Dao(
|
||||||
|
id=data["id"],
|
||||||
|
name=data["name"],
|
||||||
|
type=DaoType(data["type"]),
|
||||||
|
voting_period=data["voting_period"],
|
||||||
|
quorum=data["quorum"],
|
||||||
|
proposal_threshold=data["proposal_threshold"],
|
||||||
|
treasury=data["treasury"],
|
||||||
|
total_proposals=data["total_proposals"],
|
||||||
|
active_proposals=data["active_proposals"],
|
||||||
|
total_members=data["total_members"],
|
||||||
|
created_at=data["created_at"],
|
||||||
|
token_address=data.get("token_address"),
|
||||||
|
signers=data.get("signers"),
|
||||||
|
threshold=data.get("threshold"),
|
||||||
|
timelock_delay=data.get("timelock_delay"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_vesting_contract(self, data: Dict[str, Any]) -> VestingContract:
|
||||||
|
return VestingContract(
|
||||||
|
id=data["id"],
|
||||||
|
beneficiary=data["beneficiary"],
|
||||||
|
total_amount=data["total_amount"],
|
||||||
|
released_amount=data["released_amount"],
|
||||||
|
vested_amount=data["vested_amount"],
|
||||||
|
start_time=data["start_time"],
|
||||||
|
cliff_time=data["cliff_time"],
|
||||||
|
end_time=data["end_time"],
|
||||||
|
revocable=data["revocable"],
|
||||||
|
status=VestingStatus(data["status"]),
|
||||||
|
created_at=data["created_at"],
|
||||||
|
tx_hash=data["tx_hash"],
|
||||||
|
)
|
||||||
243
sdk/python/src/synor_governance/types.py
Normal file
243
sdk/python/src/synor_governance/types.py
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
"""
|
||||||
|
Synor Governance SDK Types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalStatus(str, Enum):
|
||||||
|
"""Proposal status."""
|
||||||
|
DRAFT = "draft"
|
||||||
|
ACTIVE = "active"
|
||||||
|
PASSED = "passed"
|
||||||
|
REJECTED = "rejected"
|
||||||
|
EXECUTED = "executed"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
EXPIRED = "expired"
|
||||||
|
|
||||||
|
|
||||||
|
class VoteChoice(str, Enum):
|
||||||
|
"""Vote choice."""
|
||||||
|
FOR = "for"
|
||||||
|
AGAINST = "against"
|
||||||
|
ABSTAIN = "abstain"
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalType(str, Enum):
|
||||||
|
"""Proposal type."""
|
||||||
|
PARAMETER_CHANGE = "parameter_change"
|
||||||
|
TREASURY_SPEND = "treasury_spend"
|
||||||
|
UPGRADE = "upgrade"
|
||||||
|
TEXT = "text"
|
||||||
|
CUSTOM = "custom"
|
||||||
|
|
||||||
|
|
||||||
|
class DaoType(str, Enum):
|
||||||
|
"""DAO type."""
|
||||||
|
TOKEN = "token"
|
||||||
|
MULTISIG = "multisig"
|
||||||
|
HYBRID = "hybrid"
|
||||||
|
|
||||||
|
|
||||||
|
class VestingStatus(str, Enum):
|
||||||
|
"""Vesting status."""
|
||||||
|
ACTIVE = "active"
|
||||||
|
PAUSED = "paused"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProposalAction:
|
||||||
|
"""Proposal action."""
|
||||||
|
target: str
|
||||||
|
value: str
|
||||||
|
calldata: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProposalDraft:
|
||||||
|
"""Proposal draft."""
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
type: ProposalType
|
||||||
|
actions: List[ProposalAction] = field(default_factory=list)
|
||||||
|
start_time: Optional[int] = None
|
||||||
|
end_time: Optional[int] = None
|
||||||
|
quorum: Optional[str] = None
|
||||||
|
threshold: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Proposal:
|
||||||
|
"""Proposal."""
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
type: ProposalType
|
||||||
|
status: ProposalStatus
|
||||||
|
proposer: str
|
||||||
|
actions: List[ProposalAction]
|
||||||
|
start_time: int
|
||||||
|
end_time: int
|
||||||
|
quorum: str
|
||||||
|
threshold: str
|
||||||
|
for_votes: str
|
||||||
|
against_votes: str
|
||||||
|
abstain_votes: str
|
||||||
|
total_votes: str
|
||||||
|
created_at: int
|
||||||
|
executed_at: Optional[int] = None
|
||||||
|
execution_tx_hash: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProposalFilter:
|
||||||
|
"""Proposal filter."""
|
||||||
|
status: Optional[ProposalStatus] = None
|
||||||
|
type: Optional[ProposalType] = None
|
||||||
|
proposer: Optional[str] = None
|
||||||
|
limit: Optional[int] = None
|
||||||
|
offset: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Vote:
|
||||||
|
"""Vote."""
|
||||||
|
choice: VoteChoice
|
||||||
|
reason: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VoteReceipt:
|
||||||
|
"""Vote receipt."""
|
||||||
|
proposal_id: str
|
||||||
|
voter: str
|
||||||
|
choice: VoteChoice
|
||||||
|
weight: str
|
||||||
|
timestamp: int
|
||||||
|
tx_hash: str
|
||||||
|
reason: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DelegationInfo:
|
||||||
|
"""Delegation info."""
|
||||||
|
address: str
|
||||||
|
amount: str
|
||||||
|
timestamp: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DelegationReceipt:
|
||||||
|
"""Delegation receipt."""
|
||||||
|
delegator: str
|
||||||
|
delegatee: str
|
||||||
|
amount: str
|
||||||
|
timestamp: int
|
||||||
|
tx_hash: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VotingPower:
|
||||||
|
"""Voting power."""
|
||||||
|
address: str
|
||||||
|
own_power: str
|
||||||
|
delegated_power: str
|
||||||
|
total_power: str
|
||||||
|
delegated_from: List[DelegationInfo]
|
||||||
|
delegated_to: Optional[DelegationInfo] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DaoConfig:
|
||||||
|
"""DAO configuration."""
|
||||||
|
name: str
|
||||||
|
type: DaoType
|
||||||
|
voting_period: int
|
||||||
|
quorum: str
|
||||||
|
proposal_threshold: str
|
||||||
|
token_address: Optional[str] = None
|
||||||
|
signers: Optional[List[str]] = None
|
||||||
|
threshold: Optional[int] = None
|
||||||
|
timelock_delay: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Dao:
|
||||||
|
"""DAO."""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
type: DaoType
|
||||||
|
voting_period: int
|
||||||
|
quorum: str
|
||||||
|
proposal_threshold: str
|
||||||
|
treasury: str
|
||||||
|
total_proposals: int
|
||||||
|
active_proposals: int
|
||||||
|
total_members: int
|
||||||
|
created_at: int
|
||||||
|
token_address: Optional[str] = None
|
||||||
|
signers: Optional[List[str]] = None
|
||||||
|
threshold: Optional[int] = None
|
||||||
|
timelock_delay: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VestingSchedule:
|
||||||
|
"""Vesting schedule."""
|
||||||
|
beneficiary: str
|
||||||
|
total_amount: str
|
||||||
|
start_time: int
|
||||||
|
cliff_duration: int
|
||||||
|
vesting_duration: int
|
||||||
|
revocable: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VestingContract:
|
||||||
|
"""Vesting contract."""
|
||||||
|
id: str
|
||||||
|
beneficiary: str
|
||||||
|
total_amount: str
|
||||||
|
released_amount: str
|
||||||
|
vested_amount: str
|
||||||
|
start_time: int
|
||||||
|
cliff_time: int
|
||||||
|
end_time: int
|
||||||
|
revocable: bool
|
||||||
|
status: VestingStatus
|
||||||
|
created_at: int
|
||||||
|
tx_hash: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ClaimResult:
|
||||||
|
"""Claim result."""
|
||||||
|
vesting_id: str
|
||||||
|
amount: str
|
||||||
|
recipient: str
|
||||||
|
timestamp: int
|
||||||
|
tx_hash: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GovernanceTransaction:
|
||||||
|
"""Governance transaction."""
|
||||||
|
tx_hash: str
|
||||||
|
timestamp: int
|
||||||
|
block_number: int
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GovernanceConfig:
|
||||||
|
"""Governance SDK configuration."""
|
||||||
|
api_key: str
|
||||||
|
endpoint: str = "https://governance.synor.io/v1"
|
||||||
|
timeout: float = 30.0
|
||||||
|
retries: int = 3
|
||||||
|
debug: bool = False
|
||||||
64
sdk/python/src/synor_mining/__init__.py
Normal file
64
sdk/python/src/synor_mining/__init__.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
"""
|
||||||
|
Synor Mining SDK for Python.
|
||||||
|
Pool connections, block templates, hashrate stats, and GPU management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import SynorMining, MiningError
|
||||||
|
from .types import (
|
||||||
|
DeviceType,
|
||||||
|
DeviceStatus,
|
||||||
|
ConnectionStatus,
|
||||||
|
TimePeriod,
|
||||||
|
SubmitResultStatus,
|
||||||
|
PoolConfig,
|
||||||
|
StratumConnection,
|
||||||
|
BlockTemplate,
|
||||||
|
TemplateTransaction,
|
||||||
|
MinedWork,
|
||||||
|
SubmitResult,
|
||||||
|
ShareInfo,
|
||||||
|
Hashrate,
|
||||||
|
MiningStats,
|
||||||
|
ShareStats,
|
||||||
|
DeviceTemperature,
|
||||||
|
EarningsSnapshot,
|
||||||
|
Earnings,
|
||||||
|
EarningsBreakdown,
|
||||||
|
MiningDevice,
|
||||||
|
DeviceConfig,
|
||||||
|
WorkerInfo,
|
||||||
|
PoolStats,
|
||||||
|
MiningAlgorithm,
|
||||||
|
MiningConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__all__ = [
|
||||||
|
"SynorMining",
|
||||||
|
"MiningError",
|
||||||
|
"DeviceType",
|
||||||
|
"DeviceStatus",
|
||||||
|
"ConnectionStatus",
|
||||||
|
"TimePeriod",
|
||||||
|
"SubmitResultStatus",
|
||||||
|
"PoolConfig",
|
||||||
|
"StratumConnection",
|
||||||
|
"BlockTemplate",
|
||||||
|
"TemplateTransaction",
|
||||||
|
"MinedWork",
|
||||||
|
"SubmitResult",
|
||||||
|
"ShareInfo",
|
||||||
|
"Hashrate",
|
||||||
|
"MiningStats",
|
||||||
|
"ShareStats",
|
||||||
|
"DeviceTemperature",
|
||||||
|
"EarningsSnapshot",
|
||||||
|
"Earnings",
|
||||||
|
"EarningsBreakdown",
|
||||||
|
"MiningDevice",
|
||||||
|
"DeviceConfig",
|
||||||
|
"WorkerInfo",
|
||||||
|
"PoolStats",
|
||||||
|
"MiningAlgorithm",
|
||||||
|
"MiningConfig",
|
||||||
|
]
|
||||||
501
sdk/python/src/synor_mining/client.py
Normal file
501
sdk/python/src/synor_mining/client.py
Normal file
|
|
@ -0,0 +1,501 @@
|
||||||
|
"""
|
||||||
|
Synor Mining SDK Client.
|
||||||
|
Pool connections, block templates, hashrate stats, and GPU management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from urllib.parse import urlencode, quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .types import (
|
||||||
|
MiningConfig,
|
||||||
|
PoolConfig,
|
||||||
|
StratumConnection,
|
||||||
|
BlockTemplate,
|
||||||
|
TemplateTransaction,
|
||||||
|
MinedWork,
|
||||||
|
SubmitResult,
|
||||||
|
ShareInfo,
|
||||||
|
SubmitResultStatus,
|
||||||
|
Hashrate,
|
||||||
|
MiningStats,
|
||||||
|
ShareStats,
|
||||||
|
DeviceTemperature,
|
||||||
|
EarningsSnapshot,
|
||||||
|
TimePeriod,
|
||||||
|
Earnings,
|
||||||
|
EarningsBreakdown,
|
||||||
|
MiningDevice,
|
||||||
|
DeviceConfig,
|
||||||
|
DeviceType,
|
||||||
|
DeviceStatus,
|
||||||
|
ConnectionStatus,
|
||||||
|
WorkerInfo,
|
||||||
|
PoolStats,
|
||||||
|
MiningAlgorithm,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MiningError(Exception):
|
||||||
|
"""Mining SDK Error."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, code: Optional[str] = None, status_code: int = 0):
|
||||||
|
super().__init__(message)
|
||||||
|
self.code = code
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class SynorMining:
|
||||||
|
"""
|
||||||
|
Synor Mining SDK client for Python.
|
||||||
|
Pool connections, block templates, hashrate stats, and GPU management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: MiningConfig):
|
||||||
|
self._config = config
|
||||||
|
self._closed = False
|
||||||
|
self._active_connection: Optional[StratumConnection] = None
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=config.endpoint,
|
||||||
|
timeout=config.timeout,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {config.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-SDK-Version": "python/0.1.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== Pool Connection ====================
|
||||||
|
|
||||||
|
async def connect(self, pool: PoolConfig) -> StratumConnection:
|
||||||
|
"""Connect to a mining pool."""
|
||||||
|
body = {
|
||||||
|
"url": pool.url,
|
||||||
|
"user": pool.user,
|
||||||
|
}
|
||||||
|
if pool.password:
|
||||||
|
body["password"] = pool.password
|
||||||
|
if pool.algorithm:
|
||||||
|
body["algorithm"] = pool.algorithm
|
||||||
|
if pool.difficulty:
|
||||||
|
body["difficulty"] = pool.difficulty
|
||||||
|
|
||||||
|
response = await self._post("/pool/connect", body)
|
||||||
|
connection = self._parse_connection(response)
|
||||||
|
self._active_connection = connection
|
||||||
|
return connection
|
||||||
|
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
"""Disconnect from the current pool."""
|
||||||
|
if self._active_connection:
|
||||||
|
await self._post(f"/pool/disconnect/{self._active_connection.id}", {})
|
||||||
|
self._active_connection = None
|
||||||
|
|
||||||
|
async def get_connection_status(self) -> Optional[StratumConnection]:
|
||||||
|
"""Get current connection status."""
|
||||||
|
if not self._active_connection:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
response = await self._get(f"/pool/status/{self._active_connection.id}")
|
||||||
|
return self._parse_connection(response)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def reconnect(self) -> StratumConnection:
|
||||||
|
"""Reconnect to the pool."""
|
||||||
|
if not self._active_connection:
|
||||||
|
raise MiningError("No active connection to reconnect")
|
||||||
|
response = await self._post(f"/pool/reconnect/{self._active_connection.id}", {})
|
||||||
|
return self._parse_connection(response)
|
||||||
|
|
||||||
|
# ==================== Mining Operations ====================
|
||||||
|
|
||||||
|
async def get_block_template(self) -> BlockTemplate:
|
||||||
|
"""Get the current block template."""
|
||||||
|
response = await self._get("/mining/template")
|
||||||
|
return self._parse_block_template(response)
|
||||||
|
|
||||||
|
async def submit_work(self, work: MinedWork) -> SubmitResult:
|
||||||
|
"""Submit mined work."""
|
||||||
|
body = {
|
||||||
|
"template_id": work.template_id,
|
||||||
|
"nonce": work.nonce,
|
||||||
|
"extra_nonce": work.extra_nonce,
|
||||||
|
"timestamp": work.timestamp,
|
||||||
|
"hash": work.hash,
|
||||||
|
}
|
||||||
|
response = await self._post("/mining/submit", body)
|
||||||
|
return self._parse_submit_result(response)
|
||||||
|
|
||||||
|
async def get_work(self) -> Dict[str, str]:
|
||||||
|
"""Get work from pool (stratum getwork)."""
|
||||||
|
return await self._get("/mining/getwork")
|
||||||
|
|
||||||
|
async def start_mining(self, algorithm: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""Start mining on all enabled devices."""
|
||||||
|
body = {"algorithm": algorithm} if algorithm else {}
|
||||||
|
return await self._post("/mining/start", body)
|
||||||
|
|
||||||
|
async def stop_mining(self) -> Dict[str, bool]:
|
||||||
|
"""Stop mining on all devices."""
|
||||||
|
return await self._post("/mining/stop", {})
|
||||||
|
|
||||||
|
# ==================== Stats ====================
|
||||||
|
|
||||||
|
async def get_hashrate(self) -> Hashrate:
|
||||||
|
"""Get current hashrate."""
|
||||||
|
response = await self._get("/stats/hashrate")
|
||||||
|
return self._parse_hashrate(response)
|
||||||
|
|
||||||
|
async def get_stats(self) -> MiningStats:
|
||||||
|
"""Get mining stats."""
|
||||||
|
response = await self._get("/stats")
|
||||||
|
return self._parse_mining_stats(response)
|
||||||
|
|
||||||
|
async def get_earnings(self, period: Optional[TimePeriod] = None) -> Earnings:
|
||||||
|
"""Get earnings for a time period."""
|
||||||
|
query = f"?period={period.value}" if period else ""
|
||||||
|
response = await self._get(f"/stats/earnings{query}")
|
||||||
|
return self._parse_earnings(response)
|
||||||
|
|
||||||
|
async def get_earnings_history(
|
||||||
|
self, limit: Optional[int] = None, offset: Optional[int] = None
|
||||||
|
) -> List[Earnings]:
|
||||||
|
"""Get earnings history."""
|
||||||
|
params = {}
|
||||||
|
if limit:
|
||||||
|
params["limit"] = str(limit)
|
||||||
|
if offset:
|
||||||
|
params["offset"] = str(offset)
|
||||||
|
query = f"?{urlencode(params)}" if params else ""
|
||||||
|
response = await self._get(f"/stats/earnings/history{query}")
|
||||||
|
return [self._parse_earnings(e) for e in response.get("earnings", [])]
|
||||||
|
|
||||||
|
async def get_pool_stats(self) -> PoolStats:
|
||||||
|
"""Get pool stats."""
|
||||||
|
response = await self._get("/pool/stats")
|
||||||
|
return PoolStats(
|
||||||
|
url=response["url"],
|
||||||
|
workers=response["workers"],
|
||||||
|
hashrate=response["hashrate"],
|
||||||
|
difficulty=response["difficulty"],
|
||||||
|
last_block=response["last_block"],
|
||||||
|
blocks_found_24h=response["blocks_found_24h"],
|
||||||
|
luck=response["luck"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== GPU Management ====================
|
||||||
|
|
||||||
|
async def list_devices(self) -> List[MiningDevice]:
|
||||||
|
"""List all mining devices."""
|
||||||
|
response = await self._get("/devices")
|
||||||
|
return [self._parse_device(d) for d in response.get("devices", [])]
|
||||||
|
|
||||||
|
async def get_device(self, device_id: str) -> MiningDevice:
|
||||||
|
"""Get device details."""
|
||||||
|
response = await self._get(f"/devices/{quote(device_id)}")
|
||||||
|
return self._parse_device(response)
|
||||||
|
|
||||||
|
async def set_device_config(
|
||||||
|
self, device_id: str, config: DeviceConfig
|
||||||
|
) -> MiningDevice:
|
||||||
|
"""Set device configuration."""
|
||||||
|
body: Dict[str, Any] = {"enabled": config.enabled}
|
||||||
|
if config.intensity is not None:
|
||||||
|
body["intensity"] = config.intensity
|
||||||
|
if config.power_limit is not None:
|
||||||
|
body["power_limit"] = config.power_limit
|
||||||
|
if config.core_clock_offset is not None:
|
||||||
|
body["core_clock_offset"] = config.core_clock_offset
|
||||||
|
if config.memory_clock_offset is not None:
|
||||||
|
body["memory_clock_offset"] = config.memory_clock_offset
|
||||||
|
if config.fan_speed is not None:
|
||||||
|
body["fan_speed"] = config.fan_speed
|
||||||
|
response = await self._post(f"/devices/{quote(device_id)}/config", body)
|
||||||
|
return self._parse_device(response)
|
||||||
|
|
||||||
|
async def enable_device(self, device_id: str) -> MiningDevice:
|
||||||
|
"""Enable a device for mining."""
|
||||||
|
response = await self._post(f"/devices/{quote(device_id)}/enable", {})
|
||||||
|
return self._parse_device(response)
|
||||||
|
|
||||||
|
async def disable_device(self, device_id: str) -> MiningDevice:
|
||||||
|
"""Disable a device."""
|
||||||
|
response = await self._post(f"/devices/{quote(device_id)}/disable", {})
|
||||||
|
return self._parse_device(response)
|
||||||
|
|
||||||
|
async def reset_device(self, device_id: str) -> MiningDevice:
|
||||||
|
"""Reset device to default settings."""
|
||||||
|
response = await self._post(f"/devices/{quote(device_id)}/reset", {})
|
||||||
|
return self._parse_device(response)
|
||||||
|
|
||||||
|
# ==================== Workers ====================
|
||||||
|
|
||||||
|
async def list_workers(self) -> List[WorkerInfo]:
|
||||||
|
"""List all workers."""
|
||||||
|
response = await self._get("/workers")
|
||||||
|
return [self._parse_worker(w) for w in response.get("workers", [])]
|
||||||
|
|
||||||
|
async def get_worker(self, worker_id: str) -> WorkerInfo:
|
||||||
|
"""Get worker details."""
|
||||||
|
response = await self._get(f"/workers/{quote(worker_id)}")
|
||||||
|
return self._parse_worker(response)
|
||||||
|
|
||||||
|
async def create_worker(self, name: str) -> WorkerInfo:
|
||||||
|
"""Create a new worker."""
|
||||||
|
response = await self._post("/workers", {"name": name})
|
||||||
|
return self._parse_worker(response)
|
||||||
|
|
||||||
|
async def delete_worker(self, worker_id: str) -> None:
|
||||||
|
"""Delete a worker."""
|
||||||
|
await self._delete(f"/workers/{quote(worker_id)}")
|
||||||
|
|
||||||
|
# ==================== Algorithms ====================
|
||||||
|
|
||||||
|
async def get_supported_algorithms(self) -> List[MiningAlgorithm]:
|
||||||
|
"""Get supported mining algorithms."""
|
||||||
|
response = await self._get("/algorithms")
|
||||||
|
return [self._parse_algorithm(a) for a in response.get("algorithms", [])]
|
||||||
|
|
||||||
|
async def get_current_algorithm(self) -> MiningAlgorithm:
|
||||||
|
"""Get current algorithm."""
|
||||||
|
response = await self._get("/algorithms/current")
|
||||||
|
return self._parse_algorithm(response)
|
||||||
|
|
||||||
|
async def switch_algorithm(self, algorithm: str) -> Dict[str, bool]:
|
||||||
|
"""Switch to a different algorithm."""
|
||||||
|
return await self._post("/algorithms/switch", {"algorithm": algorithm})
|
||||||
|
|
||||||
|
# ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
async def health_check(self) -> bool:
|
||||||
|
"""Health check."""
|
||||||
|
try:
|
||||||
|
response = await self._get("/health")
|
||||||
|
return response.get("status") == "healthy"
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self) -> bool:
|
||||||
|
"""Check if client is closed."""
|
||||||
|
return self._closed
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""Close the client."""
|
||||||
|
if self._active_connection:
|
||||||
|
await self.disconnect()
|
||||||
|
self._closed = True
|
||||||
|
await self._client.aclose()
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "SynorMining":
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args) -> None:
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
# ==================== Private Methods ====================
|
||||||
|
|
||||||
|
async def _get(self, path: str) -> Dict[str, Any]:
|
||||||
|
return await self._execute(lambda: self._client.get(path))
|
||||||
|
|
||||||
|
async def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
return await self._execute(lambda: self._client.post(path, json=body))
|
||||||
|
|
||||||
|
async def _delete(self, path: str) -> None:
|
||||||
|
await self._execute(lambda: self._client.delete(path))
|
||||||
|
|
||||||
|
async def _execute(self, operation) -> Dict[str, Any]:
|
||||||
|
if self._closed:
|
||||||
|
raise MiningError("Client has been closed")
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for attempt in range(self._config.retries):
|
||||||
|
try:
|
||||||
|
response = await operation()
|
||||||
|
self._ensure_success(response)
|
||||||
|
if response.content:
|
||||||
|
return response.json()
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
if self._config.debug:
|
||||||
|
print(f"Attempt {attempt + 1} failed: {e}")
|
||||||
|
if attempt < self._config.retries - 1:
|
||||||
|
await asyncio.sleep(2**attempt)
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
def _ensure_success(self, response: httpx.Response) -> None:
|
||||||
|
if response.status_code >= 400:
|
||||||
|
message = f"HTTP {response.status_code}"
|
||||||
|
code = None
|
||||||
|
try:
|
||||||
|
error = response.json()
|
||||||
|
message = error.get("message", message)
|
||||||
|
code = error.get("code")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise MiningError(message, code, response.status_code)
|
||||||
|
|
||||||
|
# ==================== Parsing Methods ====================
|
||||||
|
|
||||||
|
def _parse_connection(self, data: Dict[str, Any]) -> StratumConnection:
|
||||||
|
return StratumConnection(
|
||||||
|
id=data["id"],
|
||||||
|
pool=data["pool"],
|
||||||
|
status=ConnectionStatus(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.get("last_share_at"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_block_template(self, data: Dict[str, Any]) -> BlockTemplate:
|
||||||
|
return BlockTemplate(
|
||||||
|
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=[
|
||||||
|
TemplateTransaction(
|
||||||
|
txid=t["txid"],
|
||||||
|
data=t["data"],
|
||||||
|
fee=t["fee"],
|
||||||
|
weight=t["weight"],
|
||||||
|
)
|
||||||
|
for t in data.get("transactions", [])
|
||||||
|
],
|
||||||
|
target=data["target"],
|
||||||
|
algorithm=data["algorithm"],
|
||||||
|
extra_nonce=data["extra_nonce"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_submit_result(self, data: Dict[str, Any]) -> SubmitResult:
|
||||||
|
return SubmitResult(
|
||||||
|
status=SubmitResultStatus(data["status"]),
|
||||||
|
share=ShareInfo(
|
||||||
|
hash=data["share"]["hash"],
|
||||||
|
difficulty=data["share"]["difficulty"],
|
||||||
|
timestamp=data["share"]["timestamp"],
|
||||||
|
accepted=data["share"]["accepted"],
|
||||||
|
),
|
||||||
|
block_found=data["block_found"],
|
||||||
|
reason=data.get("reason"),
|
||||||
|
block_hash=data.get("block_hash"),
|
||||||
|
reward=data.get("reward"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_hashrate(self, data: Dict[str, Any]) -> Hashrate:
|
||||||
|
return Hashrate(
|
||||||
|
current=data["current"],
|
||||||
|
average_1h=data["average_1h"],
|
||||||
|
average_24h=data["average_24h"],
|
||||||
|
peak=data["peak"],
|
||||||
|
unit=data["unit"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_mining_stats(self, data: Dict[str, Any]) -> MiningStats:
|
||||||
|
temperature = None
|
||||||
|
if data.get("temperature"):
|
||||||
|
temperature = DeviceTemperature(
|
||||||
|
current=data["temperature"]["current"],
|
||||||
|
max=data["temperature"]["max"],
|
||||||
|
throttling=data["temperature"]["throttling"],
|
||||||
|
)
|
||||||
|
return MiningStats(
|
||||||
|
hashrate=self._parse_hashrate(data["hashrate"]),
|
||||||
|
shares=ShareStats(
|
||||||
|
accepted=data["shares"]["accepted"],
|
||||||
|
rejected=data["shares"]["rejected"],
|
||||||
|
stale=data["shares"]["stale"],
|
||||||
|
total=data["shares"]["total"],
|
||||||
|
accept_rate=data["shares"]["accept_rate"],
|
||||||
|
),
|
||||||
|
uptime=data["uptime"],
|
||||||
|
efficiency=data["efficiency"],
|
||||||
|
power_consumption=data.get("power_consumption"),
|
||||||
|
temperature=temperature,
|
||||||
|
earnings=EarningsSnapshot(
|
||||||
|
today=data["earnings"]["today"],
|
||||||
|
yesterday=data["earnings"]["yesterday"],
|
||||||
|
this_week=data["earnings"]["this_week"],
|
||||||
|
this_month=data["earnings"]["this_month"],
|
||||||
|
total=data["earnings"]["total"],
|
||||||
|
currency=data["earnings"]["currency"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_earnings(self, data: Dict[str, Any]) -> Earnings:
|
||||||
|
return Earnings(
|
||||||
|
period=TimePeriod(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=[
|
||||||
|
EarningsBreakdown(
|
||||||
|
date=b["date"],
|
||||||
|
amount=b["amount"],
|
||||||
|
blocks=b["blocks"],
|
||||||
|
shares=b["shares"],
|
||||||
|
hashrate=b["hashrate"],
|
||||||
|
)
|
||||||
|
for b in data.get("breakdown", [])
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_device(self, data: Dict[str, Any]) -> MiningDevice:
|
||||||
|
return MiningDevice(
|
||||||
|
id=data["id"],
|
||||||
|
name=data["name"],
|
||||||
|
type=DeviceType(data["type"]),
|
||||||
|
status=DeviceStatus(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.get("driver"),
|
||||||
|
firmware=data.get("firmware"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_worker(self, data: Dict[str, Any]) -> WorkerInfo:
|
||||||
|
return WorkerInfo(
|
||||||
|
id=data["id"],
|
||||||
|
name=data["name"],
|
||||||
|
status=ConnectionStatus(data["status"]),
|
||||||
|
hashrate=self._parse_hashrate(data["hashrate"]),
|
||||||
|
shares=ShareStats(
|
||||||
|
accepted=data["shares"]["accepted"],
|
||||||
|
rejected=data["shares"]["rejected"],
|
||||||
|
stale=data["shares"]["stale"],
|
||||||
|
total=data["shares"]["total"],
|
||||||
|
accept_rate=data["shares"]["accept_rate"],
|
||||||
|
),
|
||||||
|
devices=[self._parse_device(d) for d in data.get("devices", [])],
|
||||||
|
last_seen=data["last_seen"],
|
||||||
|
uptime=data["uptime"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_algorithm(self, data: Dict[str, Any]) -> MiningAlgorithm:
|
||||||
|
return MiningAlgorithm(
|
||||||
|
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"],
|
||||||
|
)
|
||||||
277
sdk/python/src/synor_mining/types.py
Normal file
277
sdk/python/src/synor_mining/types.py
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
"""
|
||||||
|
Synor Mining SDK Types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceType(str, Enum):
|
||||||
|
"""Device type."""
|
||||||
|
CPU = "cpu"
|
||||||
|
GPU_NVIDIA = "gpu_nvidia"
|
||||||
|
GPU_AMD = "gpu_amd"
|
||||||
|
ASIC = "asic"
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceStatus(str, Enum):
|
||||||
|
"""Device status."""
|
||||||
|
IDLE = "idle"
|
||||||
|
MINING = "mining"
|
||||||
|
ERROR = "error"
|
||||||
|
OFFLINE = "offline"
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionStatus(str, Enum):
|
||||||
|
"""Connection status."""
|
||||||
|
DISCONNECTED = "disconnected"
|
||||||
|
CONNECTING = "connecting"
|
||||||
|
CONNECTED = "connected"
|
||||||
|
RECONNECTING = "reconnecting"
|
||||||
|
|
||||||
|
|
||||||
|
class TimePeriod(str, Enum):
|
||||||
|
"""Time period for stats."""
|
||||||
|
HOUR = "hour"
|
||||||
|
DAY = "day"
|
||||||
|
WEEK = "week"
|
||||||
|
MONTH = "month"
|
||||||
|
ALL = "all"
|
||||||
|
|
||||||
|
|
||||||
|
class SubmitResultStatus(str, Enum):
|
||||||
|
"""Submit result status."""
|
||||||
|
ACCEPTED = "accepted"
|
||||||
|
REJECTED = "rejected"
|
||||||
|
STALE = "stale"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PoolConfig:
|
||||||
|
"""Pool configuration."""
|
||||||
|
url: str
|
||||||
|
user: str
|
||||||
|
password: Optional[str] = None
|
||||||
|
algorithm: Optional[str] = None
|
||||||
|
difficulty: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StratumConnection:
|
||||||
|
"""Stratum connection."""
|
||||||
|
id: str
|
||||||
|
pool: str
|
||||||
|
status: ConnectionStatus
|
||||||
|
algorithm: str
|
||||||
|
difficulty: float
|
||||||
|
connected_at: int
|
||||||
|
accepted_shares: int
|
||||||
|
rejected_shares: int
|
||||||
|
stale_shares: int
|
||||||
|
last_share_at: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TemplateTransaction:
|
||||||
|
"""Template transaction."""
|
||||||
|
txid: str
|
||||||
|
data: str
|
||||||
|
fee: str
|
||||||
|
weight: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlockTemplate:
|
||||||
|
"""Block template."""
|
||||||
|
id: str
|
||||||
|
previous_block_hash: str
|
||||||
|
merkle_root: str
|
||||||
|
timestamp: int
|
||||||
|
bits: str
|
||||||
|
height: int
|
||||||
|
coinbase_value: str
|
||||||
|
transactions: List[TemplateTransaction]
|
||||||
|
target: str
|
||||||
|
algorithm: str
|
||||||
|
extra_nonce: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MinedWork:
|
||||||
|
"""Mined work to submit."""
|
||||||
|
template_id: str
|
||||||
|
nonce: str
|
||||||
|
extra_nonce: str
|
||||||
|
timestamp: int
|
||||||
|
hash: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ShareInfo:
|
||||||
|
"""Share info."""
|
||||||
|
hash: str
|
||||||
|
difficulty: float
|
||||||
|
timestamp: int
|
||||||
|
accepted: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SubmitResult:
|
||||||
|
"""Submit result."""
|
||||||
|
status: SubmitResultStatus
|
||||||
|
share: ShareInfo
|
||||||
|
block_found: bool
|
||||||
|
reason: Optional[str] = None
|
||||||
|
block_hash: Optional[str] = None
|
||||||
|
reward: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Hashrate:
|
||||||
|
"""Hashrate."""
|
||||||
|
current: float
|
||||||
|
average_1h: float
|
||||||
|
average_24h: float
|
||||||
|
peak: float
|
||||||
|
unit: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ShareStats:
|
||||||
|
"""Share stats."""
|
||||||
|
accepted: int
|
||||||
|
rejected: int
|
||||||
|
stale: int
|
||||||
|
total: int
|
||||||
|
accept_rate: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceTemperature:
|
||||||
|
"""Device temperature."""
|
||||||
|
current: float
|
||||||
|
max: float
|
||||||
|
throttling: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EarningsSnapshot:
|
||||||
|
"""Earnings snapshot."""
|
||||||
|
today: str
|
||||||
|
yesterday: str
|
||||||
|
this_week: str
|
||||||
|
this_month: str
|
||||||
|
total: str
|
||||||
|
currency: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MiningStats:
|
||||||
|
"""Mining stats."""
|
||||||
|
hashrate: Hashrate
|
||||||
|
shares: ShareStats
|
||||||
|
uptime: int
|
||||||
|
efficiency: float
|
||||||
|
earnings: EarningsSnapshot
|
||||||
|
power_consumption: Optional[float] = None
|
||||||
|
temperature: Optional[DeviceTemperature] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EarningsBreakdown:
|
||||||
|
"""Earnings breakdown."""
|
||||||
|
date: int
|
||||||
|
amount: str
|
||||||
|
blocks: int
|
||||||
|
shares: int
|
||||||
|
hashrate: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Earnings:
|
||||||
|
"""Detailed earnings."""
|
||||||
|
period: TimePeriod
|
||||||
|
start_date: int
|
||||||
|
end_date: int
|
||||||
|
amount: str
|
||||||
|
blocks: int
|
||||||
|
shares: int
|
||||||
|
average_hashrate: float
|
||||||
|
currency: str
|
||||||
|
breakdown: List[EarningsBreakdown]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MiningDevice:
|
||||||
|
"""Mining device."""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
type: DeviceType
|
||||||
|
status: DeviceStatus
|
||||||
|
hashrate: float
|
||||||
|
temperature: float
|
||||||
|
fan_speed: float
|
||||||
|
power_draw: float
|
||||||
|
memory_used: int
|
||||||
|
memory_total: int
|
||||||
|
driver: Optional[str] = None
|
||||||
|
firmware: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceConfig:
|
||||||
|
"""Device configuration."""
|
||||||
|
enabled: bool
|
||||||
|
intensity: Optional[int] = None
|
||||||
|
power_limit: Optional[int] = None
|
||||||
|
core_clock_offset: Optional[int] = None
|
||||||
|
memory_clock_offset: Optional[int] = None
|
||||||
|
fan_speed: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorkerInfo:
|
||||||
|
"""Worker info."""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
status: ConnectionStatus
|
||||||
|
hashrate: Hashrate
|
||||||
|
shares: ShareStats
|
||||||
|
devices: List[MiningDevice]
|
||||||
|
last_seen: int
|
||||||
|
uptime: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PoolStats:
|
||||||
|
"""Pool stats."""
|
||||||
|
url: str
|
||||||
|
workers: int
|
||||||
|
hashrate: float
|
||||||
|
difficulty: float
|
||||||
|
last_block: int
|
||||||
|
blocks_found_24h: int
|
||||||
|
luck: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MiningAlgorithm:
|
||||||
|
"""Mining algorithm."""
|
||||||
|
name: str
|
||||||
|
display_name: str
|
||||||
|
hash_unit: str
|
||||||
|
profitability: str
|
||||||
|
difficulty: float
|
||||||
|
block_reward: str
|
||||||
|
block_time: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MiningConfig:
|
||||||
|
"""Mining SDK configuration."""
|
||||||
|
api_key: str
|
||||||
|
endpoint: str = "https://mining.synor.io/v1"
|
||||||
|
timeout: float = 30.0
|
||||||
|
retries: int = 3
|
||||||
|
debug: bool = False
|
||||||
809
sdk/rust/src/economics/mod.rs
Normal file
809
sdk/rust/src/economics/mod.rs
Normal file
|
|
@ -0,0 +1,809 @@
|
||||||
|
//! Synor Economics SDK for Rust.
|
||||||
|
//! Pricing, billing, staking, and discount management.
|
||||||
|
|
||||||
|
use reqwest::{Client, StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT: &str = "https://economics.synor.io/v1";
|
||||||
|
const DEFAULT_TIMEOUT: u64 = 30;
|
||||||
|
const DEFAULT_RETRIES: u32 = 3;
|
||||||
|
|
||||||
|
/// Service type for pricing.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ServiceType {
|
||||||
|
Compute,
|
||||||
|
Storage,
|
||||||
|
Database,
|
||||||
|
Hosting,
|
||||||
|
Bridge,
|
||||||
|
Rpc,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Billing period.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum BillingPeriod {
|
||||||
|
Hourly,
|
||||||
|
Daily,
|
||||||
|
Weekly,
|
||||||
|
Monthly,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stake status.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum StakeStatus {
|
||||||
|
Active,
|
||||||
|
Unstaking,
|
||||||
|
Withdrawn,
|
||||||
|
Slashed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discount type.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DiscountType {
|
||||||
|
Percentage,
|
||||||
|
Fixed,
|
||||||
|
Volume,
|
||||||
|
Referral,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoice status.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum InvoiceStatus {
|
||||||
|
Pending,
|
||||||
|
Paid,
|
||||||
|
Overdue,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Economics SDK configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EconomicsConfig {
|
||||||
|
pub api_key: String,
|
||||||
|
pub endpoint: String,
|
||||||
|
pub timeout: Duration,
|
||||||
|
pub retries: u32,
|
||||||
|
pub debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EconomicsConfig {
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
api_key: api_key.into(),
|
||||||
|
endpoint: DEFAULT_ENDPOINT.to_string(),
|
||||||
|
timeout: Duration::from_secs(DEFAULT_TIMEOUT),
|
||||||
|
retries: DEFAULT_RETRIES,
|
||||||
|
debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
|
||||||
|
self.endpoint = endpoint.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.timeout = timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_retries(mut self, retries: u32) -> Self {
|
||||||
|
self.retries = retries;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_debug(mut self, debug: bool) -> Self {
|
||||||
|
self.debug = debug;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage metrics for pricing.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct UsageMetrics {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub compute_hours: Option<f64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub storage_bytes: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub database_ops: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub hosting_requests: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bridge_transfers: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub rpc_calls: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service usage in a plan.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServiceUsage {
|
||||||
|
pub service: ServiceType,
|
||||||
|
pub metrics: UsageMetrics,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tier: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage plan for cost estimation.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UsagePlan {
|
||||||
|
pub services: Vec<ServiceUsage>,
|
||||||
|
pub period: BillingPeriod,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub start_date: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub end_date: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applied discount.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AppliedDiscount {
|
||||||
|
pub code: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub discount_type: DiscountType,
|
||||||
|
pub value: String,
|
||||||
|
pub savings: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tax amount.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TaxAmount {
|
||||||
|
pub name: String,
|
||||||
|
pub rate: f64,
|
||||||
|
pub amount: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Price breakdown.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Price {
|
||||||
|
pub service: ServiceType,
|
||||||
|
pub base_price: String,
|
||||||
|
pub quantity: String,
|
||||||
|
pub unit: String,
|
||||||
|
pub subtotal: String,
|
||||||
|
pub discounts: Vec<AppliedDiscount>,
|
||||||
|
pub total: String,
|
||||||
|
pub currency: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cost estimate for a usage plan.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CostEstimate {
|
||||||
|
pub services: Vec<Price>,
|
||||||
|
pub subtotal: String,
|
||||||
|
pub discounts: Vec<AppliedDiscount>,
|
||||||
|
pub taxes: Vec<TaxAmount>,
|
||||||
|
pub total: String,
|
||||||
|
pub currency: String,
|
||||||
|
pub valid_until: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage detail.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UsageDetail {
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub operation: String,
|
||||||
|
pub quantity: f64,
|
||||||
|
pub unit: String,
|
||||||
|
pub cost: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service usage record.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServiceUsageRecord {
|
||||||
|
pub service: ServiceType,
|
||||||
|
pub metrics: UsageMetrics,
|
||||||
|
pub cost: String,
|
||||||
|
pub details: Vec<UsageDetail>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage record.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Usage {
|
||||||
|
pub period: BillingPeriod,
|
||||||
|
pub start_date: i64,
|
||||||
|
pub end_date: i64,
|
||||||
|
pub services: Vec<ServiceUsageRecord>,
|
||||||
|
pub total_cost: String,
|
||||||
|
pub currency: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoice item.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct InvoiceItem {
|
||||||
|
pub service: ServiceType,
|
||||||
|
pub description: String,
|
||||||
|
pub quantity: String,
|
||||||
|
pub unit_price: String,
|
||||||
|
pub total: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoice.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Invoice {
|
||||||
|
pub id: String,
|
||||||
|
pub status: InvoiceStatus,
|
||||||
|
pub period: BillingPeriod,
|
||||||
|
pub start_date: i64,
|
||||||
|
pub end_date: i64,
|
||||||
|
pub items: Vec<InvoiceItem>,
|
||||||
|
pub subtotal: String,
|
||||||
|
pub discounts: Vec<AppliedDiscount>,
|
||||||
|
pub taxes: Vec<TaxAmount>,
|
||||||
|
pub total: String,
|
||||||
|
pub currency: String,
|
||||||
|
pub due_date: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub paid_at: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub payment_method: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Account balance.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AccountBalance {
|
||||||
|
pub available: String,
|
||||||
|
pub pending: String,
|
||||||
|
pub reserved: String,
|
||||||
|
pub total: String,
|
||||||
|
pub currency: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub credit_limit: Option<String>,
|
||||||
|
pub last_updated: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stake receipt.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StakeReceipt {
|
||||||
|
pub id: String,
|
||||||
|
pub amount: String,
|
||||||
|
pub lock_duration: i32,
|
||||||
|
pub start_date: i64,
|
||||||
|
pub end_date: i64,
|
||||||
|
pub apy: String,
|
||||||
|
pub estimated_rewards: String,
|
||||||
|
pub tx_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unstake receipt.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UnstakeReceipt {
|
||||||
|
pub id: String,
|
||||||
|
pub stake_id: String,
|
||||||
|
pub amount: String,
|
||||||
|
pub rewards: String,
|
||||||
|
pub total: String,
|
||||||
|
pub unbonding_period: i32,
|
||||||
|
pub available_at: i64,
|
||||||
|
pub tx_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stake info.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StakeInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub amount: String,
|
||||||
|
pub status: StakeStatus,
|
||||||
|
pub lock_duration: i32,
|
||||||
|
pub start_date: i64,
|
||||||
|
pub end_date: i64,
|
||||||
|
pub apy: String,
|
||||||
|
pub earned_rewards: String,
|
||||||
|
pub pending_rewards: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub validator: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stake reward detail.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StakeRewardDetail {
|
||||||
|
pub stake_id: String,
|
||||||
|
pub earned: String,
|
||||||
|
pub pending: String,
|
||||||
|
pub apy: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Staking rewards.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StakingRewards {
|
||||||
|
pub total_earned: String,
|
||||||
|
pub pending: String,
|
||||||
|
pub claimed: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_claim_date: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub next_claim_available: Option<i64>,
|
||||||
|
pub stakes: Vec<StakeRewardDetail>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discount.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Discount {
|
||||||
|
pub code: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub discount_type: DiscountType,
|
||||||
|
pub value: String,
|
||||||
|
pub description: String,
|
||||||
|
pub valid_from: i64,
|
||||||
|
pub valid_until: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub min_purchase: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_discount: Option<String>,
|
||||||
|
pub applicable_services: Vec<ServiceType>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub usage_limit: Option<i32>,
|
||||||
|
pub used_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stake options.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize)]
|
||||||
|
pub struct StakeOptions {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub validator: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auto_compound: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub lock_duration: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Staking APY response.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StakingApy {
|
||||||
|
pub apy: String,
|
||||||
|
pub min_lock_duration: i32,
|
||||||
|
pub max_lock_duration: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Economics SDK error.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum EconomicsError {
|
||||||
|
#[error("HTTP error: {message}")]
|
||||||
|
Http {
|
||||||
|
message: String,
|
||||||
|
code: Option<String>,
|
||||||
|
status_code: u16,
|
||||||
|
},
|
||||||
|
#[error("Client closed")]
|
||||||
|
ClientClosed,
|
||||||
|
#[error("Request error: {0}")]
|
||||||
|
Request(#[from] reqwest::Error),
|
||||||
|
#[error("JSON error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synor Economics SDK client.
|
||||||
|
pub struct SynorEconomics {
|
||||||
|
config: EconomicsConfig,
|
||||||
|
client: Client,
|
||||||
|
closed: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SynorEconomics {
|
||||||
|
/// Create a new Economics client.
|
||||||
|
pub fn new(config: EconomicsConfig) -> Result<Self, EconomicsError> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(config.timeout)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
config,
|
||||||
|
client,
|
||||||
|
closed: Arc::new(AtomicBool::new(false)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Pricing Operations ====================
|
||||||
|
|
||||||
|
/// Get price for a service based on usage.
|
||||||
|
pub async fn get_price(
|
||||||
|
&self,
|
||||||
|
service: ServiceType,
|
||||||
|
usage: UsageMetrics,
|
||||||
|
) -> Result<Price, EconomicsError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
service: ServiceType,
|
||||||
|
usage: UsageMetrics,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
price: Price,
|
||||||
|
}
|
||||||
|
let resp: Response = self
|
||||||
|
.post("/pricing/calculate", &Request { service, usage })
|
||||||
|
.await?;
|
||||||
|
Ok(resp.price)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimate cost for a usage plan.
|
||||||
|
pub async fn estimate_cost(&self, plan: UsagePlan) -> Result<CostEstimate, EconomicsError> {
|
||||||
|
self.post("/pricing/estimate", &plan).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pricing tiers for a service.
|
||||||
|
pub async fn get_pricing_tiers(
|
||||||
|
&self,
|
||||||
|
service: ServiceType,
|
||||||
|
) -> Result<Vec<Price>, EconomicsError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
tiers: Vec<Price>,
|
||||||
|
}
|
||||||
|
let resp: Response = self
|
||||||
|
.get(&format!("/pricing/{:?}/tiers", service).to_lowercase())
|
||||||
|
.await?;
|
||||||
|
Ok(resp.tiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Usage & Billing Operations ====================
|
||||||
|
|
||||||
|
/// Get usage for a billing period.
|
||||||
|
pub async fn get_usage(
|
||||||
|
&self,
|
||||||
|
period: Option<BillingPeriod>,
|
||||||
|
) -> Result<Usage, EconomicsError> {
|
||||||
|
let path = match period {
|
||||||
|
Some(p) => format!("/usage?period={:?}", p).to_lowercase(),
|
||||||
|
None => "/usage".to_string(),
|
||||||
|
};
|
||||||
|
self.get(&path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get usage history.
|
||||||
|
pub async fn get_usage_history(
|
||||||
|
&self,
|
||||||
|
limit: Option<i32>,
|
||||||
|
offset: Option<i32>,
|
||||||
|
) -> Result<Vec<Usage>, EconomicsError> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
if let Some(l) = limit {
|
||||||
|
params.push(format!("limit={}", l));
|
||||||
|
}
|
||||||
|
if let Some(o) = offset {
|
||||||
|
params.push(format!("offset={}", o));
|
||||||
|
}
|
||||||
|
let path = if params.is_empty() {
|
||||||
|
"/usage/history".to_string()
|
||||||
|
} else {
|
||||||
|
format!("/usage/history?{}", params.join("&"))
|
||||||
|
};
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
usage: Vec<Usage>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get(&path).await?;
|
||||||
|
Ok(resp.usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get invoices.
|
||||||
|
pub async fn get_invoices(&self) -> Result<Vec<Invoice>, EconomicsError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
invoices: Vec<Invoice>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get("/invoices").await?;
|
||||||
|
Ok(resp.invoices)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific invoice.
|
||||||
|
pub async fn get_invoice(&self, invoice_id: &str) -> Result<Invoice, EconomicsError> {
|
||||||
|
self.get(&format!("/invoices/{}", urlencoding::encode(invoice_id)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pay an invoice.
|
||||||
|
pub async fn pay_invoice(
|
||||||
|
&self,
|
||||||
|
invoice_id: &str,
|
||||||
|
payment_method: Option<&str>,
|
||||||
|
) -> Result<Invoice, EconomicsError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
payment_method: Option<String>,
|
||||||
|
}
|
||||||
|
self.post(
|
||||||
|
&format!("/invoices/{}/pay", urlencoding::encode(invoice_id)),
|
||||||
|
&Request {
|
||||||
|
payment_method: payment_method.map(String::from),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get account balance.
|
||||||
|
pub async fn get_balance(&self) -> Result<AccountBalance, EconomicsError> {
|
||||||
|
self.get("/balance").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add funds to account.
|
||||||
|
pub async fn add_funds(
|
||||||
|
&self,
|
||||||
|
amount: &str,
|
||||||
|
payment_method: &str,
|
||||||
|
) -> Result<AccountBalance, EconomicsError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
amount: String,
|
||||||
|
payment_method: String,
|
||||||
|
}
|
||||||
|
self.post(
|
||||||
|
"/balance/deposit",
|
||||||
|
&Request {
|
||||||
|
amount: amount.to_string(),
|
||||||
|
payment_method: payment_method.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Staking Operations ====================
|
||||||
|
|
||||||
|
/// Stake tokens.
|
||||||
|
pub async fn stake(
|
||||||
|
&self,
|
||||||
|
amount: &str,
|
||||||
|
options: Option<StakeOptions>,
|
||||||
|
) -> Result<StakeReceipt, EconomicsError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
amount: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
options: Option<StakeOptions>,
|
||||||
|
}
|
||||||
|
self.post(
|
||||||
|
"/staking/stake",
|
||||||
|
&Request {
|
||||||
|
amount: amount.to_string(),
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unstake tokens.
|
||||||
|
pub async fn unstake(&self, stake_id: &str) -> Result<UnstakeReceipt, EconomicsError> {
|
||||||
|
self.post::<(), UnstakeReceipt>(
|
||||||
|
&format!("/staking/stakes/{}/unstake", urlencoding::encode(stake_id)),
|
||||||
|
&(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get staking rewards.
|
||||||
|
pub async fn get_staking_rewards(&self) -> Result<StakingRewards, EconomicsError> {
|
||||||
|
self.get("/staking/rewards").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claim staking rewards.
|
||||||
|
pub async fn claim_rewards(
|
||||||
|
&self,
|
||||||
|
stake_id: Option<&str>,
|
||||||
|
) -> Result<StakingRewards, EconomicsError> {
|
||||||
|
let path = match stake_id {
|
||||||
|
Some(id) => format!("/staking/stakes/{}/claim", urlencoding::encode(id)),
|
||||||
|
None => "/staking/rewards/claim".to_string(),
|
||||||
|
};
|
||||||
|
self.post::<(), StakingRewards>(&path, &()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List active stakes.
|
||||||
|
pub async fn list_stakes(&self) -> Result<Vec<StakeInfo>, EconomicsError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
stakes: Vec<StakeInfo>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get("/staking/stakes").await?;
|
||||||
|
Ok(resp.stakes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get stake details.
|
||||||
|
pub async fn get_stake(&self, stake_id: &str) -> Result<StakeInfo, EconomicsError> {
|
||||||
|
self.get(&format!(
|
||||||
|
"/staking/stakes/{}",
|
||||||
|
urlencoding::encode(stake_id)
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current APY for staking.
|
||||||
|
pub async fn get_staking_apy(&self) -> Result<StakingApy, EconomicsError> {
|
||||||
|
self.get("/staking/apy").await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Discount Operations ====================
|
||||||
|
|
||||||
|
/// Apply a discount code.
|
||||||
|
pub async fn apply_discount(&self, code: &str) -> Result<Discount, EconomicsError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
self.post(
|
||||||
|
"/discounts/apply",
|
||||||
|
&Request {
|
||||||
|
code: code.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get available discounts.
|
||||||
|
pub async fn get_available_discounts(&self) -> Result<Vec<Discount>, EconomicsError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
discounts: Vec<Discount>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get("/discounts").await?;
|
||||||
|
Ok(resp.discounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get active discounts on account.
|
||||||
|
pub async fn get_active_discounts(&self) -> Result<Vec<Discount>, EconomicsError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
discounts: Vec<Discount>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get("/discounts/active").await?;
|
||||||
|
Ok(resp.discounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a discount.
|
||||||
|
pub async fn remove_discount(&self, code: &str) -> Result<(), EconomicsError> {
|
||||||
|
self.delete(&format!("/discounts/{}", urlencoding::encode(code)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
/// Health check.
|
||||||
|
pub async fn health_check(&self) -> bool {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
status: String,
|
||||||
|
}
|
||||||
|
match self.get::<Response>("/health").await {
|
||||||
|
Ok(resp) => resp.status == "healthy",
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if client is closed.
|
||||||
|
pub fn is_closed(&self) -> bool {
|
||||||
|
self.closed.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the client.
|
||||||
|
pub fn close(&self) {
|
||||||
|
self.closed.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Private Methods ====================
|
||||||
|
|
||||||
|
async fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, EconomicsError> {
|
||||||
|
self.execute(|| async {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(format!("{}{}", self.config.endpoint, path))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-SDK-Version", "rust/0.1.0")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
self.handle_response(resp).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post<B: Serialize, T: for<'de> Deserialize<'de>>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
body: &B,
|
||||||
|
) -> Result<T, EconomicsError> {
|
||||||
|
self.execute(|| async {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.post(format!("{}{}", self.config.endpoint, path))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-SDK-Version", "rust/0.1.0")
|
||||||
|
.json(body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
self.handle_response(resp).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, path: &str) -> Result<(), EconomicsError> {
|
||||||
|
self.execute(|| async {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.delete(format!("{}{}", self.config.endpoint, path))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("X-SDK-Version", "rust/0.1.0")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if resp.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(self.parse_error(resp).await)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute<F, Fut, T>(&self, operation: F) -> Result<T, EconomicsError>
|
||||||
|
where
|
||||||
|
F: Fn() -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<T, EconomicsError>>,
|
||||||
|
{
|
||||||
|
if self.closed.load(Ordering::SeqCst) {
|
||||||
|
return Err(EconomicsError::ClientClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_error = None;
|
||||||
|
for attempt in 0..self.config.retries {
|
||||||
|
match operation().await {
|
||||||
|
Ok(result) => return Ok(result),
|
||||||
|
Err(e) => {
|
||||||
|
if self.config.debug {
|
||||||
|
eprintln!("Attempt {} failed: {:?}", attempt + 1, e);
|
||||||
|
}
|
||||||
|
last_error = Some(e);
|
||||||
|
if attempt < self.config.retries - 1 {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(last_error.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_response<T: for<'de> Deserialize<'de>>(
|
||||||
|
&self,
|
||||||
|
resp: reqwest::Response,
|
||||||
|
) -> Result<T, EconomicsError> {
|
||||||
|
if resp.status().is_success() {
|
||||||
|
Ok(resp.json().await?)
|
||||||
|
} else {
|
||||||
|
Err(self.parse_error(resp).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_error(&self, resp: reqwest::Response) -> EconomicsError {
|
||||||
|
let status = resp.status().as_u16();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
message: Option<String>,
|
||||||
|
code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let (message, code) = match serde_json::from_str::<ErrorResponse>(&body) {
|
||||||
|
Ok(err) => (
|
||||||
|
err.message.unwrap_or_else(|| format!("HTTP {}", status)),
|
||||||
|
err.code,
|
||||||
|
),
|
||||||
|
Err(_) => (format!("HTTP {}", status), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
EconomicsError::Http {
|
||||||
|
message,
|
||||||
|
code,
|
||||||
|
status_code: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
796
sdk/rust/src/governance/mod.rs
Normal file
796
sdk/rust/src/governance/mod.rs
Normal file
|
|
@ -0,0 +1,796 @@
|
||||||
|
//! Synor Governance SDK for Rust.
|
||||||
|
//! Proposals, voting, DAOs, and vesting.
|
||||||
|
|
||||||
|
use reqwest::{Client, StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT: &str = "https://governance.synor.io/v1";
|
||||||
|
const DEFAULT_TIMEOUT: u64 = 30;
|
||||||
|
const DEFAULT_RETRIES: u32 = 3;
|
||||||
|
|
||||||
|
/// Proposal status.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ProposalStatus {
|
||||||
|
Draft,
|
||||||
|
Active,
|
||||||
|
Passed,
|
||||||
|
Rejected,
|
||||||
|
Executed,
|
||||||
|
Cancelled,
|
||||||
|
Expired,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vote choice.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum VoteChoice {
|
||||||
|
For,
|
||||||
|
Against,
|
||||||
|
Abstain,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proposal type.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ProposalType {
|
||||||
|
ParameterChange,
|
||||||
|
TreasurySpend,
|
||||||
|
Upgrade,
|
||||||
|
Text,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DAO type.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DaoType {
|
||||||
|
Token,
|
||||||
|
Multisig,
|
||||||
|
Hybrid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vesting status.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum VestingStatus {
|
||||||
|
Active,
|
||||||
|
Paused,
|
||||||
|
Completed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Governance SDK configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GovernanceConfig {
|
||||||
|
pub api_key: String,
|
||||||
|
pub endpoint: String,
|
||||||
|
pub timeout: Duration,
|
||||||
|
pub retries: u32,
|
||||||
|
pub debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GovernanceConfig {
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
api_key: api_key.into(),
|
||||||
|
endpoint: DEFAULT_ENDPOINT.to_string(),
|
||||||
|
timeout: Duration::from_secs(DEFAULT_TIMEOUT),
|
||||||
|
retries: DEFAULT_RETRIES,
|
||||||
|
debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
|
||||||
|
self.endpoint = endpoint.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proposal action.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProposalAction {
|
||||||
|
pub target: String,
|
||||||
|
pub value: String,
|
||||||
|
pub calldata: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proposal draft for creating proposals.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct ProposalDraft {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub proposal_type: ProposalType,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub actions: Vec<ProposalAction>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub start_time: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub end_time: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub quorum: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub threshold: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proposal.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Proposal {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub proposal_type: ProposalType,
|
||||||
|
pub status: ProposalStatus,
|
||||||
|
pub proposer: String,
|
||||||
|
pub actions: Vec<ProposalAction>,
|
||||||
|
pub start_time: i64,
|
||||||
|
pub end_time: i64,
|
||||||
|
pub quorum: String,
|
||||||
|
pub threshold: String,
|
||||||
|
pub for_votes: String,
|
||||||
|
pub against_votes: String,
|
||||||
|
pub abstain_votes: String,
|
||||||
|
pub total_votes: String,
|
||||||
|
pub created_at: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub executed_at: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub execution_tx_hash: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proposal filter.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ProposalFilter {
|
||||||
|
pub status: Option<ProposalStatus>,
|
||||||
|
pub proposal_type: Option<ProposalType>,
|
||||||
|
pub proposer: Option<String>,
|
||||||
|
pub limit: Option<i32>,
|
||||||
|
pub offset: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vote.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct Vote {
|
||||||
|
pub choice: VoteChoice,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vote receipt.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VoteReceipt {
|
||||||
|
pub proposal_id: String,
|
||||||
|
pub voter: String,
|
||||||
|
pub choice: VoteChoice,
|
||||||
|
pub weight: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reason: Option<String>,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub tx_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delegation info.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DelegationInfo {
|
||||||
|
pub address: String,
|
||||||
|
pub amount: String,
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delegation receipt.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DelegationReceipt {
|
||||||
|
pub delegator: String,
|
||||||
|
pub delegatee: String,
|
||||||
|
pub amount: String,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub tx_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Voting power.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VotingPower {
|
||||||
|
pub address: String,
|
||||||
|
pub own_power: String,
|
||||||
|
pub delegated_power: String,
|
||||||
|
pub total_power: String,
|
||||||
|
pub delegated_from: Vec<DelegationInfo>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub delegated_to: Option<DelegationInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DAO config for creating DAOs.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DaoConfig {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub dao_type: DaoType,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub token_address: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub signers: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub threshold: Option<i32>,
|
||||||
|
pub voting_period: i32,
|
||||||
|
pub quorum: String,
|
||||||
|
pub proposal_threshold: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timelock_delay: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DAO.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Dao {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub dao_type: DaoType,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub token_address: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub signers: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub threshold: Option<i32>,
|
||||||
|
pub voting_period: i32,
|
||||||
|
pub quorum: String,
|
||||||
|
pub proposal_threshold: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timelock_delay: Option<i32>,
|
||||||
|
pub treasury: String,
|
||||||
|
pub total_proposals: i32,
|
||||||
|
pub active_proposals: i32,
|
||||||
|
pub total_members: i32,
|
||||||
|
pub created_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vesting schedule for creating vesting contracts.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct VestingSchedule {
|
||||||
|
pub beneficiary: String,
|
||||||
|
pub total_amount: String,
|
||||||
|
pub start_time: i64,
|
||||||
|
pub cliff_duration: i32,
|
||||||
|
pub vesting_duration: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub revocable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vesting contract.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VestingContract {
|
||||||
|
pub id: String,
|
||||||
|
pub beneficiary: String,
|
||||||
|
pub total_amount: String,
|
||||||
|
pub released_amount: String,
|
||||||
|
pub vested_amount: String,
|
||||||
|
pub start_time: i64,
|
||||||
|
pub cliff_time: i64,
|
||||||
|
pub end_time: i64,
|
||||||
|
pub revocable: bool,
|
||||||
|
pub status: VestingStatus,
|
||||||
|
pub created_at: i64,
|
||||||
|
pub tx_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claim result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ClaimResult {
|
||||||
|
pub vesting_id: String,
|
||||||
|
pub amount: String,
|
||||||
|
pub recipient: String,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub tx_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Governance transaction.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GovernanceTransaction {
|
||||||
|
pub tx_hash: String,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub block_number: i64,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DAO member.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DaoMember {
|
||||||
|
pub address: String,
|
||||||
|
pub power: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claimable amount.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ClaimableAmount {
|
||||||
|
pub claimable: String,
|
||||||
|
pub vested: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Governance SDK error.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum GovernanceError {
|
||||||
|
#[error("HTTP error: {message}")]
|
||||||
|
Http {
|
||||||
|
message: String,
|
||||||
|
code: Option<String>,
|
||||||
|
status_code: u16,
|
||||||
|
},
|
||||||
|
#[error("Client closed")]
|
||||||
|
ClientClosed,
|
||||||
|
#[error("Request error: {0}")]
|
||||||
|
Request(#[from] reqwest::Error),
|
||||||
|
#[error("JSON error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synor Governance SDK client.
|
||||||
|
pub struct SynorGovernance {
|
||||||
|
config: GovernanceConfig,
|
||||||
|
client: Client,
|
||||||
|
closed: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SynorGovernance {
|
||||||
|
/// Create a new Governance client.
|
||||||
|
pub fn new(config: GovernanceConfig) -> Result<Self, GovernanceError> {
|
||||||
|
let client = Client::builder().timeout(config.timeout).build()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
config,
|
||||||
|
client,
|
||||||
|
closed: Arc::new(AtomicBool::new(false)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Proposal Operations ====================
|
||||||
|
|
||||||
|
/// Create a new proposal.
|
||||||
|
pub async fn create_proposal(&self, draft: ProposalDraft) -> Result<Proposal, GovernanceError> {
|
||||||
|
self.post("/proposals", &draft).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a proposal by ID.
|
||||||
|
pub async fn get_proposal(&self, proposal_id: &str) -> Result<Proposal, GovernanceError> {
|
||||||
|
self.get(&format!("/proposals/{}", urlencoding::encode(proposal_id)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List proposals with optional filtering.
|
||||||
|
pub async fn list_proposals(
|
||||||
|
&self,
|
||||||
|
filter: Option<ProposalFilter>,
|
||||||
|
) -> Result<Vec<Proposal>, GovernanceError> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
if let Some(f) = filter {
|
||||||
|
if let Some(s) = f.status {
|
||||||
|
params.push(format!("status={:?}", s).to_lowercase());
|
||||||
|
}
|
||||||
|
if let Some(t) = f.proposal_type {
|
||||||
|
params.push(format!("type={:?}", t).to_lowercase());
|
||||||
|
}
|
||||||
|
if let Some(p) = f.proposer {
|
||||||
|
params.push(format!("proposer={}", p));
|
||||||
|
}
|
||||||
|
if let Some(l) = f.limit {
|
||||||
|
params.push(format!("limit={}", l));
|
||||||
|
}
|
||||||
|
if let Some(o) = f.offset {
|
||||||
|
params.push(format!("offset={}", o));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let path = if params.is_empty() {
|
||||||
|
"/proposals".to_string()
|
||||||
|
} else {
|
||||||
|
format!("/proposals?{}", params.join("&"))
|
||||||
|
};
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
proposals: Vec<Proposal>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get(&path).await?;
|
||||||
|
Ok(resp.proposals)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel a proposal.
|
||||||
|
pub async fn cancel_proposal(&self, proposal_id: &str) -> Result<Proposal, GovernanceError> {
|
||||||
|
self.post::<(), Proposal>(
|
||||||
|
&format!("/proposals/{}/cancel", urlencoding::encode(proposal_id)),
|
||||||
|
&(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a passed proposal.
|
||||||
|
pub async fn execute_proposal(
|
||||||
|
&self,
|
||||||
|
proposal_id: &str,
|
||||||
|
) -> Result<GovernanceTransaction, GovernanceError> {
|
||||||
|
self.post::<(), GovernanceTransaction>(
|
||||||
|
&format!("/proposals/{}/execute", urlencoding::encode(proposal_id)),
|
||||||
|
&(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Voting Operations ====================
|
||||||
|
|
||||||
|
/// Vote on a proposal.
|
||||||
|
pub async fn vote(
|
||||||
|
&self,
|
||||||
|
proposal_id: &str,
|
||||||
|
vote: Vote,
|
||||||
|
weight: Option<&str>,
|
||||||
|
) -> Result<VoteReceipt, GovernanceError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct VoteRequest {
|
||||||
|
choice: VoteChoice,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
reason: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
weight: Option<String>,
|
||||||
|
}
|
||||||
|
let req = VoteRequest {
|
||||||
|
choice: vote.choice,
|
||||||
|
reason: vote.reason,
|
||||||
|
weight: weight.map(String::from),
|
||||||
|
};
|
||||||
|
self.post(
|
||||||
|
&format!("/proposals/{}/vote", urlencoding::encode(proposal_id)),
|
||||||
|
&req,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get votes for a proposal.
|
||||||
|
pub async fn get_votes(
|
||||||
|
&self,
|
||||||
|
proposal_id: &str,
|
||||||
|
limit: Option<i32>,
|
||||||
|
offset: Option<i32>,
|
||||||
|
) -> Result<Vec<VoteReceipt>, GovernanceError> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
if let Some(l) = limit {
|
||||||
|
params.push(format!("limit={}", l));
|
||||||
|
}
|
||||||
|
if let Some(o) = offset {
|
||||||
|
params.push(format!("offset={}", o));
|
||||||
|
}
|
||||||
|
let path = if params.is_empty() {
|
||||||
|
format!("/proposals/{}/votes", urlencoding::encode(proposal_id))
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"/proposals/{}/votes?{}",
|
||||||
|
urlencoding::encode(proposal_id),
|
||||||
|
params.join("&")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
votes: Vec<VoteReceipt>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get(&path).await?;
|
||||||
|
Ok(resp.votes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delegate voting power.
|
||||||
|
pub async fn delegate(
|
||||||
|
&self,
|
||||||
|
to: &str,
|
||||||
|
amount: Option<&str>,
|
||||||
|
) -> Result<DelegationReceipt, GovernanceError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
to: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
amount: Option<String>,
|
||||||
|
}
|
||||||
|
self.post(
|
||||||
|
"/voting/delegate",
|
||||||
|
&Request {
|
||||||
|
to: to.to_string(),
|
||||||
|
amount: amount.map(String::from),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Undelegate voting power.
|
||||||
|
pub async fn undelegate(
|
||||||
|
&self,
|
||||||
|
from: Option<&str>,
|
||||||
|
) -> Result<DelegationReceipt, GovernanceError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
from: Option<String>,
|
||||||
|
}
|
||||||
|
self.post(
|
||||||
|
"/voting/undelegate",
|
||||||
|
&Request {
|
||||||
|
from: from.map(String::from),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get voting power for an address.
|
||||||
|
pub async fn get_voting_power(&self, address: &str) -> Result<VotingPower, GovernanceError> {
|
||||||
|
self.get(&format!("/voting/power/{}", urlencoding::encode(address)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get voting power for the authenticated user.
|
||||||
|
pub async fn get_my_voting_power(&self) -> Result<VotingPower, GovernanceError> {
|
||||||
|
self.get("/voting/power").await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DAO Operations ====================
|
||||||
|
|
||||||
|
/// Create a new DAO.
|
||||||
|
pub async fn create_dao(&self, config: DaoConfig) -> Result<Dao, GovernanceError> {
|
||||||
|
self.post("/daos", &config).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a DAO by ID.
|
||||||
|
pub async fn get_dao(&self, dao_id: &str) -> Result<Dao, GovernanceError> {
|
||||||
|
self.get(&format!("/daos/{}", urlencoding::encode(dao_id)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List DAOs.
|
||||||
|
pub async fn list_daos(
|
||||||
|
&self,
|
||||||
|
limit: Option<i32>,
|
||||||
|
offset: Option<i32>,
|
||||||
|
) -> Result<Vec<Dao>, GovernanceError> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
if let Some(l) = limit {
|
||||||
|
params.push(format!("limit={}", l));
|
||||||
|
}
|
||||||
|
if let Some(o) = offset {
|
||||||
|
params.push(format!("offset={}", o));
|
||||||
|
}
|
||||||
|
let path = if params.is_empty() {
|
||||||
|
"/daos".to_string()
|
||||||
|
} else {
|
||||||
|
format!("/daos?{}", params.join("&"))
|
||||||
|
};
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
daos: Vec<Dao>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get(&path).await?;
|
||||||
|
Ok(resp.daos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get DAO members.
|
||||||
|
pub async fn get_dao_members(
|
||||||
|
&self,
|
||||||
|
dao_id: &str,
|
||||||
|
limit: Option<i32>,
|
||||||
|
offset: Option<i32>,
|
||||||
|
) -> Result<Vec<DaoMember>, GovernanceError> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
if let Some(l) = limit {
|
||||||
|
params.push(format!("limit={}", l));
|
||||||
|
}
|
||||||
|
if let Some(o) = offset {
|
||||||
|
params.push(format!("offset={}", o));
|
||||||
|
}
|
||||||
|
let path = if params.is_empty() {
|
||||||
|
format!("/daos/{}/members", urlencoding::encode(dao_id))
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"/daos/{}/members?{}",
|
||||||
|
urlencoding::encode(dao_id),
|
||||||
|
params.join("&")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
members: Vec<DaoMember>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get(&path).await?;
|
||||||
|
Ok(resp.members)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Vesting Operations ====================
|
||||||
|
|
||||||
|
/// Create a vesting schedule.
|
||||||
|
pub async fn create_vesting_schedule(
|
||||||
|
&self,
|
||||||
|
schedule: VestingSchedule,
|
||||||
|
) -> Result<VestingContract, GovernanceError> {
|
||||||
|
self.post("/vesting", &schedule).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a vesting contract.
|
||||||
|
pub async fn get_vesting_contract(
|
||||||
|
&self,
|
||||||
|
contract_id: &str,
|
||||||
|
) -> Result<VestingContract, GovernanceError> {
|
||||||
|
self.get(&format!("/vesting/{}", urlencoding::encode(contract_id)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List vesting contracts.
|
||||||
|
pub async fn list_vesting_contracts(
|
||||||
|
&self,
|
||||||
|
beneficiary: Option<&str>,
|
||||||
|
) -> Result<Vec<VestingContract>, GovernanceError> {
|
||||||
|
let path = match beneficiary {
|
||||||
|
Some(b) => format!("/vesting?beneficiary={}", urlencoding::encode(b)),
|
||||||
|
None => "/vesting".to_string(),
|
||||||
|
};
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
contracts: Vec<VestingContract>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get(&path).await?;
|
||||||
|
Ok(resp.contracts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claim vested tokens.
|
||||||
|
pub async fn claim_vested(&self, contract_id: &str) -> Result<ClaimResult, GovernanceError> {
|
||||||
|
self.post::<(), ClaimResult>(
|
||||||
|
&format!("/vesting/{}/claim", urlencoding::encode(contract_id)),
|
||||||
|
&(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Revoke a vesting contract.
|
||||||
|
pub async fn revoke_vesting(
|
||||||
|
&self,
|
||||||
|
contract_id: &str,
|
||||||
|
) -> Result<VestingContract, GovernanceError> {
|
||||||
|
self.post::<(), VestingContract>(
|
||||||
|
&format!("/vesting/{}/revoke", urlencoding::encode(contract_id)),
|
||||||
|
&(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get claimable amount for a vesting contract.
|
||||||
|
pub async fn get_claimable_amount(
|
||||||
|
&self,
|
||||||
|
contract_id: &str,
|
||||||
|
) -> Result<ClaimableAmount, GovernanceError> {
|
||||||
|
self.get(&format!(
|
||||||
|
"/vesting/{}/claimable",
|
||||||
|
urlencoding::encode(contract_id)
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
/// Health check.
|
||||||
|
pub async fn health_check(&self) -> bool {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
status: String,
|
||||||
|
}
|
||||||
|
match self.get::<Response>("/health").await {
|
||||||
|
Ok(resp) => resp.status == "healthy",
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if client is closed.
|
||||||
|
pub fn is_closed(&self) -> bool {
|
||||||
|
self.closed.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the client.
|
||||||
|
pub fn close(&self) {
|
||||||
|
self.closed.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Private Methods ====================
|
||||||
|
|
||||||
|
async fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, GovernanceError> {
|
||||||
|
self.execute(|| async {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(format!("{}{}", self.config.endpoint, path))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-SDK-Version", "rust/0.1.0")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
self.handle_response(resp).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post<B: Serialize, T: for<'de> Deserialize<'de>>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
body: &B,
|
||||||
|
) -> Result<T, GovernanceError> {
|
||||||
|
self.execute(|| async {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.post(format!("{}{}", self.config.endpoint, path))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-SDK-Version", "rust/0.1.0")
|
||||||
|
.json(body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
self.handle_response(resp).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute<F, Fut, T>(&self, operation: F) -> Result<T, GovernanceError>
|
||||||
|
where
|
||||||
|
F: Fn() -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<T, GovernanceError>>,
|
||||||
|
{
|
||||||
|
if self.closed.load(Ordering::SeqCst) {
|
||||||
|
return Err(GovernanceError::ClientClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_error = None;
|
||||||
|
for attempt in 0..self.config.retries {
|
||||||
|
match operation().await {
|
||||||
|
Ok(result) => return Ok(result),
|
||||||
|
Err(e) => {
|
||||||
|
if self.config.debug {
|
||||||
|
eprintln!("Attempt {} failed: {:?}", attempt + 1, e);
|
||||||
|
}
|
||||||
|
last_error = Some(e);
|
||||||
|
if attempt < self.config.retries - 1 {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(last_error.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_response<T: for<'de> Deserialize<'de>>(
|
||||||
|
&self,
|
||||||
|
resp: reqwest::Response,
|
||||||
|
) -> Result<T, GovernanceError> {
|
||||||
|
if resp.status().is_success() {
|
||||||
|
Ok(resp.json().await?)
|
||||||
|
} else {
|
||||||
|
Err(self.parse_error(resp).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_error(&self, resp: reqwest::Response) -> GovernanceError {
|
||||||
|
let status = resp.status().as_u16();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
message: Option<String>,
|
||||||
|
code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let (message, code) = match serde_json::from_str::<ErrorResponse>(&body) {
|
||||||
|
Ok(err) => (
|
||||||
|
err.message.unwrap_or_else(|| format!("HTTP {}", status)),
|
||||||
|
err.code,
|
||||||
|
),
|
||||||
|
Err(_) => (format!("HTTP {}", status), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
GovernanceError::Http {
|
||||||
|
message,
|
||||||
|
code,
|
||||||
|
status_code: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
802
sdk/rust/src/mining/mod.rs
Normal file
802
sdk/rust/src/mining/mod.rs
Normal file
|
|
@ -0,0 +1,802 @@
|
||||||
|
//! Synor Mining SDK for Rust.
|
||||||
|
//! Pool connections, block templates, hashrate stats, and GPU management.
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT: &str = "https://mining.synor.io/v1";
|
||||||
|
const DEFAULT_TIMEOUT: u64 = 30;
|
||||||
|
const DEFAULT_RETRIES: u32 = 3;
|
||||||
|
|
||||||
|
/// Device type.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum DeviceType {
|
||||||
|
Cpu,
|
||||||
|
GpuNvidia,
|
||||||
|
GpuAmd,
|
||||||
|
Asic,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device status.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DeviceStatus {
|
||||||
|
Idle,
|
||||||
|
Mining,
|
||||||
|
Error,
|
||||||
|
Offline,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection status.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ConnectionStatus {
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Reconnecting,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Time period for stats.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum TimePeriod {
|
||||||
|
Hour,
|
||||||
|
Day,
|
||||||
|
Week,
|
||||||
|
Month,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submit result status.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SubmitResultStatus {
|
||||||
|
Accepted,
|
||||||
|
Rejected,
|
||||||
|
Stale,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mining SDK configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MiningConfig {
|
||||||
|
pub api_key: String,
|
||||||
|
pub endpoint: String,
|
||||||
|
pub timeout: Duration,
|
||||||
|
pub retries: u32,
|
||||||
|
pub debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MiningConfig {
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
api_key: api_key.into(),
|
||||||
|
endpoint: DEFAULT_ENDPOINT.to_string(),
|
||||||
|
timeout: Duration::from_secs(DEFAULT_TIMEOUT),
|
||||||
|
retries: DEFAULT_RETRIES,
|
||||||
|
debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
|
||||||
|
self.endpoint = endpoint.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pool configuration.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct PoolConfig {
|
||||||
|
pub url: String,
|
||||||
|
pub user: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub password: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub algorithm: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub difficulty: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stratum connection.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StratumConnection {
|
||||||
|
pub id: String,
|
||||||
|
pub pool: String,
|
||||||
|
pub status: ConnectionStatus,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub difficulty: f64,
|
||||||
|
pub connected_at: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_share_at: Option<i64>,
|
||||||
|
pub accepted_shares: i64,
|
||||||
|
pub rejected_shares: i64,
|
||||||
|
pub stale_shares: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Template transaction.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TemplateTransaction {
|
||||||
|
pub txid: String,
|
||||||
|
pub data: String,
|
||||||
|
pub fee: String,
|
||||||
|
pub weight: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block template.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BlockTemplate {
|
||||||
|
pub id: String,
|
||||||
|
pub previous_block_hash: String,
|
||||||
|
pub merkle_root: String,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub bits: String,
|
||||||
|
pub height: i64,
|
||||||
|
pub coinbase_value: String,
|
||||||
|
pub transactions: Vec<TemplateTransaction>,
|
||||||
|
pub target: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub extra_nonce: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mined work to submit.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct MinedWork {
|
||||||
|
pub template_id: String,
|
||||||
|
pub nonce: String,
|
||||||
|
pub extra_nonce: String,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Share info.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ShareInfo {
|
||||||
|
pub hash: String,
|
||||||
|
pub difficulty: f64,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub accepted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submit result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SubmitResult {
|
||||||
|
pub status: SubmitResultStatus,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reason: Option<String>,
|
||||||
|
pub share: ShareInfo,
|
||||||
|
pub block_found: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub block_hash: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reward: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hashrate statistics.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Hashrate {
|
||||||
|
pub current: f64,
|
||||||
|
pub average_1h: f64,
|
||||||
|
pub average_24h: f64,
|
||||||
|
pub peak: f64,
|
||||||
|
pub unit: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Share stats.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ShareStats {
|
||||||
|
pub accepted: i64,
|
||||||
|
pub rejected: i64,
|
||||||
|
pub stale: i64,
|
||||||
|
pub total: i64,
|
||||||
|
pub accept_rate: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device temperature.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DeviceTemperature {
|
||||||
|
pub current: f64,
|
||||||
|
pub max: f64,
|
||||||
|
pub throttling: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Earnings snapshot.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct EarningsSnapshot {
|
||||||
|
pub today: String,
|
||||||
|
pub yesterday: String,
|
||||||
|
pub this_week: String,
|
||||||
|
pub this_month: String,
|
||||||
|
pub total: String,
|
||||||
|
pub currency: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mining stats.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MiningStats {
|
||||||
|
pub hashrate: Hashrate,
|
||||||
|
pub shares: ShareStats,
|
||||||
|
pub uptime: i64,
|
||||||
|
pub efficiency: f64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub power_consumption: Option<f64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub temperature: Option<DeviceTemperature>,
|
||||||
|
pub earnings: EarningsSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Earnings breakdown.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct EarningsBreakdown {
|
||||||
|
pub date: i64,
|
||||||
|
pub amount: String,
|
||||||
|
pub blocks: i32,
|
||||||
|
pub shares: i64,
|
||||||
|
pub hashrate: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detailed earnings.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Earnings {
|
||||||
|
pub period: TimePeriod,
|
||||||
|
pub start_date: i64,
|
||||||
|
pub end_date: i64,
|
||||||
|
pub amount: String,
|
||||||
|
pub blocks: i32,
|
||||||
|
pub shares: i64,
|
||||||
|
pub average_hashrate: f64,
|
||||||
|
pub currency: String,
|
||||||
|
pub breakdown: Vec<EarningsBreakdown>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mining device.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MiningDevice {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub device_type: DeviceType,
|
||||||
|
pub status: DeviceStatus,
|
||||||
|
pub hashrate: f64,
|
||||||
|
pub temperature: f64,
|
||||||
|
pub fan_speed: f64,
|
||||||
|
pub power_draw: f64,
|
||||||
|
pub memory_used: i64,
|
||||||
|
pub memory_total: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub driver: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub firmware: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device configuration.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DeviceConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub intensity: Option<i32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub power_limit: Option<i32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub core_clock_offset: Option<i32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub memory_clock_offset: Option<i32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fan_speed: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Worker info.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WorkerInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub status: ConnectionStatus,
|
||||||
|
pub hashrate: Hashrate,
|
||||||
|
pub shares: ShareStats,
|
||||||
|
pub devices: Vec<MiningDevice>,
|
||||||
|
pub last_seen: i64,
|
||||||
|
pub uptime: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pool stats.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PoolStats {
|
||||||
|
pub url: String,
|
||||||
|
pub workers: i32,
|
||||||
|
pub hashrate: f64,
|
||||||
|
pub difficulty: f64,
|
||||||
|
pub last_block: i64,
|
||||||
|
pub blocks_found_24h: i32,
|
||||||
|
pub luck: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mining algorithm.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MiningAlgorithm {
|
||||||
|
pub name: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub hash_unit: String,
|
||||||
|
pub profitability: String,
|
||||||
|
pub difficulty: f64,
|
||||||
|
pub block_reward: String,
|
||||||
|
pub block_time: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Work result from getwork.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WorkResult {
|
||||||
|
pub work: String,
|
||||||
|
pub target: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start mining result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StartMiningResult {
|
||||||
|
pub started: bool,
|
||||||
|
pub devices: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mining SDK error.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum MiningError {
|
||||||
|
#[error("HTTP error: {message}")]
|
||||||
|
Http {
|
||||||
|
message: String,
|
||||||
|
code: Option<String>,
|
||||||
|
status_code: u16,
|
||||||
|
},
|
||||||
|
#[error("Client closed")]
|
||||||
|
ClientClosed,
|
||||||
|
#[error("No active connection")]
|
||||||
|
NoActiveConnection,
|
||||||
|
#[error("Request error: {0}")]
|
||||||
|
Request(#[from] reqwest::Error),
|
||||||
|
#[error("JSON error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synor Mining SDK client.
|
||||||
|
pub struct SynorMining {
|
||||||
|
config: MiningConfig,
|
||||||
|
client: Client,
|
||||||
|
active_connection: Arc<RwLock<Option<StratumConnection>>>,
|
||||||
|
closed: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SynorMining {
|
||||||
|
/// Create a new Mining client.
|
||||||
|
pub fn new(config: MiningConfig) -> Result<Self, MiningError> {
|
||||||
|
let client = Client::builder().timeout(config.timeout).build()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
config,
|
||||||
|
client,
|
||||||
|
active_connection: Arc::new(RwLock::new(None)),
|
||||||
|
closed: Arc::new(AtomicBool::new(false)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Pool Connection ====================
|
||||||
|
|
||||||
|
/// Connect to a mining pool.
|
||||||
|
pub async fn connect(&self, pool: PoolConfig) -> Result<StratumConnection, MiningError> {
|
||||||
|
let conn: StratumConnection = self.post("/pool/connect", &pool).await?;
|
||||||
|
*self.active_connection.write().unwrap() = Some(conn.clone());
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the current pool.
|
||||||
|
pub async fn disconnect(&self) -> Result<(), MiningError> {
|
||||||
|
let conn = self.active_connection.read().unwrap().clone();
|
||||||
|
if let Some(c) = conn {
|
||||||
|
self.post::<(), ()>(&format!("/pool/disconnect/{}", c.id), &())
|
||||||
|
.await?;
|
||||||
|
*self.active_connection.write().unwrap() = None;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current connection status.
|
||||||
|
pub async fn get_connection_status(&self) -> Result<Option<StratumConnection>, MiningError> {
|
||||||
|
let conn = self.active_connection.read().unwrap().clone();
|
||||||
|
if let Some(c) = conn {
|
||||||
|
let status: StratumConnection =
|
||||||
|
self.get(&format!("/pool/status/{}", c.id)).await?;
|
||||||
|
Ok(Some(status))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconnect to the pool.
|
||||||
|
pub async fn reconnect(&self) -> Result<StratumConnection, MiningError> {
|
||||||
|
let conn = self.active_connection.read().unwrap().clone();
|
||||||
|
if let Some(c) = conn {
|
||||||
|
self.post(&format!("/pool/reconnect/{}", c.id), &()).await
|
||||||
|
} else {
|
||||||
|
Err(MiningError::NoActiveConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Mining Operations ====================
|
||||||
|
|
||||||
|
/// Get the current block template.
|
||||||
|
pub async fn get_block_template(&self) -> Result<BlockTemplate, MiningError> {
|
||||||
|
self.get("/mining/template").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submit mined work.
|
||||||
|
pub async fn submit_work(&self, work: MinedWork) -> Result<SubmitResult, MiningError> {
|
||||||
|
self.post("/mining/submit", &work).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get work from pool.
|
||||||
|
pub async fn get_work(&self) -> Result<WorkResult, MiningError> {
|
||||||
|
self.get("/mining/getwork").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start mining on all enabled devices.
|
||||||
|
pub async fn start_mining(
|
||||||
|
&self,
|
||||||
|
algorithm: Option<&str>,
|
||||||
|
) -> Result<StartMiningResult, MiningError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
algorithm: Option<String>,
|
||||||
|
}
|
||||||
|
self.post(
|
||||||
|
"/mining/start",
|
||||||
|
&Request {
|
||||||
|
algorithm: algorithm.map(String::from),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop mining on all devices.
|
||||||
|
pub async fn stop_mining(&self) -> Result<bool, MiningError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
stopped: bool,
|
||||||
|
}
|
||||||
|
let resp: Response = self.post("/mining/stop", &()).await?;
|
||||||
|
Ok(resp.stopped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Stats ====================
|
||||||
|
|
||||||
|
/// Get current hashrate.
|
||||||
|
pub async fn get_hashrate(&self) -> Result<Hashrate, MiningError> {
|
||||||
|
self.get("/stats/hashrate").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mining stats.
|
||||||
|
pub async fn get_stats(&self) -> Result<MiningStats, MiningError> {
|
||||||
|
self.get("/stats").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get earnings for a time period.
|
||||||
|
pub async fn get_earnings(&self, period: Option<TimePeriod>) -> Result<Earnings, MiningError> {
|
||||||
|
let path = match period {
|
||||||
|
Some(p) => format!("/stats/earnings?period={:?}", p).to_lowercase(),
|
||||||
|
None => "/stats/earnings".to_string(),
|
||||||
|
};
|
||||||
|
self.get(&path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get earnings history.
|
||||||
|
pub async fn get_earnings_history(
|
||||||
|
&self,
|
||||||
|
limit: Option<i32>,
|
||||||
|
offset: Option<i32>,
|
||||||
|
) -> Result<Vec<Earnings>, MiningError> {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
if let Some(l) = limit {
|
||||||
|
params.push(format!("limit={}", l));
|
||||||
|
}
|
||||||
|
if let Some(o) = offset {
|
||||||
|
params.push(format!("offset={}", o));
|
||||||
|
}
|
||||||
|
let path = if params.is_empty() {
|
||||||
|
"/stats/earnings/history".to_string()
|
||||||
|
} else {
|
||||||
|
format!("/stats/earnings/history?{}", params.join("&"))
|
||||||
|
};
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
earnings: Vec<Earnings>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get(&path).await?;
|
||||||
|
Ok(resp.earnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pool stats.
|
||||||
|
pub async fn get_pool_stats(&self) -> Result<PoolStats, MiningError> {
|
||||||
|
self.get("/pool/stats").await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GPU Management ====================
|
||||||
|
|
||||||
|
/// List all mining devices.
|
||||||
|
pub async fn list_devices(&self) -> Result<Vec<MiningDevice>, MiningError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
devices: Vec<MiningDevice>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get("/devices").await?;
|
||||||
|
Ok(resp.devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get device details.
|
||||||
|
pub async fn get_device(&self, device_id: &str) -> Result<MiningDevice, MiningError> {
|
||||||
|
self.get(&format!("/devices/{}", urlencoding::encode(device_id)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set device configuration.
|
||||||
|
pub async fn set_device_config(
|
||||||
|
&self,
|
||||||
|
device_id: &str,
|
||||||
|
config: DeviceConfig,
|
||||||
|
) -> Result<MiningDevice, MiningError> {
|
||||||
|
self.post(
|
||||||
|
&format!("/devices/{}/config", urlencoding::encode(device_id)),
|
||||||
|
&config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable a device for mining.
|
||||||
|
pub async fn enable_device(&self, device_id: &str) -> Result<MiningDevice, MiningError> {
|
||||||
|
self.post::<(), MiningDevice>(
|
||||||
|
&format!("/devices/{}/enable", urlencoding::encode(device_id)),
|
||||||
|
&(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable a device.
|
||||||
|
pub async fn disable_device(&self, device_id: &str) -> Result<MiningDevice, MiningError> {
|
||||||
|
self.post::<(), MiningDevice>(
|
||||||
|
&format!("/devices/{}/disable", urlencoding::encode(device_id)),
|
||||||
|
&(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset device to default settings.
|
||||||
|
pub async fn reset_device(&self, device_id: &str) -> Result<MiningDevice, MiningError> {
|
||||||
|
self.post::<(), MiningDevice>(
|
||||||
|
&format!("/devices/{}/reset", urlencoding::encode(device_id)),
|
||||||
|
&(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Workers ====================
|
||||||
|
|
||||||
|
/// List all workers.
|
||||||
|
pub async fn list_workers(&self) -> Result<Vec<WorkerInfo>, MiningError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
workers: Vec<WorkerInfo>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get("/workers").await?;
|
||||||
|
Ok(resp.workers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get worker details.
|
||||||
|
pub async fn get_worker(&self, worker_id: &str) -> Result<WorkerInfo, MiningError> {
|
||||||
|
self.get(&format!("/workers/{}", urlencoding::encode(worker_id)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new worker.
|
||||||
|
pub async fn create_worker(&self, name: &str) -> Result<WorkerInfo, MiningError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
self.post(
|
||||||
|
"/workers",
|
||||||
|
&Request {
|
||||||
|
name: name.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a worker.
|
||||||
|
pub async fn delete_worker(&self, worker_id: &str) -> Result<(), MiningError> {
|
||||||
|
self.delete(&format!("/workers/{}", urlencoding::encode(worker_id)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Algorithms ====================
|
||||||
|
|
||||||
|
/// Get supported mining algorithms.
|
||||||
|
pub async fn get_supported_algorithms(&self) -> Result<Vec<MiningAlgorithm>, MiningError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
algorithms: Vec<MiningAlgorithm>,
|
||||||
|
}
|
||||||
|
let resp: Response = self.get("/algorithms").await?;
|
||||||
|
Ok(resp.algorithms)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current algorithm.
|
||||||
|
pub async fn get_current_algorithm(&self) -> Result<MiningAlgorithm, MiningError> {
|
||||||
|
self.get("/algorithms/current").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch to a different algorithm.
|
||||||
|
pub async fn switch_algorithm(&self, algorithm: &str) -> Result<bool, MiningError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
algorithm: String,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
switched: bool,
|
||||||
|
}
|
||||||
|
let resp: Response = self
|
||||||
|
.post(
|
||||||
|
"/algorithms/switch",
|
||||||
|
&Request {
|
||||||
|
algorithm: algorithm.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(resp.switched)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
/// Health check.
|
||||||
|
pub async fn health_check(&self) -> bool {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
status: String,
|
||||||
|
}
|
||||||
|
match self.get::<Response>("/health").await {
|
||||||
|
Ok(resp) => resp.status == "healthy",
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if client is closed.
|
||||||
|
pub fn is_closed(&self) -> bool {
|
||||||
|
self.closed.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the client.
|
||||||
|
pub async fn close(&self) -> Result<(), MiningError> {
|
||||||
|
self.disconnect().await?;
|
||||||
|
self.closed.store(true, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Private Methods ====================
|
||||||
|
|
||||||
|
async fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, MiningError> {
|
||||||
|
self.execute(|| async {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(format!("{}{}", self.config.endpoint, path))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-SDK-Version", "rust/0.1.0")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
self.handle_response(resp).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post<B: Serialize, T: for<'de> Deserialize<'de>>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
body: &B,
|
||||||
|
) -> Result<T, MiningError> {
|
||||||
|
self.execute(|| async {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.post(format!("{}{}", self.config.endpoint, path))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-SDK-Version", "rust/0.1.0")
|
||||||
|
.json(body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
self.handle_response(resp).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, path: &str) -> Result<(), MiningError> {
|
||||||
|
self.execute(|| async {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.delete(format!("{}{}", self.config.endpoint, path))
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("X-SDK-Version", "rust/0.1.0")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if resp.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(self.parse_error(resp).await)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute<F, Fut, T>(&self, operation: F) -> Result<T, MiningError>
|
||||||
|
where
|
||||||
|
F: Fn() -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<T, MiningError>>,
|
||||||
|
{
|
||||||
|
if self.closed.load(Ordering::SeqCst) {
|
||||||
|
return Err(MiningError::ClientClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_error = None;
|
||||||
|
for attempt in 0..self.config.retries {
|
||||||
|
match operation().await {
|
||||||
|
Ok(result) => return Ok(result),
|
||||||
|
Err(e) => {
|
||||||
|
if self.config.debug {
|
||||||
|
eprintln!("Attempt {} failed: {:?}", attempt + 1, e);
|
||||||
|
}
|
||||||
|
last_error = Some(e);
|
||||||
|
if attempt < self.config.retries - 1 {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(last_error.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_response<T: for<'de> Deserialize<'de>>(
|
||||||
|
&self,
|
||||||
|
resp: reqwest::Response,
|
||||||
|
) -> Result<T, MiningError> {
|
||||||
|
if resp.status().is_success() {
|
||||||
|
Ok(resp.json().await?)
|
||||||
|
} else {
|
||||||
|
Err(self.parse_error(resp).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_error(&self, resp: reqwest::Response) -> MiningError {
|
||||||
|
let status = resp.status().as_u16();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
message: Option<String>,
|
||||||
|
code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let (message, code) = match serde_json::from_str::<ErrorResponse>(&body) {
|
||||||
|
Ok(err) => (
|
||||||
|
err.message.unwrap_or_else(|| format!("HTTP {}", status)),
|
||||||
|
err.code,
|
||||||
|
),
|
||||||
|
Err(_) => (format!("HTTP {}", status), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
MiningError::Http {
|
||||||
|
message,
|
||||||
|
code,
|
||||||
|
status_code: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue