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
480 lines
12 KiB
Go
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)
|
|
}
|