diff --git a/sdk/go/economics/economics.go b/sdk/go/economics/economics.go new file mode 100644 index 0000000..19358d7 --- /dev/null +++ b/sdk/go/economics/economics.go @@ -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<= 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 +} diff --git a/sdk/go/governance/governance.go b/sdk/go/governance/governance.go new file mode 100644 index 0000000..e379564 --- /dev/null +++ b/sdk/go/governance/governance.go @@ -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<= 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 +} diff --git a/sdk/go/mining/mining.go b/sdk/go/mining/mining.go new file mode 100644 index 0000000..20f365f --- /dev/null +++ b/sdk/go/mining/mining.go @@ -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<= 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 +} diff --git a/sdk/js/src/economics/client.ts b/sdk/js/src/economics/client.ts new file mode 100644 index 0000000..75466e9 --- /dev/null +++ b/sdk/js/src/economics/client.ts @@ -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; + 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 { + const response = await this.post('/pricing/calculate', { + service, + usage, + }); + return response.price; + } + + /** + * Estimate cost for a usage plan. + */ + async estimateCost(plan: UsagePlan): Promise { + return await this.post('/pricing/estimate', plan); + } + + /** + * Get pricing tiers for a service. + */ + async getPricingTiers(service: ServiceType): Promise { + 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 { + const params = period ? `?period=${period}` : ''; + return await this.get(`/usage${params}`); + } + + /** + * Get usage history. + */ + async getUsageHistory(limit?: number, offset?: number): Promise { + 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 { + const response = await this.get('/invoices'); + return response.invoices ?? []; + } + + /** + * Get a specific invoice. + */ + async getInvoice(invoiceId: string): Promise { + return await this.get(`/invoices/${encodeURIComponent(invoiceId)}`); + } + + /** + * Pay an invoice. + */ + async payInvoice(invoiceId: string, paymentMethod?: string): Promise { + return await this.post(`/invoices/${encodeURIComponent(invoiceId)}/pay`, { + paymentMethod, + }); + } + + /** + * Get account balance. + */ + async getBalance(): Promise { + return await this.get('/balance'); + } + + /** + * Add funds to account. + */ + async addFunds(amount: string, paymentMethod: string): Promise { + return await this.post('/balance/deposit', { + amount, + paymentMethod, + }); + } + + // ==================== Staking Operations ==================== + + /** + * Stake tokens. + */ + async stake(amount: string, options?: StakeOptions): Promise { + const body: Record = { 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('/staking/stake', body); + } + + /** + * Unstake tokens. + */ + async unstake(stakeId: string): Promise { + return await this.post(`/staking/stakes/${encodeURIComponent(stakeId)}/unstake`, {}); + } + + /** + * Get staking rewards. + */ + async getStakingRewards(): Promise { + return await this.get('/staking/rewards'); + } + + /** + * Claim staking rewards. + */ + async claimRewards(stakeId?: string): Promise { + const path = stakeId + ? `/staking/stakes/${encodeURIComponent(stakeId)}/claim` + : '/staking/rewards/claim'; + return await this.post(path, {}); + } + + /** + * List active stakes. + */ + async listStakes(): Promise { + const response = await this.get('/staking/stakes'); + return response.stakes ?? []; + } + + /** + * Get stake details. + */ + async getStake(stakeId: string): Promise { + return await this.get(`/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 { + return await this.post('/discounts/apply', { code }); + } + + /** + * Get available discounts. + */ + async getAvailableDiscounts(): Promise { + const response = await this.get('/discounts'); + return response.discounts ?? []; + } + + /** + * Get active discounts on account. + */ + async getActiveDiscounts(): Promise { + const response = await this.get('/discounts/active'); + return response.discounts ?? []; + } + + /** + * Remove a discount. + */ + async removeDiscount(code: string): Promise { + await this.delete(`/discounts/${encodeURIComponent(code)}`); + } + + // ==================== Lifecycle ==================== + + /** + * Health check. + */ + async healthCheck(): Promise { + 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(path: string): Promise { + 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(path: string, body: unknown): Promise { + 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 { + 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 { + return { + 'Authorization': `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json', + 'X-SDK-Version': 'js/0.1.0', + }; + } + + private async execute(operation: () => Promise): Promise { + 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 { + 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 { + 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'; diff --git a/sdk/js/src/economics/index.ts b/sdk/js/src/economics/index.ts new file mode 100644 index 0000000..4fe188c --- /dev/null +++ b/sdk/js/src/economics/index.ts @@ -0,0 +1,7 @@ +/** + * Synor Economics SDK + * Pricing, billing, staking, and discount management. + */ + +export { SynorEconomics, EconomicsError } from './client'; +export * from './types'; diff --git a/sdk/js/src/economics/types.ts b/sdk/js/src/economics/types.ts new file mode 100644 index 0000000..5d6c4a3 --- /dev/null +++ b/sdk/js/src/economics/types.ts @@ -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[]; +} diff --git a/sdk/js/src/governance/client.ts b/sdk/js/src/governance/client.ts new file mode 100644 index 0000000..c4a78b4 --- /dev/null +++ b/sdk/js/src/governance/client.ts @@ -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; + 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 { + return await this.post('/proposals', draft); + } + + /** + * Get a proposal by ID. + */ + async getProposal(proposalId: string): Promise { + return await this.get(`/proposals/${encodeURIComponent(proposalId)}`); + } + + /** + * List proposals with optional filtering. + */ + async listProposals(filter?: ProposalFilter): Promise { + 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(`/proposals${query}`); + return response.proposals ?? []; + } + + /** + * Cancel a proposal. + */ + async cancelProposal(proposalId: string): Promise { + return await this.post(`/proposals/${encodeURIComponent(proposalId)}/cancel`, {}); + } + + /** + * Execute a passed proposal. + */ + async executeProposal(proposalId: string): Promise { + return await this.post( + `/proposals/${encodeURIComponent(proposalId)}/execute`, + {} + ); + } + + // ==================== Voting Operations ==================== + + /** + * Vote on a proposal. + */ + async vote(proposalId: string, vote: Vote, weight?: string): Promise { + const body: Record = { ...vote }; + if (weight) body.weight = weight; + return await this.post( + `/proposals/${encodeURIComponent(proposalId)}/vote`, + body + ); + } + + /** + * Get votes for a proposal. + */ + async getVotes( + proposalId: string, + limit?: number, + offset?: number + ): Promise { + 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 { + const body: Record = { to }; + if (amount) body.amount = amount; + return await this.post('/voting/delegate', body); + } + + /** + * Undelegate voting power. + */ + async undelegate(from?: string): Promise { + const body: Record = {}; + if (from) body.from = from; + return await this.post('/voting/undelegate', body); + } + + /** + * Get voting power for an address. + */ + async getVotingPower(address: string): Promise { + return await this.get(`/voting/power/${encodeURIComponent(address)}`); + } + + /** + * Get current voting power for the authenticated user. + */ + async getMyVotingPower(): Promise { + return await this.get('/voting/power'); + } + + // ==================== DAO Operations ==================== + + /** + * Create a new DAO. + */ + async createDao(config: DaoConfig): Promise { + return await this.post('/daos', config); + } + + /** + * Get a DAO by ID. + */ + async getDao(daoId: string): Promise { + return await this.get(`/daos/${encodeURIComponent(daoId)}`); + } + + /** + * List DAOs. + */ + async listDaos(limit?: number, offset?: number): Promise { + 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(`/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 { + return await this.post('/vesting', schedule); + } + + /** + * Get a vesting contract. + */ + async getVestingContract(contractId: string): Promise { + return await this.get(`/vesting/${encodeURIComponent(contractId)}`); + } + + /** + * List vesting contracts. + */ + async listVestingContracts(beneficiary?: string): Promise { + const query = beneficiary ? `?beneficiary=${encodeURIComponent(beneficiary)}` : ''; + const response = await this.get(`/vesting${query}`); + return response.contracts ?? []; + } + + /** + * Claim vested tokens. + */ + async claimVested(contractId: string): Promise { + return await this.post( + `/vesting/${encodeURIComponent(contractId)}/claim`, + {} + ); + } + + /** + * Revoke a vesting contract (if revocable). + */ + async revokeVesting(contractId: string): Promise { + return await this.post( + `/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 { + 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(path: string): Promise { + 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(path: string, body: unknown): Promise { + 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 { + return { + 'Authorization': `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json', + 'X-SDK-Version': 'js/0.1.0', + }; + } + + private async execute(operation: () => Promise): Promise { + 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 { + 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 { + 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'; diff --git a/sdk/js/src/governance/index.ts b/sdk/js/src/governance/index.ts new file mode 100644 index 0000000..bc4f4b7 --- /dev/null +++ b/sdk/js/src/governance/index.ts @@ -0,0 +1,7 @@ +/** + * Synor Governance SDK + * Proposals, voting, DAOs, and vesting. + */ + +export { SynorGovernance, GovernanceError } from './client'; +export * from './types'; diff --git a/sdk/js/src/governance/types.ts b/sdk/js/src/governance/types.ts new file mode 100644 index 0000000..4653fcb --- /dev/null +++ b/sdk/js/src/governance/types.ts @@ -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[]; +} diff --git a/sdk/js/src/mining/client.ts b/sdk/js/src/mining/client.ts new file mode 100644 index 0000000..bf6578f --- /dev/null +++ b/sdk/js/src/mining/client.ts @@ -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; + 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 { + const connection = await this.post('/pool/connect', pool); + this.activeConnection = connection; + return connection; + } + + /** + * Disconnect from the current pool. + */ + async disconnect(): Promise { + if (this.activeConnection) { + await this.post(`/pool/disconnect/${this.activeConnection.id}`, {}); + this.activeConnection = undefined; + } + } + + /** + * Get current connection status. + */ + async getConnectionStatus(): Promise { + if (!this.activeConnection) return null; + try { + return await this.get(`/pool/status/${this.activeConnection.id}`); + } catch { + return null; + } + } + + /** + * Reconnect to the pool. + */ + async reconnect(): Promise { + if (!this.activeConnection) { + throw new MiningError('No active connection to reconnect'); + } + return await this.post(`/pool/reconnect/${this.activeConnection.id}`, {}); + } + + // ==================== Mining Operations ==================== + + /** + * Get the current block template. + */ + async getBlockTemplate(): Promise { + return await this.get('/mining/template'); + } + + /** + * Submit mined work. + */ + async submitWork(work: MinedWork): Promise { + return await this.post('/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 { + return await this.get('/stats/hashrate'); + } + + /** + * Get mining stats. + */ + async getStats(): Promise { + return await this.get('/stats'); + } + + /** + * Get earnings for a time period. + */ + async getEarnings(period?: TimePeriod): Promise { + const query = period ? `?period=${period}` : ''; + return await this.get(`/stats/earnings${query}`); + } + + /** + * Get earnings history. + */ + async getEarningsHistory(limit?: number, offset?: number): Promise { + 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 { + return await this.get('/pool/stats'); + } + + // ==================== GPU Management ==================== + + /** + * List all mining devices. + */ + async listDevices(): Promise { + const response = await this.get('/devices'); + return response.devices ?? []; + } + + /** + * Get device details. + */ + async getDevice(deviceId: string): Promise { + return await this.get(`/devices/${encodeURIComponent(deviceId)}`); + } + + /** + * Set device configuration. + */ + async setDeviceConfig(deviceId: string, config: DeviceConfig): Promise { + return await this.post( + `/devices/${encodeURIComponent(deviceId)}/config`, + config + ); + } + + /** + * Enable a device for mining. + */ + async enableDevice(deviceId: string): Promise { + return await this.post(`/devices/${encodeURIComponent(deviceId)}/enable`, {}); + } + + /** + * Disable a device. + */ + async disableDevice(deviceId: string): Promise { + return await this.post(`/devices/${encodeURIComponent(deviceId)}/disable`, {}); + } + + /** + * Reset device to default settings. + */ + async resetDevice(deviceId: string): Promise { + return await this.post(`/devices/${encodeURIComponent(deviceId)}/reset`, {}); + } + + // ==================== Workers ==================== + + /** + * List all workers. + */ + async listWorkers(): Promise { + const response = await this.get('/workers'); + return response.workers ?? []; + } + + /** + * Get worker details. + */ + async getWorker(workerId: string): Promise { + return await this.get(`/workers/${encodeURIComponent(workerId)}`); + } + + /** + * Create a new worker. + */ + async createWorker(name: string): Promise { + return await this.post('/workers', { name }); + } + + /** + * Delete a worker. + */ + async deleteWorker(workerId: string): Promise { + await this.delete(`/workers/${encodeURIComponent(workerId)}`); + } + + // ==================== Algorithms ==================== + + /** + * Get supported mining algorithms. + */ + async getSupportedAlgorithms(): Promise { + const response = await this.get('/algorithms'); + return response.algorithms ?? []; + } + + /** + * Get current algorithm. + */ + async getCurrentAlgorithm(): Promise { + return await this.get('/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 { + 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 { + if (this.activeConnection) { + await this.disconnect(); + } + this.closed = true; + } + + // ==================== Private HTTP Methods ==================== + + private async get(path: string): Promise { + 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(path: string, body: unknown): Promise { + 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 { + 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 { + return { + 'Authorization': `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json', + 'X-SDK-Version': 'js/0.1.0', + }; + } + + private async execute(operation: () => Promise): Promise { + 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 { + 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 { + 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'; diff --git a/sdk/js/src/mining/index.ts b/sdk/js/src/mining/index.ts new file mode 100644 index 0000000..4b2b64f --- /dev/null +++ b/sdk/js/src/mining/index.ts @@ -0,0 +1,7 @@ +/** + * Synor Mining SDK + * Pool connections, block templates, hashrate stats, and GPU management. + */ + +export { SynorMining, MiningError } from './client'; +export * from './types'; diff --git a/sdk/js/src/mining/types.ts b/sdk/js/src/mining/types.ts new file mode 100644 index 0000000..8232938 --- /dev/null +++ b/sdk/js/src/mining/types.ts @@ -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[]; +} diff --git a/sdk/python/src/synor_economics/__init__.py b/sdk/python/src/synor_economics/__init__.py new file mode 100644 index 0000000..f06f190 --- /dev/null +++ b/sdk/python/src/synor_economics/__init__.py @@ -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", +] diff --git a/sdk/python/src/synor_economics/client.py b/sdk/python/src/synor_economics/client.py new file mode 100644 index 0000000..4698a2e --- /dev/null +++ b/sdk/python/src/synor_economics/client.py @@ -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"), + ) diff --git a/sdk/python/src/synor_economics/types.py b/sdk/python/src/synor_economics/types.py new file mode 100644 index 0000000..a28b092 --- /dev/null +++ b/sdk/python/src/synor_economics/types.py @@ -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 diff --git a/sdk/python/src/synor_governance/__init__.py b/sdk/python/src/synor_governance/__init__.py new file mode 100644 index 0000000..09c63a7 --- /dev/null +++ b/sdk/python/src/synor_governance/__init__.py @@ -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", +] diff --git a/sdk/python/src/synor_governance/client.py b/sdk/python/src/synor_governance/client.py new file mode 100644 index 0000000..15f8790 --- /dev/null +++ b/sdk/python/src/synor_governance/client.py @@ -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"], + ) diff --git a/sdk/python/src/synor_governance/types.py b/sdk/python/src/synor_governance/types.py new file mode 100644 index 0000000..a718f09 --- /dev/null +++ b/sdk/python/src/synor_governance/types.py @@ -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 diff --git a/sdk/python/src/synor_mining/__init__.py b/sdk/python/src/synor_mining/__init__.py new file mode 100644 index 0000000..df41557 --- /dev/null +++ b/sdk/python/src/synor_mining/__init__.py @@ -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", +] diff --git a/sdk/python/src/synor_mining/client.py b/sdk/python/src/synor_mining/client.py new file mode 100644 index 0000000..30286c5 --- /dev/null +++ b/sdk/python/src/synor_mining/client.py @@ -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"], + ) diff --git a/sdk/python/src/synor_mining/types.py b/sdk/python/src/synor_mining/types.py new file mode 100644 index 0000000..2eb7e12 --- /dev/null +++ b/sdk/python/src/synor_mining/types.py @@ -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 diff --git a/sdk/rust/src/economics/mod.rs b/sdk/rust/src/economics/mod.rs new file mode 100644 index 0000000..798dafe --- /dev/null +++ b/sdk/rust/src/economics/mod.rs @@ -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) -> 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) -> 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub storage_bytes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub database_ops: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hosting_requests: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bridge_transfers: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rpc_calls: Option, +} + +/// 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, +} + +/// Usage plan for cost estimation. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UsagePlan { + pub services: Vec, + pub period: BillingPeriod, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_date: Option, +} + +/// 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, +} + +/// 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, + pub total: String, + pub currency: String, +} + +/// Cost estimate for a usage plan. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CostEstimate { + pub services: Vec, + pub subtotal: String, + pub discounts: Vec, + pub taxes: Vec, + 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, +} + +/// Usage record. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Usage { + pub period: BillingPeriod, + pub start_date: i64, + pub end_date: i64, + pub services: Vec, + 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, + pub subtotal: String, + pub discounts: Vec, + pub taxes: Vec, + pub total: String, + pub currency: String, + pub due_date: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub paid_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_method: Option, +} + +/// 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, + 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, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub next_claim_available: Option, + pub stakes: Vec, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_discount: Option, + pub applicable_services: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_limit: Option, + pub used_count: i32, +} + +/// Stake options. +#[derive(Debug, Clone, Default, Serialize)] +pub struct StakeOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub validator: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_compound: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lock_duration: Option, +} + +/// 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, + 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, +} + +impl SynorEconomics { + /// Create a new Economics client. + pub fn new(config: EconomicsConfig) -> Result { + 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 { + #[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 { + self.post("/pricing/estimate", &plan).await + } + + /// Get pricing tiers for a service. + pub async fn get_pricing_tiers( + &self, + service: ServiceType, + ) -> Result, EconomicsError> { + #[derive(Deserialize)] + struct Response { + tiers: Vec, + } + 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, + ) -> Result { + 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, + offset: Option, + ) -> Result, 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, + } + let resp: Response = self.get(&path).await?; + Ok(resp.usage) + } + + /// Get invoices. + pub async fn get_invoices(&self) -> Result, EconomicsError> { + #[derive(Deserialize)] + struct Response { + invoices: Vec, + } + let resp: Response = self.get("/invoices").await?; + Ok(resp.invoices) + } + + /// Get a specific invoice. + pub async fn get_invoice(&self, invoice_id: &str) -> Result { + 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 { + #[derive(Serialize)] + struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + payment_method: Option, + } + 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 { + self.get("/balance").await + } + + /// Add funds to account. + pub async fn add_funds( + &self, + amount: &str, + payment_method: &str, + ) -> Result { + #[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, + ) -> Result { + #[derive(Serialize)] + struct Request { + amount: String, + #[serde(flatten)] + options: Option, + } + self.post( + "/staking/stake", + &Request { + amount: amount.to_string(), + options, + }, + ) + .await + } + + /// Unstake tokens. + pub async fn unstake(&self, stake_id: &str) -> Result { + self.post::<(), UnstakeReceipt>( + &format!("/staking/stakes/{}/unstake", urlencoding::encode(stake_id)), + &(), + ) + .await + } + + /// Get staking rewards. + pub async fn get_staking_rewards(&self) -> Result { + self.get("/staking/rewards").await + } + + /// Claim staking rewards. + pub async fn claim_rewards( + &self, + stake_id: Option<&str>, + ) -> Result { + 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, EconomicsError> { + #[derive(Deserialize)] + struct Response { + stakes: Vec, + } + let resp: Response = self.get("/staking/stakes").await?; + Ok(resp.stakes) + } + + /// Get stake details. + pub async fn get_stake(&self, stake_id: &str) -> Result { + self.get(&format!( + "/staking/stakes/{}", + urlencoding::encode(stake_id) + )) + .await + } + + /// Get current APY for staking. + pub async fn get_staking_apy(&self) -> Result { + self.get("/staking/apy").await + } + + // ==================== Discount Operations ==================== + + /// Apply a discount code. + pub async fn apply_discount(&self, code: &str) -> Result { + #[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, EconomicsError> { + #[derive(Deserialize)] + struct Response { + discounts: Vec, + } + let resp: Response = self.get("/discounts").await?; + Ok(resp.discounts) + } + + /// Get active discounts on account. + pub async fn get_active_discounts(&self) -> Result, EconomicsError> { + #[derive(Deserialize)] + struct Response { + discounts: Vec, + } + 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::("/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 Deserialize<'de>>(&self, path: &str) -> Result { + 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 Deserialize<'de>>( + &self, + path: &str, + body: &B, + ) -> Result { + 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(&self, operation: F) -> Result + where + F: Fn() -> Fut, + Fut: std::future::Future>, + { + 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 Deserialize<'de>>( + &self, + resp: reqwest::Response, + ) -> Result { + 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, + code: Option, + } + + let (message, code) = match serde_json::from_str::(&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, + } + } +} diff --git a/sdk/rust/src/governance/mod.rs b/sdk/rust/src/governance/mod.rs new file mode 100644 index 0000000..09dfbb9 --- /dev/null +++ b/sdk/rust/src/governance/mod.rs @@ -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) -> 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) -> 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, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub quorum: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub threshold: Option, +} + +/// 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, + 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_tx_hash: Option, +} + +/// Proposal filter. +#[derive(Debug, Clone, Default)] +pub struct ProposalFilter { + pub status: Option, + pub proposal_type: Option, + pub proposer: Option, + pub limit: Option, + pub offset: Option, +} + +/// Vote. +#[derive(Debug, Clone, Serialize)] +pub struct Vote { + pub choice: VoteChoice, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +/// 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, + 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub delegated_to: Option, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub signers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub threshold: Option, + pub voting_period: i32, + pub quorum: String, + pub proposal_threshold: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub timelock_delay: Option, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub signers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub threshold: Option, + pub voting_period: i32, + pub quorum: String, + pub proposal_threshold: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub timelock_delay: Option, + 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, + 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, +} + +impl SynorGovernance { + /// Create a new Governance client. + pub fn new(config: GovernanceConfig) -> Result { + 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 { + self.post("/proposals", &draft).await + } + + /// Get a proposal by ID. + pub async fn get_proposal(&self, proposal_id: &str) -> Result { + self.get(&format!("/proposals/{}", urlencoding::encode(proposal_id))) + .await + } + + /// List proposals with optional filtering. + pub async fn list_proposals( + &self, + filter: Option, + ) -> Result, 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, + } + let resp: Response = self.get(&path).await?; + Ok(resp.proposals) + } + + /// Cancel a proposal. + pub async fn cancel_proposal(&self, proposal_id: &str) -> Result { + 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 { + 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 { + #[derive(Serialize)] + struct VoteRequest { + choice: VoteChoice, + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + weight: Option, + } + 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, + offset: Option, + ) -> Result, 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, + } + let resp: Response = self.get(&path).await?; + Ok(resp.votes) + } + + /// Delegate voting power. + pub async fn delegate( + &self, + to: &str, + amount: Option<&str>, + ) -> Result { + #[derive(Serialize)] + struct Request { + to: String, + #[serde(skip_serializing_if = "Option::is_none")] + amount: Option, + } + 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 { + #[derive(Serialize)] + struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + from: Option, + } + 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 { + 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 { + self.get("/voting/power").await + } + + // ==================== DAO Operations ==================== + + /// Create a new DAO. + pub async fn create_dao(&self, config: DaoConfig) -> Result { + self.post("/daos", &config).await + } + + /// Get a DAO by ID. + pub async fn get_dao(&self, dao_id: &str) -> Result { + self.get(&format!("/daos/{}", urlencoding::encode(dao_id))) + .await + } + + /// List DAOs. + pub async fn list_daos( + &self, + limit: Option, + offset: Option, + ) -> Result, 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, + } + 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, + offset: Option, + ) -> Result, 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, + } + 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 { + self.post("/vesting", &schedule).await + } + + /// Get a vesting contract. + pub async fn get_vesting_contract( + &self, + contract_id: &str, + ) -> Result { + self.get(&format!("/vesting/{}", urlencoding::encode(contract_id))) + .await + } + + /// List vesting contracts. + pub async fn list_vesting_contracts( + &self, + beneficiary: Option<&str>, + ) -> Result, GovernanceError> { + let path = match beneficiary { + Some(b) => format!("/vesting?beneficiary={}", urlencoding::encode(b)), + None => "/vesting".to_string(), + }; + #[derive(Deserialize)] + struct Response { + contracts: Vec, + } + let resp: Response = self.get(&path).await?; + Ok(resp.contracts) + } + + /// Claim vested tokens. + pub async fn claim_vested(&self, contract_id: &str) -> Result { + 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 { + 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 { + 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::("/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 Deserialize<'de>>(&self, path: &str) -> Result { + 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 Deserialize<'de>>( + &self, + path: &str, + body: &B, + ) -> Result { + 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(&self, operation: F) -> Result + where + F: Fn() -> Fut, + Fut: std::future::Future>, + { + 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 Deserialize<'de>>( + &self, + resp: reqwest::Response, + ) -> Result { + 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, + code: Option, + } + + let (message, code) = match serde_json::from_str::(&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, + } + } +} diff --git a/sdk/rust/src/mining/mod.rs b/sdk/rust/src/mining/mod.rs new file mode 100644 index 0000000..3341984 --- /dev/null +++ b/sdk/rust/src/mining/mod.rs @@ -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) -> 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) -> 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub algorithm: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub difficulty: Option, +} + +/// 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, + 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, + 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, + pub share: ShareInfo, + pub block_found: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reward: Option, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub temperature: Option, + 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, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub firmware: Option, +} + +/// Device configuration. +#[derive(Debug, Clone, Serialize)] +pub struct DeviceConfig { + pub enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub intensity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub power_limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub core_clock_offset: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_clock_offset: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fan_speed: Option, +} + +/// 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, + 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, +} + +/// Mining SDK error. +#[derive(Error, Debug)] +pub enum MiningError { + #[error("HTTP error: {message}")] + Http { + message: String, + code: Option, + 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>>, + closed: Arc, +} + +impl SynorMining { + /// Create a new Mining client. + pub fn new(config: MiningConfig) -> Result { + 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 { + 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, 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 { + 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 { + self.get("/mining/template").await + } + + /// Submit mined work. + pub async fn submit_work(&self, work: MinedWork) -> Result { + self.post("/mining/submit", &work).await + } + + /// Get work from pool. + pub async fn get_work(&self) -> Result { + self.get("/mining/getwork").await + } + + /// Start mining on all enabled devices. + pub async fn start_mining( + &self, + algorithm: Option<&str>, + ) -> Result { + #[derive(Serialize)] + struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + algorithm: Option, + } + self.post( + "/mining/start", + &Request { + algorithm: algorithm.map(String::from), + }, + ) + .await + } + + /// Stop mining on all devices. + pub async fn stop_mining(&self) -> Result { + #[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 { + self.get("/stats/hashrate").await + } + + /// Get mining stats. + pub async fn get_stats(&self) -> Result { + self.get("/stats").await + } + + /// Get earnings for a time period. + pub async fn get_earnings(&self, period: Option) -> Result { + 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, + offset: Option, + ) -> Result, 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, + } + let resp: Response = self.get(&path).await?; + Ok(resp.earnings) + } + + /// Get pool stats. + pub async fn get_pool_stats(&self) -> Result { + self.get("/pool/stats").await + } + + // ==================== GPU Management ==================== + + /// List all mining devices. + pub async fn list_devices(&self) -> Result, MiningError> { + #[derive(Deserialize)] + struct Response { + devices: Vec, + } + let resp: Response = self.get("/devices").await?; + Ok(resp.devices) + } + + /// Get device details. + pub async fn get_device(&self, device_id: &str) -> Result { + 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 { + 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 { + self.post::<(), MiningDevice>( + &format!("/devices/{}/enable", urlencoding::encode(device_id)), + &(), + ) + .await + } + + /// Disable a device. + pub async fn disable_device(&self, device_id: &str) -> Result { + 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 { + self.post::<(), MiningDevice>( + &format!("/devices/{}/reset", urlencoding::encode(device_id)), + &(), + ) + .await + } + + // ==================== Workers ==================== + + /// List all workers. + pub async fn list_workers(&self) -> Result, MiningError> { + #[derive(Deserialize)] + struct Response { + workers: Vec, + } + let resp: Response = self.get("/workers").await?; + Ok(resp.workers) + } + + /// Get worker details. + pub async fn get_worker(&self, worker_id: &str) -> Result { + self.get(&format!("/workers/{}", urlencoding::encode(worker_id))) + .await + } + + /// Create a new worker. + pub async fn create_worker(&self, name: &str) -> Result { + #[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, MiningError> { + #[derive(Deserialize)] + struct Response { + algorithms: Vec, + } + let resp: Response = self.get("/algorithms").await?; + Ok(resp.algorithms) + } + + /// Get current algorithm. + pub async fn get_current_algorithm(&self) -> Result { + self.get("/algorithms/current").await + } + + /// Switch to a different algorithm. + pub async fn switch_algorithm(&self, algorithm: &str) -> Result { + #[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::("/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 Deserialize<'de>>(&self, path: &str) -> Result { + 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 Deserialize<'de>>( + &self, + path: &str, + body: &B, + ) -> Result { + 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(&self, operation: F) -> Result + where + F: Fn() -> Fut, + Fut: std::future::Future>, + { + 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 Deserialize<'de>>( + &self, + resp: reqwest::Response, + ) -> Result { + 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, + code: Option, + } + + let (message, code) = match serde_json::from_str::(&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, + } + } +}