Phase 4 implementation - adds three new service SDKs: Economics SDK: - Pricing calculations for all service types - Billing and invoice management - Staking with APY rewards and vesting - Discount code system Governance SDK: - Proposal lifecycle (create, vote, execute, cancel) - Voting power with delegation support - DAO creation (token, multisig, hybrid types) - Vesting schedules with cliff periods Mining SDK: - Stratum pool connections - Block template retrieval and work submission - Hashrate and earnings statistics - GPU device management and configuration - Worker management and algorithm switching Languages: JavaScript, Python, Go, Rust Total: 12 SDK packages
723 lines
20 KiB
Go
723 lines
20 KiB
Go
// Package economics provides the Synor Economics SDK for Go.
|
|
// Pricing, billing, staking, and discount management.
|
|
package economics
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultEndpoint = "https://economics.synor.io/v1"
|
|
DefaultTimeout = 30 * time.Second
|
|
DefaultRetries = 3
|
|
)
|
|
|
|
// ServiceType represents a service type for pricing.
|
|
type ServiceType string
|
|
|
|
const (
|
|
ServiceCompute ServiceType = "compute"
|
|
ServiceStorage ServiceType = "storage"
|
|
ServiceDatabase ServiceType = "database"
|
|
ServiceHosting ServiceType = "hosting"
|
|
ServiceBridge ServiceType = "bridge"
|
|
ServiceRPC ServiceType = "rpc"
|
|
)
|
|
|
|
// BillingPeriod represents a billing period.
|
|
type BillingPeriod string
|
|
|
|
const (
|
|
PeriodHourly BillingPeriod = "hourly"
|
|
PeriodDaily BillingPeriod = "daily"
|
|
PeriodWeekly BillingPeriod = "weekly"
|
|
PeriodMonthly BillingPeriod = "monthly"
|
|
)
|
|
|
|
// StakeStatus represents stake status.
|
|
type StakeStatus string
|
|
|
|
const (
|
|
StakeActive StakeStatus = "active"
|
|
StakeUnstaking StakeStatus = "unstaking"
|
|
StakeWithdrawn StakeStatus = "withdrawn"
|
|
StakeSlashed StakeStatus = "slashed"
|
|
)
|
|
|
|
// DiscountType represents discount type.
|
|
type DiscountType string
|
|
|
|
const (
|
|
DiscountPercentage DiscountType = "percentage"
|
|
DiscountFixed DiscountType = "fixed"
|
|
DiscountVolume DiscountType = "volume"
|
|
DiscountReferral DiscountType = "referral"
|
|
)
|
|
|
|
// InvoiceStatus represents invoice status.
|
|
type InvoiceStatus string
|
|
|
|
const (
|
|
InvoicePending InvoiceStatus = "pending"
|
|
InvoicePaid InvoiceStatus = "paid"
|
|
InvoiceOverdue InvoiceStatus = "overdue"
|
|
InvoiceCancelled InvoiceStatus = "cancelled"
|
|
)
|
|
|
|
// Config for the Economics client.
|
|
type Config struct {
|
|
APIKey string
|
|
Endpoint string
|
|
Timeout time.Duration
|
|
Retries int
|
|
Debug bool
|
|
}
|
|
|
|
// UsageMetrics for pricing calculations.
|
|
type UsageMetrics struct {
|
|
ComputeHours *float64 `json:"compute_hours,omitempty"`
|
|
StorageBytes *int64 `json:"storage_bytes,omitempty"`
|
|
DatabaseOps *int64 `json:"database_ops,omitempty"`
|
|
HostingRequests *int64 `json:"hosting_requests,omitempty"`
|
|
BridgeTransfers *int64 `json:"bridge_transfers,omitempty"`
|
|
RPCCalls *int64 `json:"rpc_calls,omitempty"`
|
|
}
|
|
|
|
// ServiceUsage in a plan.
|
|
type ServiceUsage struct {
|
|
Service ServiceType `json:"service"`
|
|
Metrics UsageMetrics `json:"metrics"`
|
|
Tier string `json:"tier,omitempty"`
|
|
}
|
|
|
|
// UsagePlan for cost estimation.
|
|
type UsagePlan struct {
|
|
Services []ServiceUsage `json:"services"`
|
|
Period BillingPeriod `json:"period"`
|
|
StartDate *int64 `json:"start_date,omitempty"`
|
|
EndDate *int64 `json:"end_date,omitempty"`
|
|
}
|
|
|
|
// AppliedDiscount represents a discount applied to a price.
|
|
type AppliedDiscount struct {
|
|
Code string `json:"code"`
|
|
Type DiscountType `json:"type"`
|
|
Value string `json:"value"`
|
|
Savings string `json:"savings"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// TaxAmount represents tax.
|
|
type TaxAmount struct {
|
|
Name string `json:"name"`
|
|
Rate float64 `json:"rate"`
|
|
Amount string `json:"amount"`
|
|
}
|
|
|
|
// Price breakdown.
|
|
type Price struct {
|
|
Service ServiceType `json:"service"`
|
|
BasePrice string `json:"base_price"`
|
|
Quantity string `json:"quantity"`
|
|
Unit string `json:"unit"`
|
|
Subtotal string `json:"subtotal"`
|
|
Discounts []AppliedDiscount `json:"discounts"`
|
|
Total string `json:"total"`
|
|
Currency string `json:"currency"`
|
|
}
|
|
|
|
// CostEstimate for a usage plan.
|
|
type CostEstimate struct {
|
|
Services []Price `json:"services"`
|
|
Subtotal string `json:"subtotal"`
|
|
Discounts []AppliedDiscount `json:"discounts"`
|
|
Taxes []TaxAmount `json:"taxes"`
|
|
Total string `json:"total"`
|
|
Currency string `json:"currency"`
|
|
ValidUntil int64 `json:"valid_until"`
|
|
}
|
|
|
|
// UsageDetail for a single operation.
|
|
type UsageDetail struct {
|
|
Timestamp int64 `json:"timestamp"`
|
|
Operation string `json:"operation"`
|
|
Quantity float64 `json:"quantity"`
|
|
Unit string `json:"unit"`
|
|
Cost string `json:"cost"`
|
|
}
|
|
|
|
// ServiceUsageRecord for billing.
|
|
type ServiceUsageRecord struct {
|
|
Service ServiceType `json:"service"`
|
|
Metrics UsageMetrics `json:"metrics"`
|
|
Cost string `json:"cost"`
|
|
Details []UsageDetail `json:"details"`
|
|
}
|
|
|
|
// Usage record.
|
|
type Usage struct {
|
|
Period BillingPeriod `json:"period"`
|
|
StartDate int64 `json:"start_date"`
|
|
EndDate int64 `json:"end_date"`
|
|
Services []ServiceUsageRecord `json:"services"`
|
|
TotalCost string `json:"total_cost"`
|
|
Currency string `json:"currency"`
|
|
}
|
|
|
|
// InvoiceItem in an invoice.
|
|
type InvoiceItem struct {
|
|
Service ServiceType `json:"service"`
|
|
Description string `json:"description"`
|
|
Quantity string `json:"quantity"`
|
|
UnitPrice string `json:"unit_price"`
|
|
Total string `json:"total"`
|
|
}
|
|
|
|
// Invoice record.
|
|
type Invoice struct {
|
|
ID string `json:"id"`
|
|
Status InvoiceStatus `json:"status"`
|
|
Period BillingPeriod `json:"period"`
|
|
StartDate int64 `json:"start_date"`
|
|
EndDate int64 `json:"end_date"`
|
|
Items []InvoiceItem `json:"items"`
|
|
Subtotal string `json:"subtotal"`
|
|
Discounts []AppliedDiscount `json:"discounts"`
|
|
Taxes []TaxAmount `json:"taxes"`
|
|
Total string `json:"total"`
|
|
Currency string `json:"currency"`
|
|
DueDate int64 `json:"due_date"`
|
|
PaidAt *int64 `json:"paid_at,omitempty"`
|
|
PaymentMethod string `json:"payment_method,omitempty"`
|
|
}
|
|
|
|
// AccountBalance for billing.
|
|
type AccountBalance struct {
|
|
Available string `json:"available"`
|
|
Pending string `json:"pending"`
|
|
Reserved string `json:"reserved"`
|
|
Total string `json:"total"`
|
|
Currency string `json:"currency"`
|
|
CreditLimit string `json:"credit_limit,omitempty"`
|
|
LastUpdated int64 `json:"last_updated"`
|
|
}
|
|
|
|
// StakeReceipt from staking.
|
|
type StakeReceipt struct {
|
|
ID string `json:"id"`
|
|
Amount string `json:"amount"`
|
|
LockDuration int `json:"lock_duration"`
|
|
StartDate int64 `json:"start_date"`
|
|
EndDate int64 `json:"end_date"`
|
|
APY string `json:"apy"`
|
|
EstimatedRewards string `json:"estimated_rewards"`
|
|
TxHash string `json:"tx_hash"`
|
|
}
|
|
|
|
// UnstakeReceipt from unstaking.
|
|
type UnstakeReceipt struct {
|
|
ID string `json:"id"`
|
|
StakeID string `json:"stake_id"`
|
|
Amount string `json:"amount"`
|
|
Rewards string `json:"rewards"`
|
|
Total string `json:"total"`
|
|
UnbondingPeriod int `json:"unbonding_period"`
|
|
AvailableAt int64 `json:"available_at"`
|
|
TxHash string `json:"tx_hash"`
|
|
}
|
|
|
|
// StakeInfo for active stakes.
|
|
type StakeInfo struct {
|
|
ID string `json:"id"`
|
|
Amount string `json:"amount"`
|
|
Status StakeStatus `json:"status"`
|
|
LockDuration int `json:"lock_duration"`
|
|
StartDate int64 `json:"start_date"`
|
|
EndDate int64 `json:"end_date"`
|
|
APY string `json:"apy"`
|
|
EarnedRewards string `json:"earned_rewards"`
|
|
PendingRewards string `json:"pending_rewards"`
|
|
Validator string `json:"validator,omitempty"`
|
|
}
|
|
|
|
// StakeRewardDetail for individual stake rewards.
|
|
type StakeRewardDetail struct {
|
|
StakeID string `json:"stake_id"`
|
|
Earned string `json:"earned"`
|
|
Pending string `json:"pending"`
|
|
APY string `json:"apy"`
|
|
}
|
|
|
|
// StakingRewards summary.
|
|
type StakingRewards struct {
|
|
TotalEarned string `json:"total_earned"`
|
|
Pending string `json:"pending"`
|
|
Claimed string `json:"claimed"`
|
|
LastClaimDate *int64 `json:"last_claim_date,omitempty"`
|
|
NextClaimAvailable *int64 `json:"next_claim_available,omitempty"`
|
|
Stakes []StakeRewardDetail `json:"stakes"`
|
|
}
|
|
|
|
// Discount available or active.
|
|
type Discount struct {
|
|
Code string `json:"code"`
|
|
Type DiscountType `json:"type"`
|
|
Value string `json:"value"`
|
|
Description string `json:"description"`
|
|
ValidFrom int64 `json:"valid_from"`
|
|
ValidUntil int64 `json:"valid_until"`
|
|
MinPurchase string `json:"min_purchase,omitempty"`
|
|
MaxDiscount string `json:"max_discount,omitempty"`
|
|
ApplicableServices []ServiceType `json:"applicable_services"`
|
|
UsageLimit *int `json:"usage_limit,omitempty"`
|
|
UsedCount int `json:"used_count"`
|
|
}
|
|
|
|
// StakeOptions for staking.
|
|
type StakeOptions struct {
|
|
Validator string `json:"validator,omitempty"`
|
|
AutoCompound *bool `json:"auto_compound,omitempty"`
|
|
LockDuration *int `json:"lock_duration,omitempty"`
|
|
}
|
|
|
|
// StakingAPY response.
|
|
type StakingAPY struct {
|
|
APY string `json:"apy"`
|
|
MinLockDuration int `json:"min_lock_duration"`
|
|
MaxLockDuration int `json:"max_lock_duration"`
|
|
}
|
|
|
|
// Client for the Synor Economics SDK.
|
|
type Client struct {
|
|
config Config
|
|
client *http.Client
|
|
mu sync.RWMutex
|
|
closed bool
|
|
}
|
|
|
|
// NewClient creates a new Economics client.
|
|
func NewClient(config Config) *Client {
|
|
if config.Endpoint == "" {
|
|
config.Endpoint = DefaultEndpoint
|
|
}
|
|
if config.Timeout == 0 {
|
|
config.Timeout = DefaultTimeout
|
|
}
|
|
if config.Retries == 0 {
|
|
config.Retries = DefaultRetries
|
|
}
|
|
|
|
return &Client{
|
|
config: config,
|
|
client: &http.Client{Timeout: config.Timeout},
|
|
}
|
|
}
|
|
|
|
// ==================== Pricing Operations ====================
|
|
|
|
// GetPrice gets price for a service based on usage.
|
|
func (c *Client) GetPrice(ctx context.Context, service ServiceType, usage UsageMetrics) (*Price, error) {
|
|
var resp struct {
|
|
Price Price `json:"price"`
|
|
}
|
|
err := c.post(ctx, "/pricing/calculate", map[string]interface{}{
|
|
"service": service,
|
|
"usage": usage,
|
|
}, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Price, nil
|
|
}
|
|
|
|
// EstimateCost estimates cost for a usage plan.
|
|
func (c *Client) EstimateCost(ctx context.Context, plan UsagePlan) (*CostEstimate, error) {
|
|
var resp CostEstimate
|
|
err := c.post(ctx, "/pricing/estimate", plan, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetPricingTiers gets pricing tiers for a service.
|
|
func (c *Client) GetPricingTiers(ctx context.Context, service ServiceType) ([]Price, error) {
|
|
var resp struct {
|
|
Tiers []Price `json:"tiers"`
|
|
}
|
|
err := c.get(ctx, fmt.Sprintf("/pricing/%s/tiers", service), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Tiers, nil
|
|
}
|
|
|
|
// ==================== Usage & Billing Operations ====================
|
|
|
|
// GetUsage gets usage for a billing period.
|
|
func (c *Client) GetUsage(ctx context.Context, period *BillingPeriod) (*Usage, error) {
|
|
path := "/usage"
|
|
if period != nil {
|
|
path = fmt.Sprintf("/usage?period=%s", *period)
|
|
}
|
|
var resp Usage
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetUsageHistory gets usage history.
|
|
func (c *Client) GetUsageHistory(ctx context.Context, limit, offset *int) ([]Usage, error) {
|
|
params := url.Values{}
|
|
if limit != nil {
|
|
params.Set("limit", fmt.Sprintf("%d", *limit))
|
|
}
|
|
if offset != nil {
|
|
params.Set("offset", fmt.Sprintf("%d", *offset))
|
|
}
|
|
path := "/usage/history"
|
|
if len(params) > 0 {
|
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
|
}
|
|
var resp struct {
|
|
Usage []Usage `json:"usage"`
|
|
}
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Usage, nil
|
|
}
|
|
|
|
// GetInvoices gets all invoices.
|
|
func (c *Client) GetInvoices(ctx context.Context) ([]Invoice, error) {
|
|
var resp struct {
|
|
Invoices []Invoice `json:"invoices"`
|
|
}
|
|
err := c.get(ctx, "/invoices", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Invoices, nil
|
|
}
|
|
|
|
// GetInvoice gets a specific invoice.
|
|
func (c *Client) GetInvoice(ctx context.Context, invoiceID string) (*Invoice, error) {
|
|
var resp Invoice
|
|
err := c.get(ctx, fmt.Sprintf("/invoices/%s", url.PathEscape(invoiceID)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// PayInvoice pays an invoice.
|
|
func (c *Client) PayInvoice(ctx context.Context, invoiceID, paymentMethod string) (*Invoice, error) {
|
|
var resp Invoice
|
|
body := make(map[string]interface{})
|
|
if paymentMethod != "" {
|
|
body["payment_method"] = paymentMethod
|
|
}
|
|
err := c.post(ctx, fmt.Sprintf("/invoices/%s/pay", url.PathEscape(invoiceID)), body, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetBalance gets account balance.
|
|
func (c *Client) GetBalance(ctx context.Context) (*AccountBalance, error) {
|
|
var resp AccountBalance
|
|
err := c.get(ctx, "/balance", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// AddFunds adds funds to account.
|
|
func (c *Client) AddFunds(ctx context.Context, amount, paymentMethod string) (*AccountBalance, error) {
|
|
var resp AccountBalance
|
|
err := c.post(ctx, "/balance/deposit", map[string]interface{}{
|
|
"amount": amount,
|
|
"payment_method": paymentMethod,
|
|
}, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ==================== Staking Operations ====================
|
|
|
|
// Stake stakes tokens.
|
|
func (c *Client) Stake(ctx context.Context, amount string, options *StakeOptions) (*StakeReceipt, error) {
|
|
body := map[string]interface{}{"amount": amount}
|
|
if options != nil {
|
|
if options.Validator != "" {
|
|
body["validator"] = options.Validator
|
|
}
|
|
if options.AutoCompound != nil {
|
|
body["auto_compound"] = *options.AutoCompound
|
|
}
|
|
if options.LockDuration != nil {
|
|
body["lock_duration"] = *options.LockDuration
|
|
}
|
|
}
|
|
var resp StakeReceipt
|
|
err := c.post(ctx, "/staking/stake", body, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// Unstake unstakes tokens.
|
|
func (c *Client) Unstake(ctx context.Context, stakeID string) (*UnstakeReceipt, error) {
|
|
var resp UnstakeReceipt
|
|
err := c.post(ctx, fmt.Sprintf("/staking/stakes/%s/unstake", url.PathEscape(stakeID)), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetStakingRewards gets staking rewards.
|
|
func (c *Client) GetStakingRewards(ctx context.Context) (*StakingRewards, error) {
|
|
var resp StakingRewards
|
|
err := c.get(ctx, "/staking/rewards", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ClaimRewards claims staking rewards.
|
|
func (c *Client) ClaimRewards(ctx context.Context, stakeID string) (*StakingRewards, error) {
|
|
path := "/staking/rewards/claim"
|
|
if stakeID != "" {
|
|
path = fmt.Sprintf("/staking/stakes/%s/claim", url.PathEscape(stakeID))
|
|
}
|
|
var resp StakingRewards
|
|
err := c.post(ctx, path, nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ListStakes lists active stakes.
|
|
func (c *Client) ListStakes(ctx context.Context) ([]StakeInfo, error) {
|
|
var resp struct {
|
|
Stakes []StakeInfo `json:"stakes"`
|
|
}
|
|
err := c.get(ctx, "/staking/stakes", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Stakes, nil
|
|
}
|
|
|
|
// GetStake gets stake details.
|
|
func (c *Client) GetStake(ctx context.Context, stakeID string) (*StakeInfo, error) {
|
|
var resp StakeInfo
|
|
err := c.get(ctx, fmt.Sprintf("/staking/stakes/%s", url.PathEscape(stakeID)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetStakingAPY gets current APY for staking.
|
|
func (c *Client) GetStakingAPY(ctx context.Context) (*StakingAPY, error) {
|
|
var resp StakingAPY
|
|
err := c.get(ctx, "/staking/apy", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ==================== Discount Operations ====================
|
|
|
|
// ApplyDiscount applies a discount code.
|
|
func (c *Client) ApplyDiscount(ctx context.Context, code string) (*Discount, error) {
|
|
var resp Discount
|
|
err := c.post(ctx, "/discounts/apply", map[string]interface{}{"code": code}, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetAvailableDiscounts gets available discounts.
|
|
func (c *Client) GetAvailableDiscounts(ctx context.Context) ([]Discount, error) {
|
|
var resp struct {
|
|
Discounts []Discount `json:"discounts"`
|
|
}
|
|
err := c.get(ctx, "/discounts", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Discounts, nil
|
|
}
|
|
|
|
// GetActiveDiscounts gets active discounts on account.
|
|
func (c *Client) GetActiveDiscounts(ctx context.Context) ([]Discount, error) {
|
|
var resp struct {
|
|
Discounts []Discount `json:"discounts"`
|
|
}
|
|
err := c.get(ctx, "/discounts/active", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Discounts, nil
|
|
}
|
|
|
|
// RemoveDiscount removes a discount.
|
|
func (c *Client) RemoveDiscount(ctx context.Context, code string) error {
|
|
return c.delete(ctx, fmt.Sprintf("/discounts/%s", url.PathEscape(code)))
|
|
}
|
|
|
|
// ==================== Lifecycle ====================
|
|
|
|
// HealthCheck performs a health check.
|
|
func (c *Client) HealthCheck(ctx context.Context) bool {
|
|
var resp struct {
|
|
Status string `json:"status"`
|
|
}
|
|
err := c.get(ctx, "/health", &resp)
|
|
return err == nil && resp.Status == "healthy"
|
|
}
|
|
|
|
// IsClosed returns true if the client is closed.
|
|
func (c *Client) IsClosed() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.closed
|
|
}
|
|
|
|
// Close closes the client.
|
|
func (c *Client) Close() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.closed = true
|
|
}
|
|
|
|
// ==================== Private Methods ====================
|
|
|
|
func (c *Client) get(ctx context.Context, path string, result interface{}) error {
|
|
return c.execute(ctx, "GET", path, nil, result)
|
|
}
|
|
|
|
func (c *Client) post(ctx context.Context, path string, body, result interface{}) error {
|
|
return c.execute(ctx, "POST", path, body, result)
|
|
}
|
|
|
|
func (c *Client) delete(ctx context.Context, path string) error {
|
|
return c.execute(ctx, "DELETE", path, nil, nil)
|
|
}
|
|
|
|
func (c *Client) execute(ctx context.Context, method, path string, body, result interface{}) error {
|
|
c.mu.RLock()
|
|
if c.closed {
|
|
c.mu.RUnlock()
|
|
return &EconomicsError{Message: "Client has been closed"}
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
var lastErr error
|
|
for attempt := 0; attempt < c.config.Retries; attempt++ {
|
|
err := c.doRequest(ctx, method, path, body, result)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
if c.config.Debug {
|
|
fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
|
|
}
|
|
if attempt < c.config.Retries-1 {
|
|
time.Sleep(time.Duration(1<<attempt) * time.Second)
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func (c *Client) doRequest(ctx context.Context, method, path string, body, result interface{}) error {
|
|
url := c.config.Endpoint + path
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bodyReader = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-SDK-Version", "go/0.1.0")
|
|
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
var errResp struct {
|
|
Message string `json:"message"`
|
|
Code string `json:"code"`
|
|
}
|
|
json.Unmarshal(respBody, &errResp)
|
|
msg := errResp.Message
|
|
if msg == "" {
|
|
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return &EconomicsError{
|
|
Message: msg,
|
|
Code: errResp.Code,
|
|
StatusCode: resp.StatusCode,
|
|
}
|
|
}
|
|
|
|
if result != nil && len(respBody) > 0 {
|
|
return json.Unmarshal(respBody, result)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EconomicsError represents an error from the Economics API.
|
|
type EconomicsError struct {
|
|
Message string
|
|
Code string
|
|
StatusCode int
|
|
}
|
|
|
|
func (e *EconomicsError) Error() string {
|
|
if e.Code != "" {
|
|
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
|
|
}
|
|
return e.Message
|
|
}
|