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
722 lines
20 KiB
Go
722 lines
20 KiB
Go
// Package governance provides the Synor Governance SDK for Go.
|
|
// Proposals, voting, DAOs, and vesting.
|
|
package governance
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultEndpoint = "https://governance.synor.io/v1"
|
|
DefaultTimeout = 30 * time.Second
|
|
DefaultRetries = 3
|
|
)
|
|
|
|
// ProposalStatus represents proposal status.
|
|
type ProposalStatus string
|
|
|
|
const (
|
|
ProposalDraft ProposalStatus = "draft"
|
|
ProposalActive ProposalStatus = "active"
|
|
ProposalPassed ProposalStatus = "passed"
|
|
ProposalRejected ProposalStatus = "rejected"
|
|
ProposalExecuted ProposalStatus = "executed"
|
|
ProposalCancelled ProposalStatus = "cancelled"
|
|
ProposalExpired ProposalStatus = "expired"
|
|
)
|
|
|
|
// VoteChoice represents vote choice.
|
|
type VoteChoice string
|
|
|
|
const (
|
|
VoteFor VoteChoice = "for"
|
|
VoteAgainst VoteChoice = "against"
|
|
VoteAbstain VoteChoice = "abstain"
|
|
)
|
|
|
|
// ProposalType represents proposal type.
|
|
type ProposalType string
|
|
|
|
const (
|
|
ProposalParameterChange ProposalType = "parameter_change"
|
|
ProposalTreasurySpend ProposalType = "treasury_spend"
|
|
ProposalUpgrade ProposalType = "upgrade"
|
|
ProposalText ProposalType = "text"
|
|
ProposalCustom ProposalType = "custom"
|
|
)
|
|
|
|
// DaoType represents DAO type.
|
|
type DaoType string
|
|
|
|
const (
|
|
DaoToken DaoType = "token"
|
|
DaoMultisig DaoType = "multisig"
|
|
DaoHybrid DaoType = "hybrid"
|
|
)
|
|
|
|
// VestingStatus represents vesting status.
|
|
type VestingStatus string
|
|
|
|
const (
|
|
VestingActive VestingStatus = "active"
|
|
VestingPaused VestingStatus = "paused"
|
|
VestingCompleted VestingStatus = "completed"
|
|
VestingCancelled VestingStatus = "cancelled"
|
|
)
|
|
|
|
// Config for the Governance client.
|
|
type Config struct {
|
|
APIKey string
|
|
Endpoint string
|
|
Timeout time.Duration
|
|
Retries int
|
|
Debug bool
|
|
}
|
|
|
|
// ProposalAction represents a proposal action.
|
|
type ProposalAction struct {
|
|
Target string `json:"target"`
|
|
Value string `json:"value"`
|
|
Calldata string `json:"calldata"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// ProposalDraft for creating proposals.
|
|
type ProposalDraft struct {
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Type ProposalType `json:"type"`
|
|
Actions []ProposalAction `json:"actions,omitempty"`
|
|
StartTime *int64 `json:"start_time,omitempty"`
|
|
EndTime *int64 `json:"end_time,omitempty"`
|
|
Quorum string `json:"quorum,omitempty"`
|
|
Threshold string `json:"threshold,omitempty"`
|
|
}
|
|
|
|
// Proposal represents a governance proposal.
|
|
type Proposal struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Type ProposalType `json:"type"`
|
|
Status ProposalStatus `json:"status"`
|
|
Proposer string `json:"proposer"`
|
|
Actions []ProposalAction `json:"actions"`
|
|
StartTime int64 `json:"start_time"`
|
|
EndTime int64 `json:"end_time"`
|
|
Quorum string `json:"quorum"`
|
|
Threshold string `json:"threshold"`
|
|
ForVotes string `json:"for_votes"`
|
|
AgainstVotes string `json:"against_votes"`
|
|
AbstainVotes string `json:"abstain_votes"`
|
|
TotalVotes string `json:"total_votes"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
ExecutedAt *int64 `json:"executed_at,omitempty"`
|
|
ExecutionTxHash string `json:"execution_tx_hash,omitempty"`
|
|
}
|
|
|
|
// ProposalFilter for filtering proposals.
|
|
type ProposalFilter struct {
|
|
Status *ProposalStatus
|
|
Type *ProposalType
|
|
Proposer string
|
|
Limit *int
|
|
Offset *int
|
|
}
|
|
|
|
// Vote represents a vote.
|
|
type Vote struct {
|
|
Choice VoteChoice `json:"choice"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// VoteReceipt represents a vote receipt.
|
|
type VoteReceipt struct {
|
|
ProposalID string `json:"proposal_id"`
|
|
Voter string `json:"voter"`
|
|
Choice VoteChoice `json:"choice"`
|
|
Weight string `json:"weight"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
TxHash string `json:"tx_hash"`
|
|
}
|
|
|
|
// DelegationInfo represents delegation information.
|
|
type DelegationInfo struct {
|
|
Address string `json:"address"`
|
|
Amount string `json:"amount"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
// DelegationReceipt represents a delegation receipt.
|
|
type DelegationReceipt struct {
|
|
Delegator string `json:"delegator"`
|
|
Delegatee string `json:"delegatee"`
|
|
Amount string `json:"amount"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
TxHash string `json:"tx_hash"`
|
|
}
|
|
|
|
// VotingPower represents voting power.
|
|
type VotingPower struct {
|
|
Address string `json:"address"`
|
|
OwnPower string `json:"own_power"`
|
|
DelegatedPower string `json:"delegated_power"`
|
|
TotalPower string `json:"total_power"`
|
|
DelegatedFrom []DelegationInfo `json:"delegated_from"`
|
|
DelegatedTo *DelegationInfo `json:"delegated_to,omitempty"`
|
|
}
|
|
|
|
// DaoConfig for creating DAOs.
|
|
type DaoConfig struct {
|
|
Name string `json:"name"`
|
|
Type DaoType `json:"type"`
|
|
TokenAddress string `json:"token_address,omitempty"`
|
|
Signers []string `json:"signers,omitempty"`
|
|
Threshold *int `json:"threshold,omitempty"`
|
|
VotingPeriod int `json:"voting_period"`
|
|
Quorum string `json:"quorum"`
|
|
ProposalThreshold string `json:"proposal_threshold"`
|
|
TimelockDelay *int `json:"timelock_delay,omitempty"`
|
|
}
|
|
|
|
// Dao represents a DAO.
|
|
type Dao struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type DaoType `json:"type"`
|
|
TokenAddress string `json:"token_address,omitempty"`
|
|
Signers []string `json:"signers,omitempty"`
|
|
Threshold *int `json:"threshold,omitempty"`
|
|
VotingPeriod int `json:"voting_period"`
|
|
Quorum string `json:"quorum"`
|
|
ProposalThreshold string `json:"proposal_threshold"`
|
|
TimelockDelay *int `json:"timelock_delay,omitempty"`
|
|
Treasury string `json:"treasury"`
|
|
TotalProposals int `json:"total_proposals"`
|
|
ActiveProposals int `json:"active_proposals"`
|
|
TotalMembers int `json:"total_members"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
}
|
|
|
|
// VestingSchedule for creating vesting contracts.
|
|
type VestingSchedule struct {
|
|
Beneficiary string `json:"beneficiary"`
|
|
TotalAmount string `json:"total_amount"`
|
|
StartTime int64 `json:"start_time"`
|
|
CliffDuration int `json:"cliff_duration"`
|
|
VestingDuration int `json:"vesting_duration"`
|
|
Revocable bool `json:"revocable"`
|
|
}
|
|
|
|
// VestingContract represents a vesting contract.
|
|
type VestingContract struct {
|
|
ID string `json:"id"`
|
|
Beneficiary string `json:"beneficiary"`
|
|
TotalAmount string `json:"total_amount"`
|
|
ReleasedAmount string `json:"released_amount"`
|
|
VestedAmount string `json:"vested_amount"`
|
|
StartTime int64 `json:"start_time"`
|
|
CliffTime int64 `json:"cliff_time"`
|
|
EndTime int64 `json:"end_time"`
|
|
Revocable bool `json:"revocable"`
|
|
Status VestingStatus `json:"status"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
TxHash string `json:"tx_hash"`
|
|
}
|
|
|
|
// ClaimResult represents a claim result.
|
|
type ClaimResult struct {
|
|
VestingID string `json:"vesting_id"`
|
|
Amount string `json:"amount"`
|
|
Recipient string `json:"recipient"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
TxHash string `json:"tx_hash"`
|
|
}
|
|
|
|
// GovernanceTransaction represents a transaction result.
|
|
type GovernanceTransaction struct {
|
|
TxHash string `json:"tx_hash"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
BlockNumber int64 `json:"block_number"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// DaoMember represents a DAO member.
|
|
type DaoMember struct {
|
|
Address string `json:"address"`
|
|
Power string `json:"power"`
|
|
}
|
|
|
|
// ClaimableAmount represents claimable amount.
|
|
type ClaimableAmount struct {
|
|
Claimable string `json:"claimable"`
|
|
Vested string `json:"vested"`
|
|
}
|
|
|
|
// Client for the Synor Governance SDK.
|
|
type Client struct {
|
|
config Config
|
|
client *http.Client
|
|
mu sync.RWMutex
|
|
closed bool
|
|
}
|
|
|
|
// NewClient creates a new Governance client.
|
|
func NewClient(config Config) *Client {
|
|
if config.Endpoint == "" {
|
|
config.Endpoint = DefaultEndpoint
|
|
}
|
|
if config.Timeout == 0 {
|
|
config.Timeout = DefaultTimeout
|
|
}
|
|
if config.Retries == 0 {
|
|
config.Retries = DefaultRetries
|
|
}
|
|
|
|
return &Client{
|
|
config: config,
|
|
client: &http.Client{Timeout: config.Timeout},
|
|
}
|
|
}
|
|
|
|
// ==================== Proposal Operations ====================
|
|
|
|
// CreateProposal creates a new proposal.
|
|
func (c *Client) CreateProposal(ctx context.Context, draft ProposalDraft) (*Proposal, error) {
|
|
var resp Proposal
|
|
err := c.post(ctx, "/proposals", draft, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetProposal gets a proposal by ID.
|
|
func (c *Client) GetProposal(ctx context.Context, proposalID string) (*Proposal, error) {
|
|
var resp Proposal
|
|
err := c.get(ctx, fmt.Sprintf("/proposals/%s", url.PathEscape(proposalID)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ListProposals lists proposals with optional filtering.
|
|
func (c *Client) ListProposals(ctx context.Context, filter *ProposalFilter) ([]Proposal, error) {
|
|
params := url.Values{}
|
|
if filter != nil {
|
|
if filter.Status != nil {
|
|
params.Set("status", string(*filter.Status))
|
|
}
|
|
if filter.Type != nil {
|
|
params.Set("type", string(*filter.Type))
|
|
}
|
|
if filter.Proposer != "" {
|
|
params.Set("proposer", filter.Proposer)
|
|
}
|
|
if filter.Limit != nil {
|
|
params.Set("limit", fmt.Sprintf("%d", *filter.Limit))
|
|
}
|
|
if filter.Offset != nil {
|
|
params.Set("offset", fmt.Sprintf("%d", *filter.Offset))
|
|
}
|
|
}
|
|
path := "/proposals"
|
|
if len(params) > 0 {
|
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
|
}
|
|
var resp struct {
|
|
Proposals []Proposal `json:"proposals"`
|
|
}
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Proposals, nil
|
|
}
|
|
|
|
// CancelProposal cancels a proposal.
|
|
func (c *Client) CancelProposal(ctx context.Context, proposalID string) (*Proposal, error) {
|
|
var resp Proposal
|
|
err := c.post(ctx, fmt.Sprintf("/proposals/%s/cancel", url.PathEscape(proposalID)), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ExecuteProposal executes a passed proposal.
|
|
func (c *Client) ExecuteProposal(ctx context.Context, proposalID string) (*GovernanceTransaction, error) {
|
|
var resp GovernanceTransaction
|
|
err := c.post(ctx, fmt.Sprintf("/proposals/%s/execute", url.PathEscape(proposalID)), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ==================== Voting Operations ====================
|
|
|
|
// Vote votes on a proposal.
|
|
func (c *Client) Vote(ctx context.Context, proposalID string, vote Vote, weight string) (*VoteReceipt, error) {
|
|
body := map[string]interface{}{
|
|
"choice": vote.Choice,
|
|
}
|
|
if vote.Reason != "" {
|
|
body["reason"] = vote.Reason
|
|
}
|
|
if weight != "" {
|
|
body["weight"] = weight
|
|
}
|
|
var resp VoteReceipt
|
|
err := c.post(ctx, fmt.Sprintf("/proposals/%s/vote", url.PathEscape(proposalID)), body, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetVotes gets votes for a proposal.
|
|
func (c *Client) GetVotes(ctx context.Context, proposalID string, limit, offset *int) ([]VoteReceipt, error) {
|
|
params := url.Values{}
|
|
if limit != nil {
|
|
params.Set("limit", fmt.Sprintf("%d", *limit))
|
|
}
|
|
if offset != nil {
|
|
params.Set("offset", fmt.Sprintf("%d", *offset))
|
|
}
|
|
path := fmt.Sprintf("/proposals/%s/votes", url.PathEscape(proposalID))
|
|
if len(params) > 0 {
|
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
|
}
|
|
var resp struct {
|
|
Votes []VoteReceipt `json:"votes"`
|
|
}
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Votes, nil
|
|
}
|
|
|
|
// Delegate delegates voting power.
|
|
func (c *Client) Delegate(ctx context.Context, to string, amount string) (*DelegationReceipt, error) {
|
|
body := map[string]interface{}{"to": to}
|
|
if amount != "" {
|
|
body["amount"] = amount
|
|
}
|
|
var resp DelegationReceipt
|
|
err := c.post(ctx, "/voting/delegate", body, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// Undelegate undelegates voting power.
|
|
func (c *Client) Undelegate(ctx context.Context, from string) (*DelegationReceipt, error) {
|
|
body := make(map[string]interface{})
|
|
if from != "" {
|
|
body["from"] = from
|
|
}
|
|
var resp DelegationReceipt
|
|
err := c.post(ctx, "/voting/undelegate", body, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetVotingPower gets voting power for an address.
|
|
func (c *Client) GetVotingPower(ctx context.Context, address string) (*VotingPower, error) {
|
|
var resp VotingPower
|
|
err := c.get(ctx, fmt.Sprintf("/voting/power/%s", url.PathEscape(address)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetMyVotingPower gets voting power for the authenticated user.
|
|
func (c *Client) GetMyVotingPower(ctx context.Context) (*VotingPower, error) {
|
|
var resp VotingPower
|
|
err := c.get(ctx, "/voting/power", &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ==================== DAO Operations ====================
|
|
|
|
// CreateDao creates a new DAO.
|
|
func (c *Client) CreateDao(ctx context.Context, config DaoConfig) (*Dao, error) {
|
|
var resp Dao
|
|
err := c.post(ctx, "/daos", config, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetDao gets a DAO by ID.
|
|
func (c *Client) GetDao(ctx context.Context, daoID string) (*Dao, error) {
|
|
var resp Dao
|
|
err := c.get(ctx, fmt.Sprintf("/daos/%s", url.PathEscape(daoID)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ListDaos lists DAOs.
|
|
func (c *Client) ListDaos(ctx context.Context, limit, offset *int) ([]Dao, error) {
|
|
params := url.Values{}
|
|
if limit != nil {
|
|
params.Set("limit", fmt.Sprintf("%d", *limit))
|
|
}
|
|
if offset != nil {
|
|
params.Set("offset", fmt.Sprintf("%d", *offset))
|
|
}
|
|
path := "/daos"
|
|
if len(params) > 0 {
|
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
|
}
|
|
var resp struct {
|
|
Daos []Dao `json:"daos"`
|
|
}
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Daos, nil
|
|
}
|
|
|
|
// GetDaoMembers gets DAO members.
|
|
func (c *Client) GetDaoMembers(ctx context.Context, daoID string, limit, offset *int) ([]DaoMember, error) {
|
|
params := url.Values{}
|
|
if limit != nil {
|
|
params.Set("limit", fmt.Sprintf("%d", *limit))
|
|
}
|
|
if offset != nil {
|
|
params.Set("offset", fmt.Sprintf("%d", *offset))
|
|
}
|
|
path := fmt.Sprintf("/daos/%s/members", url.PathEscape(daoID))
|
|
if len(params) > 0 {
|
|
path = fmt.Sprintf("%s?%s", path, params.Encode())
|
|
}
|
|
var resp struct {
|
|
Members []DaoMember `json:"members"`
|
|
}
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Members, nil
|
|
}
|
|
|
|
// ==================== Vesting Operations ====================
|
|
|
|
// CreateVestingSchedule creates a vesting schedule.
|
|
func (c *Client) CreateVestingSchedule(ctx context.Context, schedule VestingSchedule) (*VestingContract, error) {
|
|
var resp VestingContract
|
|
err := c.post(ctx, "/vesting", schedule, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetVestingContract gets a vesting contract.
|
|
func (c *Client) GetVestingContract(ctx context.Context, contractID string) (*VestingContract, error) {
|
|
var resp VestingContract
|
|
err := c.get(ctx, fmt.Sprintf("/vesting/%s", url.PathEscape(contractID)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ListVestingContracts lists vesting contracts.
|
|
func (c *Client) ListVestingContracts(ctx context.Context, beneficiary string) ([]VestingContract, error) {
|
|
path := "/vesting"
|
|
if beneficiary != "" {
|
|
path = fmt.Sprintf("%s?beneficiary=%s", path, url.QueryEscape(beneficiary))
|
|
}
|
|
var resp struct {
|
|
Contracts []VestingContract `json:"contracts"`
|
|
}
|
|
err := c.get(ctx, path, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Contracts, nil
|
|
}
|
|
|
|
// ClaimVested claims vested tokens.
|
|
func (c *Client) ClaimVested(ctx context.Context, contractID string) (*ClaimResult, error) {
|
|
var resp ClaimResult
|
|
err := c.post(ctx, fmt.Sprintf("/vesting/%s/claim", url.PathEscape(contractID)), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// RevokeVesting revokes a vesting contract.
|
|
func (c *Client) RevokeVesting(ctx context.Context, contractID string) (*VestingContract, error) {
|
|
var resp VestingContract
|
|
err := c.post(ctx, fmt.Sprintf("/vesting/%s/revoke", url.PathEscape(contractID)), nil, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetClaimableAmount gets claimable amount for a vesting contract.
|
|
func (c *Client) GetClaimableAmount(ctx context.Context, contractID string) (*ClaimableAmount, error) {
|
|
var resp ClaimableAmount
|
|
err := c.get(ctx, fmt.Sprintf("/vesting/%s/claimable", url.PathEscape(contractID)), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ==================== Lifecycle ====================
|
|
|
|
// HealthCheck performs a health check.
|
|
func (c *Client) HealthCheck(ctx context.Context) bool {
|
|
var resp struct {
|
|
Status string `json:"status"`
|
|
}
|
|
err := c.get(ctx, "/health", &resp)
|
|
return err == nil && resp.Status == "healthy"
|
|
}
|
|
|
|
// IsClosed returns true if the client is closed.
|
|
func (c *Client) IsClosed() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.closed
|
|
}
|
|
|
|
// Close closes the client.
|
|
func (c *Client) Close() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.closed = true
|
|
}
|
|
|
|
// ==================== Private Methods ====================
|
|
|
|
func (c *Client) get(ctx context.Context, path string, result interface{}) error {
|
|
return c.execute(ctx, "GET", path, nil, result)
|
|
}
|
|
|
|
func (c *Client) post(ctx context.Context, path string, body, result interface{}) error {
|
|
return c.execute(ctx, "POST", path, body, result)
|
|
}
|
|
|
|
func (c *Client) execute(ctx context.Context, method, path string, body, result interface{}) error {
|
|
c.mu.RLock()
|
|
if c.closed {
|
|
c.mu.RUnlock()
|
|
return &GovernanceError{Message: "Client has been closed"}
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
var lastErr error
|
|
for attempt := 0; attempt < c.config.Retries; attempt++ {
|
|
err := c.doRequest(ctx, method, path, body, result)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
if c.config.Debug {
|
|
fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
|
|
}
|
|
if attempt < c.config.Retries-1 {
|
|
time.Sleep(time.Duration(1<<attempt) * time.Second)
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func (c *Client) doRequest(ctx context.Context, method, path string, body, result interface{}) error {
|
|
url := c.config.Endpoint + path
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bodyReader = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-SDK-Version", "go/0.1.0")
|
|
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
var errResp struct {
|
|
Message string `json:"message"`
|
|
Code string `json:"code"`
|
|
}
|
|
json.Unmarshal(respBody, &errResp)
|
|
msg := errResp.Message
|
|
if msg == "" {
|
|
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return &GovernanceError{
|
|
Message: msg,
|
|
Code: errResp.Code,
|
|
StatusCode: resp.StatusCode,
|
|
}
|
|
}
|
|
|
|
if result != nil && len(respBody) > 0 {
|
|
return json.Unmarshal(respBody, result)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GovernanceError represents an error from the Governance API.
|
|
type GovernanceError struct {
|
|
Message string
|
|
Code string
|
|
StatusCode int
|
|
}
|
|
|
|
func (e *GovernanceError) Error() string {
|
|
if e.Code != "" {
|
|
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
|
|
}
|
|
return e.Message
|
|
}
|