synor/sdk/go/rpc/rpc.go
Gulshan Yadav 59a7123535 feat(sdk): implement Phase 1 SDKs for Wallet, RPC, and Storage
Implements comprehensive SDK support for three core services across
four programming languages (JavaScript/TypeScript, Python, Go, Rust).

## New SDKs

### Wallet SDK
- Key management (create, import, export)
- Transaction signing
- Message signing and verification
- Balance and UTXO queries
- Stealth address support

### RPC SDK
- Block and transaction queries
- Chain state information
- Fee estimation
- Mempool information
- WebSocket subscriptions for real-time updates

### Storage SDK
- Content upload and download
- Pinning operations
- CAR file support
- Directory management
- Gateway URL generation

## Shared Infrastructure

- JSON Schema definitions for all 11 services
- Common type definitions (Address, Amount, UTXO, etc.)
- Unified error handling patterns
- Builder patterns for configuration

## Package Updates

- JavaScript: Updated to @synor/sdk with module exports
- Python: Updated to synor-sdk with websockets dependency
- Go: Added gorilla/websocket dependency
- Rust: Added base64, urlencoding, multipart support

## Fixes

- Fixed Tensor Default trait implementation
- Fixed ProcessorType enum casing
2026-01-27 00:46:24 +05:30

480 lines
12 KiB
Go

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