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