// 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 }