// Package rpc provides a Go SDK for Synor blockchain RPC operations. // // Query blocks, transactions, and chain state with WebSocket subscription support. // // Example: // // client := rpc.NewClient("your-api-key") // block, err := client.GetLatestBlock(ctx) // fmt.Println("Height:", block.Height) package rpc import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "github.com/gorilla/websocket" ) // Version of the SDK. const Version = "0.1.0" // Default endpoints. const ( DefaultEndpoint = "https://rpc.synor.cc/api/v1" DefaultWSEndpoint = "wss://rpc.synor.cc/ws" ) // Network type. type Network string const ( Mainnet Network = "mainnet" Testnet Network = "testnet" ) // Priority levels. type Priority string const ( Low Priority = "low" Medium Priority = "medium" High Priority = "high" Urgent Priority = "urgent" ) // TransactionStatus represents transaction states. type TransactionStatus string const ( Pending TransactionStatus = "pending" Confirmed TransactionStatus = "confirmed" Failed TransactionStatus = "failed" Replaced TransactionStatus = "replaced" ) // Config holds client configuration. type Config struct { APIKey string Endpoint string WSEndpoint string Network Network Timeout time.Duration Debug bool } // DefaultConfig returns a default configuration. func DefaultConfig(apiKey string) Config { return Config{ APIKey: apiKey, Endpoint: DefaultEndpoint, WSEndpoint: DefaultWSEndpoint, Network: Mainnet, Timeout: 30 * time.Second, Debug: false, } } // Client is the Synor RPC client. type Client struct { config Config httpClient *http.Client wsConn *websocket.Conn subs map[string]func(json.RawMessage) } // NewClient creates a new client with the given API key. func NewClient(apiKey string) *Client { return NewClientWithConfig(DefaultConfig(apiKey)) } // NewClientWithConfig creates a new client with custom configuration. func NewClientWithConfig(config Config) *Client { return &Client{ config: config, httpClient: &http.Client{ Timeout: config.Timeout, }, subs: make(map[string]func(json.RawMessage)), } } // BlockHeader represents a block header. type BlockHeader struct { Hash string `json:"hash"` Height int64 `json:"height"` Version int `json:"version"` PreviousHash string `json:"previousHash"` MerkleRoot string `json:"merkleRoot"` Timestamp int64 `json:"timestamp"` Difficulty string `json:"difficulty"` Nonce uint64 `json:"nonce"` } // Block represents a full block. type Block struct { BlockHeader Transactions []string `json:"transactions"` Size int `json:"size"` Weight int `json:"weight"` TxCount int `json:"txCount"` } // Transaction represents a transaction. type Transaction struct { TxID string `json:"txid"` BlockHash string `json:"blockHash,omitempty"` BlockHeight int64 `json:"blockHeight,omitempty"` Confirmations int `json:"confirmations"` Timestamp int64 `json:"timestamp,omitempty"` Status TransactionStatus `json:"status"` Raw string `json:"raw,omitempty"` Size int `json:"size"` Fee string `json:"fee"` } // FeeEstimate represents a fee estimation. type FeeEstimate struct { Priority Priority `json:"priority"` FeeRate string `json:"feeRate"` EstimatedBlocks int `json:"estimatedBlocks"` } // ChainInfo represents chain information. type ChainInfo struct { Chain string `json:"chain"` Network string `json:"network"` Height int64 `json:"height"` BestBlockHash string `json:"bestBlockHash"` Difficulty string `json:"difficulty"` MedianTime int64 `json:"medianTime"` ChainWork string `json:"chainWork"` Syncing bool `json:"syncing"` SyncProgress float64 `json:"syncProgress"` } // MempoolInfo represents mempool information. type MempoolInfo struct { Size int `json:"size"` Bytes int `json:"bytes"` Usage int `json:"usage"` MaxMempool int `json:"maxMempool"` MinFee string `json:"minFee"` } // UTXO represents an unspent transaction output. type UTXO struct { TxID string `json:"txid"` Vout int `json:"vout"` Amount string `json:"amount"` Address string `json:"address"` Confirmations int `json:"confirmations"` ScriptPubKey string `json:"scriptPubKey,omitempty"` } // Balance represents balance information. type Balance struct { Confirmed string `json:"confirmed"` Unconfirmed string `json:"unconfirmed"` Total string `json:"total"` } // GetBlock gets a block by hash or height. func (c *Client) GetBlock(ctx context.Context, hashOrHeight interface{}) (*Block, error) { var path string switch v := hashOrHeight.(type) { case int, int64: path = fmt.Sprintf("/blocks/height/%d", v) case string: path = fmt.Sprintf("/blocks/%s", v) default: return nil, fmt.Errorf("invalid hashOrHeight type") } var block Block if err := c.request(ctx, "GET", path, nil, &block); err != nil { return nil, err } return &block, nil } // GetLatestBlock gets the latest block. func (c *Client) GetLatestBlock(ctx context.Context) (*Block, error) { var block Block if err := c.request(ctx, "GET", "/blocks/latest", nil, &block); err != nil { return nil, err } return &block, nil } // GetBlockHeader gets a block header. func (c *Client) GetBlockHeader(ctx context.Context, hashOrHeight interface{}) (*BlockHeader, error) { var path string switch v := hashOrHeight.(type) { case int, int64: path = fmt.Sprintf("/blocks/height/%d/header", v) case string: path = fmt.Sprintf("/blocks/%s/header", v) default: return nil, fmt.Errorf("invalid hashOrHeight type") } var header BlockHeader if err := c.request(ctx, "GET", path, nil, &header); err != nil { return nil, err } return &header, nil } // GetTransaction gets a transaction by hash. func (c *Client) GetTransaction(ctx context.Context, txid string) (*Transaction, error) { var tx Transaction if err := c.request(ctx, "GET", "/transactions/"+txid, nil, &tx); err != nil { return nil, err } return &tx, nil } // SendRawTransaction sends a raw transaction. func (c *Client) SendRawTransaction(ctx context.Context, rawTx string) (string, error) { var resp struct { TxID string `json:"txid"` } if err := c.request(ctx, "POST", "/transactions", map[string]string{"raw": rawTx}, &resp); err != nil { return "", err } return resp.TxID, nil } // EstimateFee estimates the transaction fee. func (c *Client) EstimateFee(ctx context.Context, priority Priority) (*FeeEstimate, error) { path := fmt.Sprintf("/fees/estimate?priority=%s", priority) var estimate FeeEstimate if err := c.request(ctx, "GET", path, nil, &estimate); err != nil { return nil, err } return &estimate, nil } // GetChainInfo gets chain information. func (c *Client) GetChainInfo(ctx context.Context) (*ChainInfo, error) { var info ChainInfo if err := c.request(ctx, "GET", "/chain", nil, &info); err != nil { return nil, err } return &info, nil } // GetMempoolInfo gets mempool information. func (c *Client) GetMempoolInfo(ctx context.Context) (*MempoolInfo, error) { var info MempoolInfo if err := c.request(ctx, "GET", "/mempool", nil, &info); err != nil { return nil, err } return &info, nil } // GetUTXOs gets UTXOs for an address. func (c *Client) GetUTXOs(ctx context.Context, address string) ([]UTXO, error) { var resp struct { UTXOs []UTXO `json:"utxos"` } if err := c.request(ctx, "GET", "/addresses/"+address+"/utxos", nil, &resp); err != nil { return nil, err } return resp.UTXOs, nil } // GetBalance gets the balance for an address. func (c *Client) GetBalance(ctx context.Context, address string) (*Balance, error) { var balance Balance if err := c.request(ctx, "GET", "/addresses/"+address+"/balance", nil, &balance); err != nil { return nil, err } return &balance, nil } // Subscription represents a WebSocket subscription. type Subscription struct { ID string Type string CreatedAt int64 Unsubscribe func() } // SubscribeBlocks subscribes to new blocks. func (c *Client) SubscribeBlocks(ctx context.Context, callback func(*Block)) (*Subscription, error) { return c.subscribe(ctx, "blocks", nil, func(data json.RawMessage) { var resp struct { Block Block `json:"block"` } if err := json.Unmarshal(data, &resp); err == nil { callback(&resp.Block) } }) } // SubscribeAddress subscribes to address transactions. func (c *Client) SubscribeAddress(ctx context.Context, address string, callback func(*Transaction)) (*Subscription, error) { return c.subscribe(ctx, "address", map[string]string{"address": address}, func(data json.RawMessage) { var resp struct { Transaction Transaction `json:"transaction"` } if err := json.Unmarshal(data, &resp); err == nil { callback(&resp.Transaction) } }) } func (c *Client) subscribe(ctx context.Context, subType string, params map[string]string, callback func(json.RawMessage)) (*Subscription, error) { if err := c.ensureWebSocket(ctx); err != nil { return nil, err } subID := fmt.Sprintf("%s_%d", subType, time.Now().UnixMilli()) c.subs[subID] = callback msg := map[string]interface{}{ "type": "subscribe", "id": subID, "subscription": subType, } for k, v := range params { msg[k] = v } if err := c.wsConn.WriteJSON(msg); err != nil { return nil, err } return &Subscription{ ID: subID, Type: subType, CreatedAt: time.Now().UnixMilli(), Unsubscribe: func() { delete(c.subs, subID) c.wsConn.WriteJSON(map[string]string{"type": "unsubscribe", "id": subID}) }, }, nil } func (c *Client) ensureWebSocket(ctx context.Context) error { if c.wsConn != nil { return nil } u, _ := url.Parse(c.config.WSEndpoint) q := u.Query() q.Set("apiKey", c.config.APIKey) q.Set("network", string(c.config.Network)) u.RawQuery = q.Encode() conn, _, err := websocket.DefaultDialer.DialContext(ctx, u.String(), nil) if err != nil { return fmt.Errorf("websocket connection failed: %w", err) } c.wsConn = conn go c.wsListener() return nil } func (c *Client) wsListener() { for { _, message, err := c.wsConn.ReadMessage() if err != nil { return } var msg struct { SubscriptionID string `json:"subscriptionId"` Data json.RawMessage `json:"data"` } if err := json.Unmarshal(message, &msg); err != nil { continue } if callback, ok := c.subs[msg.SubscriptionID]; ok { callback(msg.Data) } } } // Close closes the client. func (c *Client) Close() { if c.wsConn != nil { c.wsConn.Close() } } func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error { reqURL := c.config.Endpoint + path var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.config.APIKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Network", string(c.config.Network)) if c.config.Debug { fmt.Printf("[SynorRpc] %s %s\n", method, path) } resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { var errResp struct { Message string `json:"message"` Code string `json:"code"` } json.NewDecoder(resp.Body).Decode(&errResp) return &RpcError{ Message: errResp.Message, StatusCode: resp.StatusCode, Code: errResp.Code, } } if result != nil { if err := json.NewDecoder(resp.Body).Decode(result); err != nil { return fmt.Errorf("failed to decode response: %w", err) } } return nil } // RpcError represents an API error. type RpcError struct { Message string StatusCode int Code string } func (e *RpcError) Error() string { return fmt.Sprintf("rpc: %s (status %d)", e.Message, e.StatusCode) }