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
This commit is contained in:
parent
7785dbe8f8
commit
59a7123535
52 changed files with 10946 additions and 13 deletions
|
|
@ -4,4 +4,7 @@ go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/goccy/go-json v0.10.2
|
github.com/goccy/go-json v0.10.2
|
||||||
|
github.com/gorilla/websocket v1.5.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/net v0.17.0 // indirect
|
||||||
|
|
|
||||||
480
sdk/go/rpc/rpc.go
Normal file
480
sdk/go/rpc/rpc.go
Normal file
|
|
@ -0,0 +1,480 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
673
sdk/go/storage/storage.go
Normal file
673
sdk/go/storage/storage.go
Normal file
|
|
@ -0,0 +1,673 @@
|
||||||
|
// Package storage provides a Go SDK for Synor Storage operations.
|
||||||
|
//
|
||||||
|
// Decentralized storage, pinning, and content retrieval on the Synor network.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// client := storage.NewClient("your-api-key")
|
||||||
|
// result, err := client.Upload(ctx, []byte("Hello, World!"), nil)
|
||||||
|
// fmt.Println("CID:", result.CID)
|
||||||
|
//
|
||||||
|
// // Get gateway URL
|
||||||
|
// gateway := client.GetGatewayURL(result.CID, "")
|
||||||
|
// fmt.Println("URL:", gateway.URL)
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version of the SDK.
|
||||||
|
const Version = "0.1.0"
|
||||||
|
|
||||||
|
// Default endpoints.
|
||||||
|
const (
|
||||||
|
DefaultEndpoint = "https://storage.synor.cc/api/v1"
|
||||||
|
DefaultGateway = "https://gateway.synor.cc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PinStatus represents the status of a pin.
|
||||||
|
type PinStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Queued PinStatus = "queued"
|
||||||
|
Pinning PinStatus = "pinning"
|
||||||
|
Pinned PinStatus = "pinned"
|
||||||
|
Failed PinStatus = "failed"
|
||||||
|
Unpinned PinStatus = "unpinned"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HashAlgorithm represents a hash algorithm.
|
||||||
|
type HashAlgorithm string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SHA256 HashAlgorithm = "sha2-256"
|
||||||
|
BLAKE3 HashAlgorithm = "blake3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EntryType represents a directory entry type.
|
||||||
|
type EntryType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FileType EntryType = "file"
|
||||||
|
DirectoryType EntryType = "directory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MatchType represents a pin matching type.
|
||||||
|
type MatchType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Exact MatchType = "exact"
|
||||||
|
IExact MatchType = "iexact"
|
||||||
|
Partial MatchType = "partial"
|
||||||
|
IPartial MatchType = "ipartial"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds client configuration.
|
||||||
|
type Config struct {
|
||||||
|
APIKey string
|
||||||
|
Endpoint string
|
||||||
|
Gateway string
|
||||||
|
PinningService string
|
||||||
|
ChunkSize int
|
||||||
|
Timeout time.Duration
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns a default configuration.
|
||||||
|
func DefaultConfig(apiKey string) Config {
|
||||||
|
return Config{
|
||||||
|
APIKey: apiKey,
|
||||||
|
Endpoint: DefaultEndpoint,
|
||||||
|
Gateway: DefaultGateway,
|
||||||
|
ChunkSize: 262144, // 256KB
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is the Synor Storage client.
|
||||||
|
type Client struct {
|
||||||
|
config Config
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadOptions contains options for uploading content.
|
||||||
|
type UploadOptions struct {
|
||||||
|
Pin bool
|
||||||
|
WrapWithDirectory bool
|
||||||
|
CIDVersion int
|
||||||
|
HashAlgorithm HashAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadResponse contains the result of an upload.
|
||||||
|
type UploadResponse struct {
|
||||||
|
CID string `json:"cid"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Hash string `json:"hash,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadOptions contains options for downloading content.
|
||||||
|
type DownloadOptions struct {
|
||||||
|
Offset int64
|
||||||
|
Length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin represents pin information.
|
||||||
|
type Pin struct {
|
||||||
|
CID string `json:"cid"`
|
||||||
|
Status PinStatus `json:"status"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
CreatedAt int64 `json:"createdAt,omitempty"`
|
||||||
|
ExpiresAt int64 `json:"expiresAt,omitempty"`
|
||||||
|
Delegates []string `json:"delegates,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PinRequest contains options for pinning content.
|
||||||
|
type PinRequest struct {
|
||||||
|
CID string `json:"cid"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Duration int64 `json:"duration,omitempty"`
|
||||||
|
Origins []string `json:"origins,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPinsOptions contains options for listing pins.
|
||||||
|
type ListPinsOptions struct {
|
||||||
|
Status []PinStatus
|
||||||
|
Match MatchType
|
||||||
|
Name string
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPinsResponse contains the result of listing pins.
|
||||||
|
type ListPinsResponse struct {
|
||||||
|
Pins []Pin `json:"pins"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
HasMore bool `json:"hasMore"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GatewayURL represents a gateway URL.
|
||||||
|
type GatewayURL struct {
|
||||||
|
URL string
|
||||||
|
CID string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CarBlock represents a CAR block.
|
||||||
|
type CarBlock struct {
|
||||||
|
CID string `json:"cid"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CarFile represents a CAR file.
|
||||||
|
type CarFile struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
Roots []string `json:"roots"`
|
||||||
|
Blocks []CarBlock `json:"blocks,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileEntry represents a file entry for directory creation.
|
||||||
|
type FileEntry struct {
|
||||||
|
Name string
|
||||||
|
Content []byte
|
||||||
|
CID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectoryEntry represents a directory entry.
|
||||||
|
type DirectoryEntry struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
CID string `json:"cid"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
Type EntryType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportCarResponse contains the result of importing a CAR file.
|
||||||
|
type ImportCarResponse struct {
|
||||||
|
Roots []string `json:"roots"`
|
||||||
|
BlocksImported int `json:"blocksImported"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageStats represents storage statistics.
|
||||||
|
type StorageStats struct {
|
||||||
|
TotalSize int64 `json:"totalSize"`
|
||||||
|
PinCount int `json:"pinCount"`
|
||||||
|
Bandwidth struct {
|
||||||
|
Upload int64 `json:"upload"`
|
||||||
|
Download int64 `json:"download"`
|
||||||
|
} `json:"bandwidth,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload uploads content to storage.
|
||||||
|
func (c *Client) Upload(ctx context.Context, data []byte, opts *UploadOptions) (*UploadResponse, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&buf)
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile("file", "file")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create form file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := part.Write(data); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to close writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
if opts != nil {
|
||||||
|
if opts.Pin {
|
||||||
|
params.Set("pin", "true")
|
||||||
|
}
|
||||||
|
if opts.WrapWithDirectory {
|
||||||
|
params.Set("wrapWithDirectory", "true")
|
||||||
|
}
|
||||||
|
if opts.CIDVersion != 0 {
|
||||||
|
params.Set("cidVersion", fmt.Sprintf("%d", opts.CIDVersion))
|
||||||
|
}
|
||||||
|
if opts.HashAlgorithm != "" {
|
||||||
|
params.Set("hashAlgorithm", string(opts.HashAlgorithm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "/upload"
|
||||||
|
if len(params) > 0 {
|
||||||
|
path += "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL := c.config.Endpoint + path
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
if c.config.Debug {
|
||||||
|
fmt.Printf("[SynorStorage] POST %s\n", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, c.parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result UploadResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download downloads content by CID.
|
||||||
|
func (c *Client) Download(ctx context.Context, cid string, opts *DownloadOptions) ([]byte, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
if opts != nil {
|
||||||
|
if opts.Offset > 0 {
|
||||||
|
params.Set("offset", fmt.Sprintf("%d", opts.Offset))
|
||||||
|
}
|
||||||
|
if opts.Length > 0 {
|
||||||
|
params.Set("length", fmt.Sprintf("%d", opts.Length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := fmt.Sprintf("/content/%s", cid)
|
||||||
|
if len(params) > 0 {
|
||||||
|
path += "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL := c.config.Endpoint + path
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, c.parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadReader returns a reader for downloading content.
|
||||||
|
func (c *Client) DownloadReader(ctx context.Context, cid string, opts *DownloadOptions) (io.ReadCloser, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
if opts != nil {
|
||||||
|
if opts.Offset > 0 {
|
||||||
|
params.Set("offset", fmt.Sprintf("%d", opts.Offset))
|
||||||
|
}
|
||||||
|
if opts.Length > 0 {
|
||||||
|
params.Set("length", fmt.Sprintf("%d", opts.Length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := fmt.Sprintf("/content/%s/stream", cid)
|
||||||
|
if len(params) > 0 {
|
||||||
|
path += "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL := c.config.Endpoint + path
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return nil, c.parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin pins content by CID.
|
||||||
|
func (c *Client) Pin(ctx context.Context, req PinRequest) (*Pin, error) {
|
||||||
|
var result Pin
|
||||||
|
if err := c.request(ctx, "POST", "/pins", req, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpin unpins content by CID.
|
||||||
|
func (c *Client) Unpin(ctx context.Context, cid string) error {
|
||||||
|
return c.request(ctx, "DELETE", "/pins/"+cid, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPinStatus gets the pin status for a CID.
|
||||||
|
func (c *Client) GetPinStatus(ctx context.Context, cid string) (*Pin, error) {
|
||||||
|
var result Pin
|
||||||
|
if err := c.request(ctx, "GET", "/pins/"+cid, nil, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPins lists pins.
|
||||||
|
func (c *Client) ListPins(ctx context.Context, opts *ListPinsOptions) (*ListPinsResponse, error) {
|
||||||
|
path := "/pins"
|
||||||
|
if opts != nil {
|
||||||
|
params := url.Values{}
|
||||||
|
if len(opts.Status) > 0 {
|
||||||
|
var statuses []string
|
||||||
|
for _, s := range opts.Status {
|
||||||
|
statuses = append(statuses, string(s))
|
||||||
|
}
|
||||||
|
params.Set("status", joinStrings(statuses, ","))
|
||||||
|
}
|
||||||
|
if opts.Match != "" {
|
||||||
|
params.Set("match", string(opts.Match))
|
||||||
|
}
|
||||||
|
if opts.Name != "" {
|
||||||
|
params.Set("name", opts.Name)
|
||||||
|
}
|
||||||
|
if opts.Limit > 0 {
|
||||||
|
params.Set("limit", fmt.Sprintf("%d", opts.Limit))
|
||||||
|
}
|
||||||
|
if opts.Offset > 0 {
|
||||||
|
params.Set("offset", fmt.Sprintf("%d", opts.Offset))
|
||||||
|
}
|
||||||
|
if len(params) > 0 {
|
||||||
|
path += "?" + params.Encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result ListPinsResponse
|
||||||
|
if err := c.request(ctx, "GET", path, nil, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGatewayURL returns the gateway URL for content.
|
||||||
|
func (c *Client) GetGatewayURL(cid string, path string) GatewayURL {
|
||||||
|
fullPath := "/" + cid
|
||||||
|
if path != "" {
|
||||||
|
fullPath += "/" + path
|
||||||
|
}
|
||||||
|
return GatewayURL{
|
||||||
|
URL: c.config.Gateway + "/ipfs" + fullPath,
|
||||||
|
CID: cid,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCar creates a CAR file from files.
|
||||||
|
func (c *Client) CreateCar(ctx context.Context, files []FileEntry) (*CarFile, error) {
|
||||||
|
type fileData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
CID string `json:"cid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []fileData
|
||||||
|
for _, f := range files {
|
||||||
|
entry := fileData{Name: f.Name}
|
||||||
|
if len(f.Content) > 0 {
|
||||||
|
entry.Content = base64.StdEncoding.EncodeToString(f.Content)
|
||||||
|
}
|
||||||
|
if f.CID != "" {
|
||||||
|
entry.CID = f.CID
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result CarFile
|
||||||
|
if err := c.request(ctx, "POST", "/car/create", map[string]interface{}{"files": entries}, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportCar imports a CAR file.
|
||||||
|
func (c *Client) ImportCar(ctx context.Context, carData []byte, pin bool) (*ImportCarResponse, error) {
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(carData)
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"car": encoded,
|
||||||
|
"pin": pin,
|
||||||
|
}
|
||||||
|
|
||||||
|
var result ImportCarResponse
|
||||||
|
if err := c.request(ctx, "POST", "/car/import", body, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportCar exports content as a CAR file.
|
||||||
|
func (c *Client) ExportCar(ctx context.Context, cid string) ([]byte, error) {
|
||||||
|
reqURL := c.config.Endpoint + "/car/export/" + cid
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, c.parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDirectory creates a directory from files.
|
||||||
|
func (c *Client) CreateDirectory(ctx context.Context, files []FileEntry) (*UploadResponse, error) {
|
||||||
|
type fileData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
CID string `json:"cid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []fileData
|
||||||
|
for _, f := range files {
|
||||||
|
entry := fileData{Name: f.Name}
|
||||||
|
if len(f.Content) > 0 {
|
||||||
|
entry.Content = base64.StdEncoding.EncodeToString(f.Content)
|
||||||
|
}
|
||||||
|
if f.CID != "" {
|
||||||
|
entry.CID = f.CID
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result UploadResponse
|
||||||
|
if err := c.request(ctx, "POST", "/directory", map[string]interface{}{"files": entries}, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDirectory lists directory contents.
|
||||||
|
func (c *Client) ListDirectory(ctx context.Context, cid string, path string) ([]DirectoryEntry, error) {
|
||||||
|
apiPath := fmt.Sprintf("/directory/%s", cid)
|
||||||
|
if path != "" {
|
||||||
|
apiPath += "?path=" + url.QueryEscape(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Entries []DirectoryEntry `json:"entries"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "GET", apiPath, nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats gets storage statistics.
|
||||||
|
func (c *Client) GetStats(ctx context.Context) (*StorageStats, error) {
|
||||||
|
var result StorageStats
|
||||||
|
if err := c.request(ctx, "GET", "/stats", nil, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists checks if content exists.
|
||||||
|
func (c *Client) Exists(ctx context.Context, cid string) (bool, error) {
|
||||||
|
reqURL := c.config.Endpoint + "/content/" + cid
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "HEAD", reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp.StatusCode == 200, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadata gets content metadata.
|
||||||
|
func (c *Client) GetMetadata(ctx context.Context, cid string) (map[string]interface{}, error) {
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := c.request(ctx, "GET", "/content/"+cid+"/metadata", nil, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
if c.config.Debug {
|
||||||
|
fmt.Printf("[SynorStorage] %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 {
|
||||||
|
return c.parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != nil && resp.StatusCode != 204 {
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) parseError(resp *http.Response) error {
|
||||||
|
var errResp struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&errResp)
|
||||||
|
return &StorageError{
|
||||||
|
Message: errResp.Message,
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Code: errResp.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinStrings(strs []string, sep string) string {
|
||||||
|
if len(strs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
result := strs[0]
|
||||||
|
for i := 1; i < len(strs); i++ {
|
||||||
|
result += sep + strs[i]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageError represents an API error.
|
||||||
|
type StorageError struct {
|
||||||
|
Message string
|
||||||
|
StatusCode int
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *StorageError) Error() string {
|
||||||
|
return fmt.Sprintf("storage: %s (status %d)", e.Message, e.StatusCode)
|
||||||
|
}
|
||||||
507
sdk/go/wallet/wallet.go
Normal file
507
sdk/go/wallet/wallet.go
Normal file
|
|
@ -0,0 +1,507 @@
|
||||||
|
// Package wallet provides a Go SDK for Synor Wallet operations.
|
||||||
|
//
|
||||||
|
// Manage wallets, sign transactions, and query balances on the Synor blockchain.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// client := wallet.NewClient("your-api-key")
|
||||||
|
// result, err := client.CreateWallet(ctx, wallet.Standard)
|
||||||
|
// fmt.Println("Address:", result.Wallet.Address)
|
||||||
|
// fmt.Println("Mnemonic:", result.Mnemonic) // Store securely!
|
||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version of the SDK.
|
||||||
|
const Version = "0.1.0"
|
||||||
|
|
||||||
|
// DefaultEndpoint is the default API endpoint.
|
||||||
|
const DefaultEndpoint = "https://wallet.synor.cc/api/v1"
|
||||||
|
|
||||||
|
// Network represents the blockchain network.
|
||||||
|
type Network string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Mainnet Network = "mainnet"
|
||||||
|
Testnet Network = "testnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WalletType represents the type of wallet.
|
||||||
|
type WalletType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Standard WalletType = "standard"
|
||||||
|
Multisig WalletType = "multisig"
|
||||||
|
Stealth WalletType = "stealth"
|
||||||
|
Hardware WalletType = "hardware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Priority represents transaction priority levels.
|
||||||
|
type Priority string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Low Priority = "low"
|
||||||
|
Medium Priority = "medium"
|
||||||
|
High Priority = "high"
|
||||||
|
Urgent Priority = "urgent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds client configuration.
|
||||||
|
type Config struct {
|
||||||
|
APIKey string
|
||||||
|
Endpoint string
|
||||||
|
Network Network
|
||||||
|
Timeout time.Duration
|
||||||
|
Debug bool
|
||||||
|
DerivationPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns a default configuration.
|
||||||
|
func DefaultConfig(apiKey string) Config {
|
||||||
|
return Config{
|
||||||
|
APIKey: apiKey,
|
||||||
|
Endpoint: DefaultEndpoint,
|
||||||
|
Network: Mainnet,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is the Synor Wallet client.
|
||||||
|
type Client struct {
|
||||||
|
config Config
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallet represents a wallet instance.
|
||||||
|
type Wallet struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
Type WalletType `json:"type"`
|
||||||
|
CreatedAt int64 `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateWalletResult contains the created wallet and mnemonic.
|
||||||
|
type CreateWalletResult struct {
|
||||||
|
Wallet Wallet `json:"wallet"`
|
||||||
|
Mnemonic string `json:"mnemonic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StealthAddress represents a stealth address for private payments.
|
||||||
|
type StealthAddress struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
ViewKey string `json:"viewKey"`
|
||||||
|
SpendKey string `json:"spendKey"`
|
||||||
|
EphemeralKey string `json:"ephemeralKey,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionInput represents a transaction input.
|
||||||
|
type TransactionInput struct {
|
||||||
|
TxID string `json:"txid"`
|
||||||
|
Vout int `json:"vout"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
ScriptSig string `json:"scriptSig,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionOutput represents a transaction output.
|
||||||
|
type TransactionOutput struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
ScriptPubKey string `json:"scriptPubKey,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction represents an unsigned transaction.
|
||||||
|
type Transaction struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
Inputs []TransactionInput `json:"inputs"`
|
||||||
|
Outputs []TransactionOutput `json:"outputs"`
|
||||||
|
LockTime int `json:"lockTime,omitempty"`
|
||||||
|
Fee string `json:"fee,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignedTransaction represents a signed transaction.
|
||||||
|
type SignedTransaction struct {
|
||||||
|
Raw string `json:"raw"`
|
||||||
|
TxID string `json:"txid"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Weight int `json:"weight,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignedMessage represents a signed message.
|
||||||
|
type SignedMessage struct {
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenBalance represents a token balance.
|
||||||
|
type TokenBalance struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Decimals int `json:"decimals"`
|
||||||
|
Balance string `json:"balance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceResponse contains native and token balances.
|
||||||
|
type BalanceResponse struct {
|
||||||
|
Native Balance `json:"native"`
|
||||||
|
Tokens []TokenBalance `json:"tokens,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeeEstimate represents a fee estimation result.
|
||||||
|
type FeeEstimate struct {
|
||||||
|
Priority Priority `json:"priority"`
|
||||||
|
FeeRate string `json:"feeRate"`
|
||||||
|
EstimatedBlocks int `json:"estimatedBlocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportWalletOptions contains options for importing a wallet.
|
||||||
|
type ImportWalletOptions struct {
|
||||||
|
Mnemonic string
|
||||||
|
Passphrase string
|
||||||
|
Type WalletType
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildTransactionOptions contains options for building a transaction.
|
||||||
|
type BuildTransactionOptions struct {
|
||||||
|
To string
|
||||||
|
Amount string
|
||||||
|
FeeRate float64
|
||||||
|
UTXOs []UTXO
|
||||||
|
ChangeAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUtxosOptions contains options for querying UTXOs.
|
||||||
|
type GetUtxosOptions struct {
|
||||||
|
MinConfirmations int
|
||||||
|
MinAmount string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateWallet creates a new wallet.
|
||||||
|
func (c *Client) CreateWallet(ctx context.Context, walletType WalletType) (*CreateWalletResult, error) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"type": walletType,
|
||||||
|
"network": c.config.Network,
|
||||||
|
}
|
||||||
|
if c.config.DerivationPath != "" {
|
||||||
|
body["derivationPath"] = c.config.DerivationPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var result CreateWalletResult
|
||||||
|
if err := c.request(ctx, "POST", "/wallets", body, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportWallet imports a wallet from mnemonic phrase.
|
||||||
|
func (c *Client) ImportWallet(ctx context.Context, opts ImportWalletOptions) (*Wallet, error) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"mnemonic": opts.Mnemonic,
|
||||||
|
"passphrase": opts.Passphrase,
|
||||||
|
"type": opts.Type,
|
||||||
|
"network": c.config.Network,
|
||||||
|
}
|
||||||
|
if c.config.DerivationPath != "" {
|
||||||
|
body["derivationPath"] = c.config.DerivationPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Wallet Wallet `json:"wallet"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "POST", "/wallets/import", body, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp.Wallet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWallet gets a wallet by ID.
|
||||||
|
func (c *Client) GetWallet(ctx context.Context, walletID string) (*Wallet, error) {
|
||||||
|
var resp struct {
|
||||||
|
Wallet Wallet `json:"wallet"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "GET", "/wallets/"+walletID, nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp.Wallet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListWallets lists all wallets for this account.
|
||||||
|
func (c *Client) ListWallets(ctx context.Context) ([]Wallet, error) {
|
||||||
|
var resp struct {
|
||||||
|
Wallets []Wallet `json:"wallets"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "GET", "/wallets", nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Wallets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAddress gets an address at a specific index for a wallet.
|
||||||
|
func (c *Client) GetAddress(ctx context.Context, walletID string, index int) (string, error) {
|
||||||
|
path := fmt.Sprintf("/wallets/%s/addresses/%d", walletID, index)
|
||||||
|
var resp struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "GET", path, nil, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resp.Address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStealthAddress generates a stealth address for receiving private payments.
|
||||||
|
func (c *Client) GetStealthAddress(ctx context.Context, walletID string) (*StealthAddress, error) {
|
||||||
|
path := fmt.Sprintf("/wallets/%s/stealth", walletID)
|
||||||
|
var resp struct {
|
||||||
|
StealthAddress StealthAddress `json:"stealthAddress"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "POST", path, nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp.StealthAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignTransaction signs a transaction.
|
||||||
|
func (c *Client) SignTransaction(ctx context.Context, walletID string, tx *Transaction) (*SignedTransaction, error) {
|
||||||
|
path := fmt.Sprintf("/wallets/%s/sign", walletID)
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"transaction": tx,
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
SignedTransaction SignedTransaction `json:"signedTransaction"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "POST", path, body, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp.SignedTransaction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignMessage signs a message.
|
||||||
|
func (c *Client) SignMessage(ctx context.Context, walletID, message, format string) (*SignedMessage, error) {
|
||||||
|
if format == "" {
|
||||||
|
format = "text"
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("/wallets/%s/sign-message", walletID)
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"message": message,
|
||||||
|
"format": format,
|
||||||
|
}
|
||||||
|
var result SignedMessage
|
||||||
|
if err := c.request(ctx, "POST", path, body, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyMessage verifies a signed message.
|
||||||
|
func (c *Client) VerifyMessage(ctx context.Context, message, signature, address string) (bool, error) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"message": message,
|
||||||
|
"signature": signature,
|
||||||
|
"address": address,
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "POST", "/verify-message", body, &resp); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return resp.Valid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBalance gets the balance for an address.
|
||||||
|
func (c *Client) GetBalance(ctx context.Context, address string, includeTokens bool) (*BalanceResponse, error) {
|
||||||
|
path := fmt.Sprintf("/balances/%s?includeTokens=%t", address, includeTokens)
|
||||||
|
var result BalanceResponse
|
||||||
|
if err := c.request(ctx, "GET", path, nil, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUTXOs gets UTXOs for an address.
|
||||||
|
func (c *Client) GetUTXOs(ctx context.Context, address string, opts *GetUtxosOptions) ([]UTXO, error) {
|
||||||
|
path := "/utxos/" + address
|
||||||
|
if opts != nil {
|
||||||
|
params := url.Values{}
|
||||||
|
if opts.MinConfirmations > 0 {
|
||||||
|
params.Set("minConfirmations", fmt.Sprintf("%d", opts.MinConfirmations))
|
||||||
|
}
|
||||||
|
if opts.MinAmount != "" {
|
||||||
|
params.Set("minAmount", opts.MinAmount)
|
||||||
|
}
|
||||||
|
if len(params) > 0 {
|
||||||
|
path += "?" + params.Encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
UTXOs []UTXO `json:"utxos"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "GET", path, nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.UTXOs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildTransaction builds a transaction without signing.
|
||||||
|
func (c *Client) BuildTransaction(ctx context.Context, walletID string, opts BuildTransactionOptions) (*Transaction, error) {
|
||||||
|
path := fmt.Sprintf("/wallets/%s/build-tx", walletID)
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"to": opts.To,
|
||||||
|
"amount": opts.Amount,
|
||||||
|
}
|
||||||
|
if opts.FeeRate > 0 {
|
||||||
|
body["feeRate"] = opts.FeeRate
|
||||||
|
}
|
||||||
|
if len(opts.UTXOs) > 0 {
|
||||||
|
body["utxos"] = opts.UTXOs
|
||||||
|
}
|
||||||
|
if opts.ChangeAddress != "" {
|
||||||
|
body["changeAddress"] = opts.ChangeAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Transaction Transaction `json:"transaction"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "POST", path, body, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp.Transaction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendTransaction builds and signs a transaction in one step.
|
||||||
|
func (c *Client) SendTransaction(ctx context.Context, walletID string, opts BuildTransactionOptions) (*SignedTransaction, error) {
|
||||||
|
tx, err := c.BuildTransaction(ctx, walletID, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.SignTransaction(ctx, walletID, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 result FeeEstimate
|
||||||
|
if err := c.request(ctx, "GET", path, nil, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllFeeEstimates gets fee estimates for all priority levels.
|
||||||
|
func (c *Client) GetAllFeeEstimates(ctx context.Context) ([]FeeEstimate, error) {
|
||||||
|
var resp struct {
|
||||||
|
Estimates []FeeEstimate `json:"estimates"`
|
||||||
|
}
|
||||||
|
if err := c.request(ctx, "GET", "/fees/estimate/all", nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Estimates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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("[SynorWallet] %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 &WalletError{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalletError represents an API error.
|
||||||
|
type WalletError struct {
|
||||||
|
Message string
|
||||||
|
StatusCode int
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *WalletError) Error() string {
|
||||||
|
return fmt.Sprintf("wallet: %s (status %d)", e.Message, e.StatusCode)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@synor/compute-sdk",
|
"name": "@synor/sdk",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Synor Compute SDK for browser and Node.js - Access distributed GPU/TPU/NPU compute",
|
"description": "Synor SDK for browser and Node.js - Complete blockchain SDK including Compute, Wallet, RPC, and Storage",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
@ -10,28 +10,52 @@
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.mjs",
|
"import": "./dist/index.mjs",
|
||||||
"require": "./dist/index.js"
|
"require": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./compute": {
|
||||||
|
"types": "./dist/compute/index.d.ts",
|
||||||
|
"import": "./dist/compute/index.mjs",
|
||||||
|
"require": "./dist/compute/index.js"
|
||||||
|
},
|
||||||
|
"./wallet": {
|
||||||
|
"types": "./dist/wallet/index.d.ts",
|
||||||
|
"import": "./dist/wallet/index.mjs",
|
||||||
|
"require": "./dist/wallet/index.js"
|
||||||
|
},
|
||||||
|
"./rpc": {
|
||||||
|
"types": "./dist/rpc/index.d.ts",
|
||||||
|
"import": "./dist/rpc/index.mjs",
|
||||||
|
"require": "./dist/rpc/index.js"
|
||||||
|
},
|
||||||
|
"./storage": {
|
||||||
|
"types": "./dist/storage/index.d.ts",
|
||||||
|
"import": "./dist/storage/index.mjs",
|
||||||
|
"require": "./dist/storage/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
"build": "tsup src/index.ts src/compute/index.ts src/wallet/index.ts src/rpc/index.ts src/storage/index.ts --format cjs,esm --dts",
|
||||||
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
"dev": "tsup src/index.ts src/compute/index.ts src/wallet/index.ts src/rpc/index.ts src/storage/index.ts --format cjs,esm --dts --watch",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"lint": "biome check src/",
|
"lint": "biome check src/",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"synor",
|
"synor",
|
||||||
|
"blockchain",
|
||||||
"compute",
|
"compute",
|
||||||
|
"wallet",
|
||||||
|
"rpc",
|
||||||
|
"storage",
|
||||||
|
"ipfs",
|
||||||
"gpu",
|
"gpu",
|
||||||
"tpu",
|
"tpu",
|
||||||
"npu",
|
"npu",
|
||||||
"ai",
|
"ai",
|
||||||
"ml",
|
"ml",
|
||||||
"distributed-computing",
|
"distributed-computing"
|
||||||
"heterogeneous-compute"
|
|
||||||
],
|
],
|
||||||
"author": "Synor Team",
|
"author": "Synor Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
403
sdk/js/src/rpc/client.ts
Normal file
403
sdk/js/src/rpc/client.ts
Normal file
|
|
@ -0,0 +1,403 @@
|
||||||
|
/**
|
||||||
|
* Synor RPC Client
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
RpcConfig,
|
||||||
|
Block,
|
||||||
|
BlockHeader,
|
||||||
|
Transaction,
|
||||||
|
FeeEstimate,
|
||||||
|
ChainInfo,
|
||||||
|
MempoolInfo,
|
||||||
|
UTXO,
|
||||||
|
Subscription,
|
||||||
|
SubscriptionType,
|
||||||
|
Priority,
|
||||||
|
Notification,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT = 'https://rpc.synor.cc/api/v1';
|
||||||
|
const DEFAULT_WS_ENDPOINT = 'wss://rpc.synor.cc/ws';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synor RPC SDK error.
|
||||||
|
*/
|
||||||
|
export class RpcError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public statusCode?: number,
|
||||||
|
public code?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'RpcError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Synor RPC client.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const rpc = new SynorRpc({ apiKey: 'sk_...' });
|
||||||
|
*
|
||||||
|
* // Get latest block
|
||||||
|
* const block = await rpc.getLatestBlock();
|
||||||
|
* console.log('Height:', block.height);
|
||||||
|
*
|
||||||
|
* // Subscribe to new blocks
|
||||||
|
* const sub = await rpc.subscribeBlocks((block) => {
|
||||||
|
* console.log('New block:', block.height);
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class SynorRpc {
|
||||||
|
private config: Required<RpcConfig>;
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private subscriptions: Map<string, (data: Notification) => void> = new Map();
|
||||||
|
private reconnectAttempts = 0;
|
||||||
|
private maxReconnectAttempts = 5;
|
||||||
|
|
||||||
|
constructor(config: RpcConfig) {
|
||||||
|
this.config = {
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
||||||
|
wsEndpoint: config.wsEndpoint ?? DEFAULT_WS_ENDPOINT,
|
||||||
|
network: config.network ?? 'mainnet',
|
||||||
|
timeout: config.timeout ?? 30000,
|
||||||
|
debug: config.debug ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Block Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get block by hash or height.
|
||||||
|
*/
|
||||||
|
async getBlock(hashOrHeight: string | number): Promise<Block> {
|
||||||
|
const path = typeof hashOrHeight === 'number'
|
||||||
|
? `/blocks/height/${hashOrHeight}`
|
||||||
|
: `/blocks/${hashOrHeight}`;
|
||||||
|
return this.request('GET', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latest block.
|
||||||
|
*/
|
||||||
|
async getLatestBlock(): Promise<Block> {
|
||||||
|
return this.request('GET', '/blocks/latest');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get block header only.
|
||||||
|
*/
|
||||||
|
async getBlockHeader(hashOrHeight: string | number): Promise<BlockHeader> {
|
||||||
|
const path = typeof hashOrHeight === 'number'
|
||||||
|
? `/blocks/height/${hashOrHeight}/header`
|
||||||
|
: `/blocks/${hashOrHeight}/header`;
|
||||||
|
return this.request('GET', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blocks in a range.
|
||||||
|
*/
|
||||||
|
async getBlocks(startHeight: number, endHeight: number): Promise<Block[]> {
|
||||||
|
const response = await this.request('GET', '/blocks', undefined, {
|
||||||
|
start: startHeight.toString(),
|
||||||
|
end: endHeight.toString(),
|
||||||
|
});
|
||||||
|
return response.blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Transaction Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transaction by hash.
|
||||||
|
*/
|
||||||
|
async getTransaction(txid: string): Promise<Transaction> {
|
||||||
|
return this.request('GET', `/transactions/${txid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get raw transaction hex.
|
||||||
|
*/
|
||||||
|
async getRawTransaction(txid: string): Promise<string> {
|
||||||
|
const response = await this.request('GET', `/transactions/${txid}/raw`);
|
||||||
|
return response.raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a raw transaction.
|
||||||
|
*/
|
||||||
|
async sendRawTransaction(rawTx: string): Promise<string> {
|
||||||
|
const response = await this.request('POST', '/transactions', { raw: rawTx });
|
||||||
|
return response.txid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transactions for an address.
|
||||||
|
*/
|
||||||
|
async getAddressTransactions(
|
||||||
|
address: string,
|
||||||
|
options?: { limit?: number; offset?: number }
|
||||||
|
): Promise<Transaction[]> {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (options?.limit) params.limit = options.limit.toString();
|
||||||
|
if (options?.offset) params.offset = options.offset.toString();
|
||||||
|
|
||||||
|
const response = await this.request(
|
||||||
|
'GET',
|
||||||
|
`/addresses/${address}/transactions`,
|
||||||
|
undefined,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return response.transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Fee Estimation ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate fee for a priority level.
|
||||||
|
*/
|
||||||
|
async estimateFee(priority: Priority = 'medium'): Promise<FeeEstimate> {
|
||||||
|
return this.request('GET', '/fees/estimate', undefined, { priority });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all fee estimates.
|
||||||
|
*/
|
||||||
|
async getAllFeeEstimates(): Promise<FeeEstimate[]> {
|
||||||
|
const response = await this.request('GET', '/fees/estimate/all');
|
||||||
|
return response.estimates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Chain Information ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chain information.
|
||||||
|
*/
|
||||||
|
async getChainInfo(): Promise<ChainInfo> {
|
||||||
|
return this.request('GET', '/chain');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mempool information.
|
||||||
|
*/
|
||||||
|
async getMempoolInfo(): Promise<MempoolInfo> {
|
||||||
|
return this.request('GET', '/mempool');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mempool transactions.
|
||||||
|
*/
|
||||||
|
async getMempoolTransactions(limit: number = 100): Promise<string[]> {
|
||||||
|
const response = await this.request('GET', '/mempool/transactions', undefined, {
|
||||||
|
limit: limit.toString(),
|
||||||
|
});
|
||||||
|
return response.txids;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UTXO Operations ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get UTXOs for an address.
|
||||||
|
*/
|
||||||
|
async getUtxos(address: string): Promise<UTXO[]> {
|
||||||
|
const response = await this.request('GET', `/addresses/${address}/utxos`);
|
||||||
|
return response.utxos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get balance for an address.
|
||||||
|
*/
|
||||||
|
async getBalance(address: string): Promise<{ confirmed: string; unconfirmed: string; total: string }> {
|
||||||
|
return this.request('GET', `/addresses/${address}/balance`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Subscriptions ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to new blocks.
|
||||||
|
*/
|
||||||
|
async subscribeBlocks(callback: (block: Block) => void): Promise<Subscription> {
|
||||||
|
return this.subscribe('blocks', (notification) => {
|
||||||
|
if (notification.type === 'block') {
|
||||||
|
callback(notification.block);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to address transactions.
|
||||||
|
*/
|
||||||
|
async subscribeAddress(
|
||||||
|
address: string,
|
||||||
|
callback: (tx: Transaction) => void
|
||||||
|
): Promise<Subscription> {
|
||||||
|
return this.subscribe('address', (notification) => {
|
||||||
|
if (notification.type === 'transaction') {
|
||||||
|
callback(notification.transaction);
|
||||||
|
}
|
||||||
|
}, { address });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to mempool transactions.
|
||||||
|
*/
|
||||||
|
async subscribeMempool(callback: (tx: Transaction) => void): Promise<Subscription> {
|
||||||
|
return this.subscribe('mempool', (notification) => {
|
||||||
|
if (notification.type === 'transaction') {
|
||||||
|
callback(notification.transaction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async subscribe(
|
||||||
|
type: SubscriptionType,
|
||||||
|
callback: (notification: Notification) => void,
|
||||||
|
params?: Record<string, string>
|
||||||
|
): Promise<Subscription> {
|
||||||
|
await this.ensureWebSocket();
|
||||||
|
|
||||||
|
const id = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
this.subscriptions.set(id, callback);
|
||||||
|
|
||||||
|
this.ws!.send(JSON.stringify({
|
||||||
|
type: 'subscribe',
|
||||||
|
id,
|
||||||
|
subscription: type,
|
||||||
|
...params,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
unsubscribe: () => this.unsubscribe(id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsubscribe(id: string): void {
|
||||||
|
this.subscriptions.delete(id);
|
||||||
|
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
type: 'unsubscribe',
|
||||||
|
id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureWebSocket(): Promise<void> {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const wsUrl = `${this.config.wsEndpoint}?apiKey=${this.config.apiKey}&network=${this.config.network}`;
|
||||||
|
|
||||||
|
this.ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log('[SynorRpc] WebSocket connected');
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
if (message.subscriptionId && this.subscriptions.has(message.subscriptionId)) {
|
||||||
|
this.subscriptions.get(message.subscriptionId)!(message.data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.error('[SynorRpc] Failed to parse WebSocket message');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('[SynorRpc] WebSocket error:', error);
|
||||||
|
reject(new RpcError('WebSocket connection failed'));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log('[SynorRpc] WebSocket closed');
|
||||||
|
}
|
||||||
|
this.attemptReconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private attemptReconnect(): void {
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
console.error('[SynorRpc] Max reconnect attempts reached');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.subscriptions.size > 0) {
|
||||||
|
this.ensureWebSocket().catch(console.error);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the client and all connections.
|
||||||
|
*/
|
||||||
|
close(): void {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
this.subscriptions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== HTTP Request ====================
|
||||||
|
|
||||||
|
private async request(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: unknown,
|
||||||
|
params?: Record<string, string>
|
||||||
|
): Promise<any> {
|
||||||
|
let url = `${this.config.endpoint}${path}`;
|
||||||
|
|
||||||
|
if (params && Object.keys(params).length > 0) {
|
||||||
|
const searchParams = new URLSearchParams(params);
|
||||||
|
url += `?${searchParams.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log(`[SynorRpc] ${method} ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.config.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Network': this.config.network,
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ message: response.statusText }));
|
||||||
|
throw new RpcError(
|
||||||
|
error.message || 'Request failed',
|
||||||
|
response.status,
|
||||||
|
error.code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
sdk/js/src/rpc/index.ts
Normal file
26
sdk/js/src/rpc/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Synor RPC SDK
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SynorRpc, RpcError } from './client';
|
||||||
|
export type {
|
||||||
|
RpcConfig,
|
||||||
|
TransactionStatus,
|
||||||
|
Priority,
|
||||||
|
SubscriptionType,
|
||||||
|
BlockHeader,
|
||||||
|
Block,
|
||||||
|
TxInput,
|
||||||
|
TxOutput,
|
||||||
|
Transaction,
|
||||||
|
FeeEstimate,
|
||||||
|
ChainInfo,
|
||||||
|
MempoolInfo,
|
||||||
|
UTXO,
|
||||||
|
Subscription,
|
||||||
|
BlockNotification,
|
||||||
|
TransactionNotification,
|
||||||
|
Notification,
|
||||||
|
} from './types';
|
||||||
203
sdk/js/src/rpc/types.ts
Normal file
203
sdk/js/src/rpc/types.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
/**
|
||||||
|
* Synor RPC SDK Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** RPC SDK configuration */
|
||||||
|
export interface RpcConfig {
|
||||||
|
/** API key for authentication */
|
||||||
|
apiKey: string;
|
||||||
|
/** HTTP API endpoint */
|
||||||
|
endpoint?: string;
|
||||||
|
/** WebSocket endpoint for subscriptions */
|
||||||
|
wsEndpoint?: string;
|
||||||
|
/** Network type */
|
||||||
|
network?: 'mainnet' | 'testnet';
|
||||||
|
/** Request timeout in milliseconds */
|
||||||
|
timeout?: number;
|
||||||
|
/** Enable debug logging */
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction status */
|
||||||
|
export type TransactionStatus = 'pending' | 'confirmed' | 'failed' | 'replaced';
|
||||||
|
|
||||||
|
/** Transaction priority */
|
||||||
|
export type Priority = 'low' | 'medium' | 'high' | 'urgent';
|
||||||
|
|
||||||
|
/** Subscription type */
|
||||||
|
export type SubscriptionType = 'blocks' | 'transactions' | 'address' | 'mempool';
|
||||||
|
|
||||||
|
/** Block header */
|
||||||
|
export interface BlockHeader {
|
||||||
|
/** Block hash */
|
||||||
|
hash: string;
|
||||||
|
/** Block height */
|
||||||
|
height: number;
|
||||||
|
/** Block version */
|
||||||
|
version: number;
|
||||||
|
/** Previous block hash */
|
||||||
|
previousHash: string;
|
||||||
|
/** Merkle root */
|
||||||
|
merkleRoot: string;
|
||||||
|
/** Block timestamp */
|
||||||
|
timestamp: number;
|
||||||
|
/** Difficulty target */
|
||||||
|
difficulty: string;
|
||||||
|
/** Nonce */
|
||||||
|
nonce: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full block */
|
||||||
|
export interface Block extends BlockHeader {
|
||||||
|
/** Transaction hashes */
|
||||||
|
transactions: string[];
|
||||||
|
/** Block size in bytes */
|
||||||
|
size: number;
|
||||||
|
/** Block weight */
|
||||||
|
weight: number;
|
||||||
|
/** Transaction count */
|
||||||
|
txCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction input */
|
||||||
|
export interface TxInput {
|
||||||
|
/** Previous transaction hash */
|
||||||
|
txid: string;
|
||||||
|
/** Output index */
|
||||||
|
vout: number;
|
||||||
|
/** Script signature */
|
||||||
|
scriptSig: string;
|
||||||
|
/** Sequence number */
|
||||||
|
sequence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction output */
|
||||||
|
export interface TxOutput {
|
||||||
|
/** Amount */
|
||||||
|
value: string;
|
||||||
|
/** Output index */
|
||||||
|
n: number;
|
||||||
|
/** Script pubkey */
|
||||||
|
scriptPubKey: {
|
||||||
|
asm: string;
|
||||||
|
hex: string;
|
||||||
|
type: string;
|
||||||
|
addresses?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction */
|
||||||
|
export interface Transaction {
|
||||||
|
/** Transaction hash */
|
||||||
|
txid: string;
|
||||||
|
/** Block hash (if confirmed) */
|
||||||
|
blockHash?: string;
|
||||||
|
/** Block height (if confirmed) */
|
||||||
|
blockHeight?: number;
|
||||||
|
/** Number of confirmations */
|
||||||
|
confirmations: number;
|
||||||
|
/** Block timestamp */
|
||||||
|
timestamp?: number;
|
||||||
|
/** Transaction status */
|
||||||
|
status: TransactionStatus;
|
||||||
|
/** Raw transaction hex */
|
||||||
|
raw?: string;
|
||||||
|
/** Transaction size */
|
||||||
|
size: number;
|
||||||
|
/** Transaction fee */
|
||||||
|
fee: string;
|
||||||
|
/** Inputs */
|
||||||
|
inputs: TxInput[];
|
||||||
|
/** Outputs */
|
||||||
|
outputs: TxOutput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fee estimate */
|
||||||
|
export interface FeeEstimate {
|
||||||
|
/** Priority level */
|
||||||
|
priority: Priority;
|
||||||
|
/** Fee rate (satoshis per byte) */
|
||||||
|
feeRate: string;
|
||||||
|
/** Estimated blocks until confirmation */
|
||||||
|
estimatedBlocks: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Chain information */
|
||||||
|
export interface ChainInfo {
|
||||||
|
/** Chain name */
|
||||||
|
chain: string;
|
||||||
|
/** Network name */
|
||||||
|
network: string;
|
||||||
|
/** Current block height */
|
||||||
|
height: number;
|
||||||
|
/** Best block hash */
|
||||||
|
bestBlockHash: string;
|
||||||
|
/** Current difficulty */
|
||||||
|
difficulty: string;
|
||||||
|
/** Median time */
|
||||||
|
medianTime: number;
|
||||||
|
/** Chain work */
|
||||||
|
chainWork: string;
|
||||||
|
/** Syncing status */
|
||||||
|
syncing: boolean;
|
||||||
|
/** Sync progress (0-1) */
|
||||||
|
syncProgress: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mempool information */
|
||||||
|
export interface MempoolInfo {
|
||||||
|
/** Number of transactions */
|
||||||
|
size: number;
|
||||||
|
/** Total size in bytes */
|
||||||
|
bytes: number;
|
||||||
|
/** Memory usage */
|
||||||
|
usage: number;
|
||||||
|
/** Maximum mempool size */
|
||||||
|
maxMempool: number;
|
||||||
|
/** Minimum fee for relay */
|
||||||
|
minFee: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTXO */
|
||||||
|
export interface UTXO {
|
||||||
|
/** Transaction hash */
|
||||||
|
txid: string;
|
||||||
|
/** Output index */
|
||||||
|
vout: number;
|
||||||
|
/** Amount */
|
||||||
|
amount: string;
|
||||||
|
/** Address */
|
||||||
|
address: string;
|
||||||
|
/** Confirmations */
|
||||||
|
confirmations: number;
|
||||||
|
/** Script pubkey */
|
||||||
|
scriptPubKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subscription */
|
||||||
|
export interface Subscription {
|
||||||
|
/** Subscription ID */
|
||||||
|
id: string;
|
||||||
|
/** Subscription type */
|
||||||
|
type: SubscriptionType;
|
||||||
|
/** Creation timestamp */
|
||||||
|
createdAt: number;
|
||||||
|
/** Unsubscribe function */
|
||||||
|
unsubscribe: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Block notification */
|
||||||
|
export interface BlockNotification {
|
||||||
|
type: 'block';
|
||||||
|
block: Block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction notification */
|
||||||
|
export interface TransactionNotification {
|
||||||
|
type: 'transaction';
|
||||||
|
transaction: Transaction;
|
||||||
|
address?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notification types */
|
||||||
|
export type Notification = BlockNotification | TransactionNotification;
|
||||||
365
sdk/js/src/storage/client.ts
Normal file
365
sdk/js/src/storage/client.ts
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
/**
|
||||||
|
* Synor Storage SDK Client
|
||||||
|
*
|
||||||
|
* Decentralized storage, pinning, and content retrieval.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { SynorStorage } from '@synor/storage';
|
||||||
|
*
|
||||||
|
* const storage = new SynorStorage({ apiKey: 'your-api-key' });
|
||||||
|
*
|
||||||
|
* // Upload a file
|
||||||
|
* const result = await storage.upload(Buffer.from('Hello, World!'));
|
||||||
|
* console.log('CID:', result.cid);
|
||||||
|
*
|
||||||
|
* // Download content
|
||||||
|
* const data = await storage.download(result.cid);
|
||||||
|
* console.log('Content:', data.toString());
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
StorageConfig,
|
||||||
|
UploadOptions,
|
||||||
|
UploadResponse,
|
||||||
|
DownloadOptions,
|
||||||
|
Pin,
|
||||||
|
PinRequest,
|
||||||
|
ListPinsOptions,
|
||||||
|
ListPinsResponse,
|
||||||
|
GatewayUrl,
|
||||||
|
CarFile,
|
||||||
|
FileEntry,
|
||||||
|
DirectoryEntry,
|
||||||
|
ImportCarResponse,
|
||||||
|
StorageStats,
|
||||||
|
StorageError,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT = 'https://storage.synor.cc/api/v1';
|
||||||
|
const DEFAULT_GATEWAY = 'https://gateway.synor.cc';
|
||||||
|
const DEFAULT_CHUNK_SIZE = 262144; // 256KB
|
||||||
|
const DEFAULT_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
export class SynorStorage {
|
||||||
|
private config: Required<Omit<StorageConfig, 'pinningService'>> & Pick<StorageConfig, 'pinningService'>;
|
||||||
|
|
||||||
|
constructor(config: StorageConfig) {
|
||||||
|
this.config = {
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
endpoint: config.endpoint || DEFAULT_ENDPOINT,
|
||||||
|
gateway: config.gateway || DEFAULT_GATEWAY,
|
||||||
|
pinningService: config.pinningService,
|
||||||
|
chunkSize: config.chunkSize || DEFAULT_CHUNK_SIZE,
|
||||||
|
timeout: config.timeout || DEFAULT_TIMEOUT,
|
||||||
|
debug: config.debug || false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload content to storage.
|
||||||
|
*/
|
||||||
|
async upload(
|
||||||
|
data: Buffer | Uint8Array | string,
|
||||||
|
options: UploadOptions = {}
|
||||||
|
): Promise<UploadResponse> {
|
||||||
|
const formData = new FormData();
|
||||||
|
const blob = new Blob([data]);
|
||||||
|
formData.append('file', blob);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.pin !== undefined) params.set('pin', String(options.pin));
|
||||||
|
if (options.wrapWithDirectory) params.set('wrapWithDirectory', 'true');
|
||||||
|
if (options.cidVersion !== undefined) params.set('cidVersion', String(options.cidVersion));
|
||||||
|
if (options.hashAlgorithm) params.set('hashAlgorithm', options.hashAlgorithm);
|
||||||
|
|
||||||
|
const url = `${this.config.endpoint}/upload${params.toString() ? `?${params}` : ''}`;
|
||||||
|
|
||||||
|
const response = await this.fetchWithAuth(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response as UploadResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a file from a path (Node.js only).
|
||||||
|
*/
|
||||||
|
async uploadFile(
|
||||||
|
filePath: string,
|
||||||
|
options: UploadOptions = {}
|
||||||
|
): Promise<UploadResponse> {
|
||||||
|
// This would need fs module in Node.js
|
||||||
|
// For browser, use upload() with File object
|
||||||
|
const response = await this.request('POST', '/upload/file', {
|
||||||
|
filePath,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return response as UploadResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download content by CID.
|
||||||
|
*/
|
||||||
|
async download(cid: string, options: DownloadOptions = {}): Promise<Buffer> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.offset !== undefined) params.set('offset', String(options.offset));
|
||||||
|
if (options.length !== undefined) params.set('length', String(options.length));
|
||||||
|
|
||||||
|
const url = `${this.config.endpoint}/content/${cid}${params.toString() ? `?${params}` : ''}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.config.apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
await this.handleError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
return Buffer.from(arrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download content as a stream.
|
||||||
|
*/
|
||||||
|
async downloadStream(cid: string, options: DownloadOptions = {}): Promise<ReadableStream<Uint8Array>> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.offset !== undefined) params.set('offset', String(options.offset));
|
||||||
|
if (options.length !== undefined) params.set('length', String(options.length));
|
||||||
|
|
||||||
|
const url = `${this.config.endpoint}/content/${cid}/stream${params.toString() ? `?${params}` : ''}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.config.apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
await this.handleError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('No response body');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin content by CID.
|
||||||
|
*/
|
||||||
|
async pin(request: PinRequest): Promise<Pin> {
|
||||||
|
const response = await this.request('POST', '/pins', request);
|
||||||
|
return response as Pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpin content by CID.
|
||||||
|
*/
|
||||||
|
async unpin(cid: string): Promise<void> {
|
||||||
|
await this.request('DELETE', `/pins/${cid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pin status by CID.
|
||||||
|
*/
|
||||||
|
async getPinStatus(cid: string): Promise<Pin> {
|
||||||
|
const response = await this.request('GET', `/pins/${cid}`);
|
||||||
|
return response as Pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List pins.
|
||||||
|
*/
|
||||||
|
async listPins(options: ListPinsOptions = {}): Promise<ListPinsResponse> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.status) params.set('status', options.status.join(','));
|
||||||
|
if (options.match) params.set('match', options.match);
|
||||||
|
if (options.name) params.set('name', options.name);
|
||||||
|
if (options.limit !== undefined) params.set('limit', String(options.limit));
|
||||||
|
if (options.offset !== undefined) params.set('offset', String(options.offset));
|
||||||
|
|
||||||
|
const response = await this.request('GET', `/pins?${params}`);
|
||||||
|
return response as ListPinsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get gateway URL for content.
|
||||||
|
*/
|
||||||
|
getGatewayUrl(cid: string, path?: string): GatewayUrl {
|
||||||
|
const fullPath = path ? `/${cid}/${path}` : `/${cid}`;
|
||||||
|
return {
|
||||||
|
url: `${this.config.gateway}/ipfs${fullPath}`,
|
||||||
|
cid,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a CAR file from files.
|
||||||
|
*/
|
||||||
|
async createCar(files: FileEntry[]): Promise<CarFile> {
|
||||||
|
const response = await this.request('POST', '/car/create', { files });
|
||||||
|
return response as CarFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a CAR file.
|
||||||
|
*/
|
||||||
|
async importCar(carData: Buffer | string, pin = true): Promise<ImportCarResponse> {
|
||||||
|
const base64 = typeof carData === 'string' ? carData : carData.toString('base64');
|
||||||
|
const response = await this.request('POST', '/car/import', {
|
||||||
|
car: base64,
|
||||||
|
pin,
|
||||||
|
});
|
||||||
|
return response as ImportCarResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export content as a CAR file.
|
||||||
|
*/
|
||||||
|
async exportCar(cid: string): Promise<Buffer> {
|
||||||
|
const url = `${this.config.endpoint}/car/export/${cid}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.config.apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
await this.handleError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
return Buffer.from(arrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a directory from files.
|
||||||
|
*/
|
||||||
|
async createDirectory(files: FileEntry[]): Promise<UploadResponse> {
|
||||||
|
const response = await this.request('POST', '/directory', { files });
|
||||||
|
return response as UploadResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List directory contents.
|
||||||
|
*/
|
||||||
|
async listDirectory(cid: string, path?: string): Promise<DirectoryEntry[]> {
|
||||||
|
const params = path ? `?path=${encodeURIComponent(path)}` : '';
|
||||||
|
const response = await this.request('GET', `/directory/${cid}${params}`);
|
||||||
|
return (response as { entries: DirectoryEntry[] }).entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get storage statistics.
|
||||||
|
*/
|
||||||
|
async getStats(): Promise<StorageStats> {
|
||||||
|
const response = await this.request('GET', '/stats');
|
||||||
|
return response as StorageStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if content exists.
|
||||||
|
*/
|
||||||
|
async exists(cid: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.request('HEAD', `/content/${cid}`);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content metadata.
|
||||||
|
*/
|
||||||
|
async getMetadata(cid: string): Promise<{ size: number; type: string }> {
|
||||||
|
const response = await this.request('GET', `/content/${cid}/metadata`);
|
||||||
|
return response as { size: number; type: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: unknown
|
||||||
|
): Promise<unknown> {
|
||||||
|
const url = `${this.config.endpoint}${path}`;
|
||||||
|
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log(`[SynorStorage] ${method} ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.fetchWithAuth(url, {
|
||||||
|
method,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchWithAuth(url: string, options: RequestInit): Promise<unknown> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Authorization: `Bearer ${this.config.apiKey}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.body && typeof options.body === 'string') {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...(options.headers as Record<string, string>),
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
await this.handleError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204 || options.method === 'HEAD') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleError(response: Response): Promise<never> {
|
||||||
|
let message = 'Unknown error';
|
||||||
|
let code: string | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errorBody = await response.json();
|
||||||
|
message = errorBody.message || message;
|
||||||
|
code = errorBody.code;
|
||||||
|
} catch {
|
||||||
|
message = response.statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = new Error(message) as StorageError;
|
||||||
|
error.statusCode = response.status;
|
||||||
|
error.code = code;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StorageApiError extends Error implements StorageError {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public statusCode: number,
|
||||||
|
public code?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'StorageApiError';
|
||||||
|
}
|
||||||
|
}
|
||||||
51
sdk/js/src/storage/index.ts
Normal file
51
sdk/js/src/storage/index.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* Synor Storage SDK
|
||||||
|
*
|
||||||
|
* Decentralized storage, pinning, and content retrieval.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { SynorStorage } from '@synor/storage';
|
||||||
|
*
|
||||||
|
* const storage = new SynorStorage({ apiKey: 'your-api-key' });
|
||||||
|
*
|
||||||
|
* // Upload content
|
||||||
|
* const result = await storage.upload(Buffer.from('Hello, World!'));
|
||||||
|
* console.log('CID:', result.cid);
|
||||||
|
*
|
||||||
|
* // Pin content
|
||||||
|
* await storage.pin({ cid: result.cid, name: 'my-file' });
|
||||||
|
*
|
||||||
|
* // Get gateway URL
|
||||||
|
* const gateway = storage.getGatewayUrl(result.cid);
|
||||||
|
* console.log('URL:', gateway.url);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SynorStorage, StorageApiError } from './client';
|
||||||
|
export type {
|
||||||
|
StorageConfig,
|
||||||
|
PinStatus,
|
||||||
|
HashAlgorithm,
|
||||||
|
EntryType,
|
||||||
|
MatchType,
|
||||||
|
UploadOptions,
|
||||||
|
UploadProgress,
|
||||||
|
UploadResponse,
|
||||||
|
DownloadOptions,
|
||||||
|
DownloadProgress,
|
||||||
|
Pin,
|
||||||
|
PinRequest,
|
||||||
|
ListPinsOptions,
|
||||||
|
ListPinsResponse,
|
||||||
|
GatewayUrl,
|
||||||
|
CarBlock,
|
||||||
|
CarFile,
|
||||||
|
FileEntry,
|
||||||
|
DirectoryEntry,
|
||||||
|
ImportCarResponse,
|
||||||
|
StorageStats,
|
||||||
|
StorageError,
|
||||||
|
} from './types';
|
||||||
158
sdk/js/src/storage/types.ts
Normal file
158
sdk/js/src/storage/types.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
/**
|
||||||
|
* Synor Storage SDK Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Pin status */
|
||||||
|
export type PinStatus = 'queued' | 'pinning' | 'pinned' | 'failed' | 'unpinned';
|
||||||
|
|
||||||
|
/** Hash algorithm */
|
||||||
|
export type HashAlgorithm = 'sha2-256' | 'blake3';
|
||||||
|
|
||||||
|
/** Entry type */
|
||||||
|
export type EntryType = 'file' | 'directory';
|
||||||
|
|
||||||
|
/** Match type for listing pins */
|
||||||
|
export type MatchType = 'exact' | 'iexact' | 'partial' | 'ipartial';
|
||||||
|
|
||||||
|
/** Storage SDK configuration */
|
||||||
|
export interface StorageConfig {
|
||||||
|
apiKey: string;
|
||||||
|
endpoint?: string;
|
||||||
|
gateway?: string;
|
||||||
|
pinningService?: string;
|
||||||
|
chunkSize?: number;
|
||||||
|
timeout?: number;
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload options */
|
||||||
|
export interface UploadOptions {
|
||||||
|
pin?: boolean;
|
||||||
|
wrapWithDirectory?: boolean;
|
||||||
|
cidVersion?: 0 | 1;
|
||||||
|
hashAlgorithm?: HashAlgorithm;
|
||||||
|
onProgress?: (progress: UploadProgress) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload progress */
|
||||||
|
export interface UploadProgress {
|
||||||
|
bytesUploaded: number;
|
||||||
|
totalBytes: number;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload response */
|
||||||
|
export interface UploadResponse {
|
||||||
|
cid: string;
|
||||||
|
size: number;
|
||||||
|
name?: string;
|
||||||
|
hash?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Download options */
|
||||||
|
export interface DownloadOptions {
|
||||||
|
offset?: number;
|
||||||
|
length?: number;
|
||||||
|
onProgress?: (progress: DownloadProgress) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Download progress */
|
||||||
|
export interface DownloadProgress {
|
||||||
|
bytesDownloaded: number;
|
||||||
|
totalBytes: number;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pin information */
|
||||||
|
export interface Pin {
|
||||||
|
cid: string;
|
||||||
|
status: PinStatus;
|
||||||
|
name?: string;
|
||||||
|
size?: number;
|
||||||
|
createdAt?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
delegates?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pin request */
|
||||||
|
export interface PinRequest {
|
||||||
|
cid: string;
|
||||||
|
name?: string;
|
||||||
|
duration?: number;
|
||||||
|
origins?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List pins options */
|
||||||
|
export interface ListPinsOptions {
|
||||||
|
status?: PinStatus[];
|
||||||
|
match?: MatchType;
|
||||||
|
name?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List pins response */
|
||||||
|
export interface ListPinsResponse {
|
||||||
|
pins: Pin[];
|
||||||
|
total: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gateway URL */
|
||||||
|
export interface GatewayUrl {
|
||||||
|
url: string;
|
||||||
|
cid: string;
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CAR block */
|
||||||
|
export interface CarBlock {
|
||||||
|
cid: string;
|
||||||
|
data: string; // Base64-encoded
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CAR file */
|
||||||
|
export interface CarFile {
|
||||||
|
version: 1 | 2;
|
||||||
|
roots: string[];
|
||||||
|
blocks?: CarBlock[];
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** File entry for directory creation */
|
||||||
|
export interface FileEntry {
|
||||||
|
name: string;
|
||||||
|
content?: string | Buffer;
|
||||||
|
cid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Directory entry */
|
||||||
|
export interface DirectoryEntry {
|
||||||
|
name: string;
|
||||||
|
cid: string;
|
||||||
|
size?: number;
|
||||||
|
type: EntryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Import CAR response */
|
||||||
|
export interface ImportCarResponse {
|
||||||
|
roots: string[];
|
||||||
|
blocksImported: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Storage statistics */
|
||||||
|
export interface StorageStats {
|
||||||
|
totalSize: number;
|
||||||
|
pinCount: number;
|
||||||
|
bandwidth?: {
|
||||||
|
upload: number;
|
||||||
|
download: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API error response */
|
||||||
|
export interface StorageError extends Error {
|
||||||
|
statusCode: number;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
334
sdk/js/src/wallet/client.ts
Normal file
334
sdk/js/src/wallet/client.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
/**
|
||||||
|
* Synor Wallet Client
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
WalletConfig,
|
||||||
|
Wallet,
|
||||||
|
WalletType,
|
||||||
|
CreateWalletResult,
|
||||||
|
StealthAddress,
|
||||||
|
Transaction,
|
||||||
|
SignedTransaction,
|
||||||
|
SignMessageOptions,
|
||||||
|
SignedMessage,
|
||||||
|
UTXO,
|
||||||
|
Balance,
|
||||||
|
BalanceResponse,
|
||||||
|
GetUtxosOptions,
|
||||||
|
ImportWalletOptions,
|
||||||
|
BuildTransactionOptions,
|
||||||
|
FeeEstimate,
|
||||||
|
Priority,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT = 'https://wallet.synor.cc/api/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synor Wallet SDK error.
|
||||||
|
*/
|
||||||
|
export class WalletError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public statusCode?: number,
|
||||||
|
public code?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'WalletError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Synor Wallet client.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const wallet = new SynorWallet({ apiKey: 'sk_...' });
|
||||||
|
*
|
||||||
|
* // Create a new wallet
|
||||||
|
* const { wallet: w, mnemonic } = await wallet.createWallet();
|
||||||
|
* console.log('Address:', w.address);
|
||||||
|
* console.log('Mnemonic:', mnemonic); // Store securely!
|
||||||
|
*
|
||||||
|
* // Get balance
|
||||||
|
* const balance = await wallet.getBalance(w.address);
|
||||||
|
* console.log('Balance:', balance.native.total);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class SynorWallet {
|
||||||
|
private config: Required<Omit<WalletConfig, 'derivationPath'>> & { derivationPath?: string };
|
||||||
|
|
||||||
|
constructor(config: WalletConfig) {
|
||||||
|
this.config = {
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
||||||
|
network: config.network ?? 'mainnet',
|
||||||
|
timeout: config.timeout ?? 30000,
|
||||||
|
debug: config.debug ?? false,
|
||||||
|
derivationPath: config.derivationPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new wallet.
|
||||||
|
*
|
||||||
|
* @param type - Wallet type (standard, stealth)
|
||||||
|
* @returns Created wallet and mnemonic phrase
|
||||||
|
*/
|
||||||
|
async createWallet(type: WalletType = 'standard'): Promise<CreateWalletResult> {
|
||||||
|
const response = await this.request('POST', '/wallets', {
|
||||||
|
type,
|
||||||
|
network: this.config.network,
|
||||||
|
derivationPath: this.config.derivationPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
wallet: response.wallet,
|
||||||
|
mnemonic: response.mnemonic,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a wallet from mnemonic phrase.
|
||||||
|
*
|
||||||
|
* @param options - Import options including mnemonic
|
||||||
|
* @returns Imported wallet
|
||||||
|
*/
|
||||||
|
async importWallet(options: ImportWalletOptions): Promise<Wallet> {
|
||||||
|
const response = await this.request('POST', '/wallets/import', {
|
||||||
|
mnemonic: options.mnemonic,
|
||||||
|
passphrase: options.passphrase,
|
||||||
|
type: options.type ?? 'standard',
|
||||||
|
network: this.config.network,
|
||||||
|
derivationPath: this.config.derivationPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get wallet by ID.
|
||||||
|
*
|
||||||
|
* @param walletId - Wallet ID
|
||||||
|
* @returns Wallet details
|
||||||
|
*/
|
||||||
|
async getWallet(walletId: string): Promise<Wallet> {
|
||||||
|
const response = await this.request('GET', `/wallets/${walletId}`);
|
||||||
|
return response.wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all wallets for this account.
|
||||||
|
*
|
||||||
|
* @returns List of wallets
|
||||||
|
*/
|
||||||
|
async listWallets(): Promise<Wallet[]> {
|
||||||
|
const response = await this.request('GET', '/wallets');
|
||||||
|
return response.wallets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get address at a specific index for a wallet.
|
||||||
|
*
|
||||||
|
* @param walletId - Wallet ID
|
||||||
|
* @param index - Derivation index
|
||||||
|
* @returns Address at the index
|
||||||
|
*/
|
||||||
|
async getAddress(walletId: string, index: number = 0): Promise<string> {
|
||||||
|
const response = await this.request('GET', `/wallets/${walletId}/addresses/${index}`);
|
||||||
|
return response.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a stealth address for receiving private payments.
|
||||||
|
*
|
||||||
|
* @param walletId - Wallet ID
|
||||||
|
* @returns Stealth address details
|
||||||
|
*/
|
||||||
|
async getStealthAddress(walletId: string): Promise<StealthAddress> {
|
||||||
|
const response = await this.request('POST', `/wallets/${walletId}/stealth`);
|
||||||
|
return response.stealthAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a transaction.
|
||||||
|
*
|
||||||
|
* @param walletId - Wallet ID
|
||||||
|
* @param transaction - Transaction to sign
|
||||||
|
* @returns Signed transaction
|
||||||
|
*/
|
||||||
|
async signTransaction(walletId: string, transaction: Transaction): Promise<SignedTransaction> {
|
||||||
|
const response = await this.request('POST', `/wallets/${walletId}/sign`, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.signedTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message.
|
||||||
|
*
|
||||||
|
* @param walletId - Wallet ID
|
||||||
|
* @param options - Message signing options
|
||||||
|
* @returns Signed message
|
||||||
|
*/
|
||||||
|
async signMessage(walletId: string, options: SignMessageOptions): Promise<SignedMessage> {
|
||||||
|
const response = await this.request('POST', `/wallets/${walletId}/sign-message`, {
|
||||||
|
message: options.message,
|
||||||
|
format: options.format ?? 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a signed message.
|
||||||
|
*
|
||||||
|
* @param message - Original message
|
||||||
|
* @param signature - Signature to verify
|
||||||
|
* @param address - Expected signer address
|
||||||
|
* @returns True if signature is valid
|
||||||
|
*/
|
||||||
|
async verifyMessage(message: string, signature: string, address: string): Promise<boolean> {
|
||||||
|
const response = await this.request('POST', '/verify-message', {
|
||||||
|
message,
|
||||||
|
signature,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get balance for an address.
|
||||||
|
*
|
||||||
|
* @param address - Address to check
|
||||||
|
* @param includeTokens - Include token balances
|
||||||
|
* @returns Balance information
|
||||||
|
*/
|
||||||
|
async getBalance(address: string, includeTokens: boolean = false): Promise<BalanceResponse> {
|
||||||
|
const response = await this.request('GET', `/balances/${address}`, undefined, {
|
||||||
|
includeTokens: includeTokens.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get UTXOs for an address.
|
||||||
|
*
|
||||||
|
* @param address - Address to query
|
||||||
|
* @param options - Query options
|
||||||
|
* @returns List of UTXOs
|
||||||
|
*/
|
||||||
|
async getUtxos(address: string, options?: GetUtxosOptions): Promise<UTXO[]> {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (options?.minConfirmations) {
|
||||||
|
params.minConfirmations = options.minConfirmations.toString();
|
||||||
|
}
|
||||||
|
if (options?.minAmount) {
|
||||||
|
params.minAmount = options.minAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.request('GET', `/utxos/${address}`, undefined, params);
|
||||||
|
return response.utxos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a transaction (without signing).
|
||||||
|
*
|
||||||
|
* @param walletId - Wallet ID
|
||||||
|
* @param options - Transaction building options
|
||||||
|
* @returns Unsigned transaction
|
||||||
|
*/
|
||||||
|
async buildTransaction(walletId: string, options: BuildTransactionOptions): Promise<Transaction> {
|
||||||
|
const response = await this.request('POST', `/wallets/${walletId}/build-tx`, {
|
||||||
|
to: options.to,
|
||||||
|
amount: options.amount,
|
||||||
|
feeRate: options.feeRate,
|
||||||
|
utxos: options.utxos,
|
||||||
|
changeAddress: options.changeAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and sign a transaction in one step.
|
||||||
|
*
|
||||||
|
* @param walletId - Wallet ID
|
||||||
|
* @param options - Transaction building options
|
||||||
|
* @returns Signed transaction
|
||||||
|
*/
|
||||||
|
async sendTransaction(walletId: string, options: BuildTransactionOptions): Promise<SignedTransaction> {
|
||||||
|
const tx = await this.buildTransaction(walletId, options);
|
||||||
|
return this.signTransaction(walletId, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate transaction fee.
|
||||||
|
*
|
||||||
|
* @param priority - Priority level
|
||||||
|
* @returns Fee estimate
|
||||||
|
*/
|
||||||
|
async estimateFee(priority: Priority = 'medium'): Promise<FeeEstimate> {
|
||||||
|
const response = await this.request('GET', '/fees/estimate', undefined, {
|
||||||
|
priority,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all fee estimates.
|
||||||
|
*
|
||||||
|
* @returns Fee estimates for all priority levels
|
||||||
|
*/
|
||||||
|
async getAllFeeEstimates(): Promise<FeeEstimate[]> {
|
||||||
|
const response = await this.request('GET', '/fees/estimate/all');
|
||||||
|
return response.estimates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an API request.
|
||||||
|
*/
|
||||||
|
private async request(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: unknown,
|
||||||
|
params?: Record<string, string>
|
||||||
|
): Promise<any> {
|
||||||
|
let url = `${this.config.endpoint}${path}`;
|
||||||
|
|
||||||
|
if (params && Object.keys(params).length > 0) {
|
||||||
|
const searchParams = new URLSearchParams(params);
|
||||||
|
url += `?${searchParams.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.debug) {
|
||||||
|
console.log(`[SynorWallet] ${method} ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.config.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Network': this.config.network,
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
signal: AbortSignal.timeout(this.config.timeout),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ message: response.statusText }));
|
||||||
|
throw new WalletError(
|
||||||
|
error.message || 'Request failed',
|
||||||
|
response.status,
|
||||||
|
error.code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
sdk/js/src/wallet/index.ts
Normal file
29
sdk/js/src/wallet/index.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Synor Wallet SDK
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SynorWallet, WalletError } from './client';
|
||||||
|
export type {
|
||||||
|
WalletConfig,
|
||||||
|
WalletType,
|
||||||
|
Wallet,
|
||||||
|
CreateWalletResult,
|
||||||
|
StealthAddress,
|
||||||
|
TransactionInput,
|
||||||
|
TransactionOutput,
|
||||||
|
Transaction,
|
||||||
|
SignedTransaction,
|
||||||
|
SignMessageOptions,
|
||||||
|
SignedMessage,
|
||||||
|
UTXO,
|
||||||
|
Balance,
|
||||||
|
TokenBalance,
|
||||||
|
BalanceResponse,
|
||||||
|
GetUtxosOptions,
|
||||||
|
ImportWalletOptions,
|
||||||
|
BuildTransactionOptions,
|
||||||
|
Priority,
|
||||||
|
FeeEstimate,
|
||||||
|
} from './types';
|
||||||
213
sdk/js/src/wallet/types.ts
Normal file
213
sdk/js/src/wallet/types.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
/**
|
||||||
|
* Synor Wallet SDK Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Wallet SDK configuration */
|
||||||
|
export interface WalletConfig {
|
||||||
|
/** API key for authentication */
|
||||||
|
apiKey: string;
|
||||||
|
/** API endpoint (defaults to production) */
|
||||||
|
endpoint?: string;
|
||||||
|
/** Network type */
|
||||||
|
network?: 'mainnet' | 'testnet';
|
||||||
|
/** Request timeout in milliseconds */
|
||||||
|
timeout?: number;
|
||||||
|
/** Enable debug logging */
|
||||||
|
debug?: boolean;
|
||||||
|
/** BIP44 derivation path */
|
||||||
|
derivationPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wallet type */
|
||||||
|
export type WalletType = 'standard' | 'multisig' | 'stealth' | 'hardware';
|
||||||
|
|
||||||
|
/** Wallet instance */
|
||||||
|
export interface Wallet {
|
||||||
|
/** Unique wallet ID */
|
||||||
|
id: string;
|
||||||
|
/** Primary address */
|
||||||
|
address: string;
|
||||||
|
/** Compressed public key (hex) */
|
||||||
|
publicKey: string;
|
||||||
|
/** Wallet type */
|
||||||
|
type: WalletType;
|
||||||
|
/** Creation timestamp */
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wallet creation result */
|
||||||
|
export interface CreateWalletResult {
|
||||||
|
/** Created wallet */
|
||||||
|
wallet: Wallet;
|
||||||
|
/** BIP39 mnemonic phrase (24 words) - SENSITIVE */
|
||||||
|
mnemonic: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stealth address */
|
||||||
|
export interface StealthAddress {
|
||||||
|
/** One-time address */
|
||||||
|
address: string;
|
||||||
|
/** View public key */
|
||||||
|
viewKey: string;
|
||||||
|
/** Spend public key */
|
||||||
|
spendKey: string;
|
||||||
|
/** Ephemeral public key */
|
||||||
|
ephemeralKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction input */
|
||||||
|
export interface TransactionInput {
|
||||||
|
/** Previous transaction hash */
|
||||||
|
txid: string;
|
||||||
|
/** Output index */
|
||||||
|
vout: number;
|
||||||
|
/** Amount */
|
||||||
|
amount: string;
|
||||||
|
/** Script signature (for signed inputs) */
|
||||||
|
scriptSig?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction output */
|
||||||
|
export interface TransactionOutput {
|
||||||
|
/** Recipient address */
|
||||||
|
address: string;
|
||||||
|
/** Amount */
|
||||||
|
amount: string;
|
||||||
|
/** Script pubkey */
|
||||||
|
scriptPubKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unsigned transaction */
|
||||||
|
export interface Transaction {
|
||||||
|
/** Version */
|
||||||
|
version: number;
|
||||||
|
/** Inputs */
|
||||||
|
inputs: TransactionInput[];
|
||||||
|
/** Outputs */
|
||||||
|
outputs: TransactionOutput[];
|
||||||
|
/** Lock time */
|
||||||
|
lockTime?: number;
|
||||||
|
/** Transaction fee */
|
||||||
|
fee?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signed transaction */
|
||||||
|
export interface SignedTransaction {
|
||||||
|
/** Raw hex-encoded transaction */
|
||||||
|
raw: string;
|
||||||
|
/** Transaction hash */
|
||||||
|
txid: string;
|
||||||
|
/** Transaction size in bytes */
|
||||||
|
size: number;
|
||||||
|
/** Transaction weight */
|
||||||
|
weight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Message signing options */
|
||||||
|
export interface SignMessageOptions {
|
||||||
|
/** Message to sign */
|
||||||
|
message: string;
|
||||||
|
/** Message format */
|
||||||
|
format?: 'text' | 'hex' | 'base64';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signed message result */
|
||||||
|
export interface SignedMessage {
|
||||||
|
/** Signature (hex) */
|
||||||
|
signature: string;
|
||||||
|
/** Public key used for signing */
|
||||||
|
publicKey: string;
|
||||||
|
/** Address that signed */
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTXO (Unspent Transaction Output) */
|
||||||
|
export interface UTXO {
|
||||||
|
/** Transaction hash */
|
||||||
|
txid: string;
|
||||||
|
/** Output index */
|
||||||
|
vout: number;
|
||||||
|
/** Amount */
|
||||||
|
amount: string;
|
||||||
|
/** Address */
|
||||||
|
address: string;
|
||||||
|
/** Number of confirmations */
|
||||||
|
confirmations: number;
|
||||||
|
/** Script pubkey */
|
||||||
|
scriptPubKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Balance information */
|
||||||
|
export interface Balance {
|
||||||
|
/** Confirmed balance */
|
||||||
|
confirmed: string;
|
||||||
|
/** Unconfirmed balance */
|
||||||
|
unconfirmed: string;
|
||||||
|
/** Total balance */
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Token balance */
|
||||||
|
export interface TokenBalance {
|
||||||
|
/** Token contract address */
|
||||||
|
token: string;
|
||||||
|
/** Token symbol */
|
||||||
|
symbol: string;
|
||||||
|
/** Token decimals */
|
||||||
|
decimals: number;
|
||||||
|
/** Balance */
|
||||||
|
balance: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full balance response */
|
||||||
|
export interface BalanceResponse {
|
||||||
|
/** Native token balance */
|
||||||
|
native: Balance;
|
||||||
|
/** Token balances */
|
||||||
|
tokens?: TokenBalance[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTXO query options */
|
||||||
|
export interface GetUtxosOptions {
|
||||||
|
/** Minimum confirmations */
|
||||||
|
minConfirmations?: number;
|
||||||
|
/** Minimum amount */
|
||||||
|
minAmount?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Import wallet options */
|
||||||
|
export interface ImportWalletOptions {
|
||||||
|
/** BIP39 mnemonic phrase */
|
||||||
|
mnemonic: string;
|
||||||
|
/** Optional passphrase */
|
||||||
|
passphrase?: string;
|
||||||
|
/** Wallet type */
|
||||||
|
type?: WalletType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction building options */
|
||||||
|
export interface BuildTransactionOptions {
|
||||||
|
/** Recipient address */
|
||||||
|
to: string;
|
||||||
|
/** Amount to send */
|
||||||
|
amount: string;
|
||||||
|
/** Fee rate (satoshis per byte) */
|
||||||
|
feeRate?: number;
|
||||||
|
/** Specific UTXOs to use */
|
||||||
|
utxos?: UTXO[];
|
||||||
|
/** Change address (defaults to sender) */
|
||||||
|
changeAddress?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transaction priority */
|
||||||
|
export type Priority = 'low' | 'medium' | 'high' | 'urgent';
|
||||||
|
|
||||||
|
/** Fee estimation result */
|
||||||
|
export interface FeeEstimate {
|
||||||
|
/** Priority level */
|
||||||
|
priority: Priority;
|
||||||
|
/** Fee rate */
|
||||||
|
feeRate: string;
|
||||||
|
/** Estimated blocks until confirmation */
|
||||||
|
estimatedBlocks: number;
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
[project]
|
[project]
|
||||||
name = "synor-compute"
|
name = "synor-sdk"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Synor Compute SDK for Python - Access distributed GPU/TPU/NPU compute"
|
description = "Synor SDK for Python - Complete blockchain SDK including Compute, Wallet, RPC, and Storage"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Synor Team", email = "dev@synor.cc" }
|
{ name = "Synor Team", email = "dev@synor.cc" }
|
||||||
]
|
]
|
||||||
keywords = ["synor", "compute", "gpu", "tpu", "npu", "ai", "ml", "distributed-computing"]
|
keywords = ["synor", "blockchain", "compute", "wallet", "rpc", "storage", "ipfs", "gpu", "tpu", "npu", "ai", "ml", "distributed-computing"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
|
|
@ -17,12 +17,14 @@ classifiers = [
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"httpx>=0.25.0",
|
"httpx>=0.25.0",
|
||||||
"numpy>=1.24.0",
|
"numpy>=1.24.0",
|
||||||
"pydantic>=2.0.0",
|
"pydantic>=2.0.0",
|
||||||
|
"websockets>=12.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|
@ -35,8 +37,9 @@ dev = [
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://synor.cc"
|
Homepage = "https://synor.cc"
|
||||||
Documentation = "https://docs.synor.cc/compute"
|
Documentation = "https://docs.synor.cc/sdk"
|
||||||
Repository = "https://github.com/synor/synor"
|
Repository = "https://github.com/synor/synor"
|
||||||
|
Changelog = "https://github.com/synor/synor/blob/main/CHANGELOG.md"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
|
|
|
||||||
57
sdk/python/synor_rpc/__init__.py
Normal file
57
sdk/python/synor_rpc/__init__.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""
|
||||||
|
Synor RPC SDK
|
||||||
|
|
||||||
|
A Python SDK for querying blocks, transactions, and chain state
|
||||||
|
on the Synor blockchain with WebSocket subscription support.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from synor_rpc import SynorRpc
|
||||||
|
>>> async with SynorRpc(api_key="sk_...") as rpc:
|
||||||
|
... block = await rpc.get_latest_block()
|
||||||
|
... print(f"Height: {block.height}")
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import SynorRpc, RpcError
|
||||||
|
from .types import (
|
||||||
|
RpcConfig,
|
||||||
|
Network,
|
||||||
|
Priority,
|
||||||
|
TransactionStatus,
|
||||||
|
SubscriptionType,
|
||||||
|
BlockHeader,
|
||||||
|
Block,
|
||||||
|
TxInput,
|
||||||
|
TxOutput,
|
||||||
|
Transaction,
|
||||||
|
FeeEstimate,
|
||||||
|
ChainInfo,
|
||||||
|
MempoolInfo,
|
||||||
|
UTXO,
|
||||||
|
Subscription,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Client
|
||||||
|
"SynorRpc",
|
||||||
|
"RpcError",
|
||||||
|
# Config
|
||||||
|
"RpcConfig",
|
||||||
|
# Enums
|
||||||
|
"Network",
|
||||||
|
"Priority",
|
||||||
|
"TransactionStatus",
|
||||||
|
"SubscriptionType",
|
||||||
|
# Types
|
||||||
|
"BlockHeader",
|
||||||
|
"Block",
|
||||||
|
"TxInput",
|
||||||
|
"TxOutput",
|
||||||
|
"Transaction",
|
||||||
|
"FeeEstimate",
|
||||||
|
"ChainInfo",
|
||||||
|
"MempoolInfo",
|
||||||
|
"UTXO",
|
||||||
|
"Subscription",
|
||||||
|
]
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
454
sdk/python/synor_rpc/client.py
Normal file
454
sdk/python/synor_rpc/client.py
Normal file
|
|
@ -0,0 +1,454 @@
|
||||||
|
"""Synor RPC Client."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
import httpx
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
from .types import (
|
||||||
|
RpcConfig,
|
||||||
|
Network,
|
||||||
|
Priority,
|
||||||
|
TransactionStatus,
|
||||||
|
SubscriptionType,
|
||||||
|
BlockHeader,
|
||||||
|
Block,
|
||||||
|
TxInput,
|
||||||
|
TxOutput,
|
||||||
|
ScriptPubKey,
|
||||||
|
Transaction,
|
||||||
|
FeeEstimate,
|
||||||
|
ChainInfo,
|
||||||
|
MempoolInfo,
|
||||||
|
UTXO,
|
||||||
|
Balance,
|
||||||
|
Subscription,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RpcError(Exception):
|
||||||
|
"""Synor RPC SDK error."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
status_code: Optional[int] = None,
|
||||||
|
code: Optional[str] = None,
|
||||||
|
):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
|
class SynorRpc:
|
||||||
|
"""
|
||||||
|
Synor RPC SDK client.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> async with SynorRpc(api_key="sk_...") as rpc:
|
||||||
|
... block = await rpc.get_latest_block()
|
||||||
|
... print(f"Height: {block.height}")
|
||||||
|
...
|
||||||
|
... # Subscribe to new blocks
|
||||||
|
... async def on_block(block):
|
||||||
|
... print(f"New block: {block.height}")
|
||||||
|
... sub = await rpc.subscribe_blocks(on_block)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str,
|
||||||
|
endpoint: str = "https://rpc.synor.cc/api/v1",
|
||||||
|
ws_endpoint: str = "wss://rpc.synor.cc/ws",
|
||||||
|
network: Network = Network.MAINNET,
|
||||||
|
timeout: float = 30.0,
|
||||||
|
debug: bool = False,
|
||||||
|
):
|
||||||
|
self.config = RpcConfig(
|
||||||
|
api_key=api_key,
|
||||||
|
endpoint=endpoint,
|
||||||
|
ws_endpoint=ws_endpoint,
|
||||||
|
network=network,
|
||||||
|
timeout=timeout,
|
||||||
|
debug=debug,
|
||||||
|
)
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=endpoint,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Network": network.value,
|
||||||
|
},
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
self._ws: Optional[websockets.WebSocketClientProtocol] = None
|
||||||
|
self._subscriptions: dict[str, Callable] = {}
|
||||||
|
self._ws_task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "SynorRpc":
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: Any) -> None:
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""Close the client."""
|
||||||
|
await self._client.aclose()
|
||||||
|
if self._ws:
|
||||||
|
await self._ws.close()
|
||||||
|
if self._ws_task:
|
||||||
|
self._ws_task.cancel()
|
||||||
|
|
||||||
|
# ==================== Block Operations ====================
|
||||||
|
|
||||||
|
async def get_block(self, hash_or_height: str | int) -> Block:
|
||||||
|
"""Get block by hash or height."""
|
||||||
|
if isinstance(hash_or_height, int):
|
||||||
|
path = f"/blocks/height/{hash_or_height}"
|
||||||
|
else:
|
||||||
|
path = f"/blocks/{hash_or_height}"
|
||||||
|
|
||||||
|
data = await self._request("GET", path)
|
||||||
|
return self._parse_block(data)
|
||||||
|
|
||||||
|
async def get_latest_block(self) -> Block:
|
||||||
|
"""Get the latest block."""
|
||||||
|
data = await self._request("GET", "/blocks/latest")
|
||||||
|
return self._parse_block(data)
|
||||||
|
|
||||||
|
async def get_block_header(self, hash_or_height: str | int) -> BlockHeader:
|
||||||
|
"""Get block header only."""
|
||||||
|
if isinstance(hash_or_height, int):
|
||||||
|
path = f"/blocks/height/{hash_or_height}/header"
|
||||||
|
else:
|
||||||
|
path = f"/blocks/{hash_or_height}/header"
|
||||||
|
|
||||||
|
data = await self._request("GET", path)
|
||||||
|
return self._parse_block_header(data)
|
||||||
|
|
||||||
|
async def get_blocks(self, start_height: int, end_height: int) -> list[Block]:
|
||||||
|
"""Get blocks in a range."""
|
||||||
|
data = await self._request(
|
||||||
|
"GET",
|
||||||
|
"/blocks",
|
||||||
|
params={"start": str(start_height), "end": str(end_height)},
|
||||||
|
)
|
||||||
|
return [self._parse_block(b) for b in data.get("blocks", [])]
|
||||||
|
|
||||||
|
# ==================== Transaction Operations ====================
|
||||||
|
|
||||||
|
async def get_transaction(self, txid: str) -> Transaction:
|
||||||
|
"""Get transaction by hash."""
|
||||||
|
data = await self._request("GET", f"/transactions/{txid}")
|
||||||
|
return self._parse_transaction(data)
|
||||||
|
|
||||||
|
async def get_raw_transaction(self, txid: str) -> str:
|
||||||
|
"""Get raw transaction hex."""
|
||||||
|
data = await self._request("GET", f"/transactions/{txid}/raw")
|
||||||
|
return data["raw"]
|
||||||
|
|
||||||
|
async def send_raw_transaction(self, raw_tx: str) -> str:
|
||||||
|
"""Send a raw transaction."""
|
||||||
|
data = await self._request("POST", "/transactions", {"raw": raw_tx})
|
||||||
|
return data["txid"]
|
||||||
|
|
||||||
|
async def get_address_transactions(
|
||||||
|
self,
|
||||||
|
address: str,
|
||||||
|
limit: int = 20,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> list[Transaction]:
|
||||||
|
"""Get transactions for an address."""
|
||||||
|
data = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/addresses/{address}/transactions",
|
||||||
|
params={"limit": str(limit), "offset": str(offset)},
|
||||||
|
)
|
||||||
|
return [self._parse_transaction(tx) for tx in data.get("transactions", [])]
|
||||||
|
|
||||||
|
# ==================== Fee Estimation ====================
|
||||||
|
|
||||||
|
async def estimate_fee(self, priority: Priority = Priority.MEDIUM) -> FeeEstimate:
|
||||||
|
"""Estimate fee for a priority level."""
|
||||||
|
data = await self._request(
|
||||||
|
"GET", "/fees/estimate", params={"priority": priority.value}
|
||||||
|
)
|
||||||
|
return FeeEstimate(
|
||||||
|
priority=Priority(data["priority"]),
|
||||||
|
fee_rate=data["feeRate"],
|
||||||
|
estimated_blocks=data["estimatedBlocks"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_all_fee_estimates(self) -> list[FeeEstimate]:
|
||||||
|
"""Get all fee estimates."""
|
||||||
|
data = await self._request("GET", "/fees/estimate/all")
|
||||||
|
return [
|
||||||
|
FeeEstimate(
|
||||||
|
priority=Priority(e["priority"]),
|
||||||
|
fee_rate=e["feeRate"],
|
||||||
|
estimated_blocks=e["estimatedBlocks"],
|
||||||
|
)
|
||||||
|
for e in data.get("estimates", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
# ==================== Chain Information ====================
|
||||||
|
|
||||||
|
async def get_chain_info(self) -> ChainInfo:
|
||||||
|
"""Get chain information."""
|
||||||
|
data = await self._request("GET", "/chain")
|
||||||
|
return ChainInfo(
|
||||||
|
chain=data["chain"],
|
||||||
|
network=data["network"],
|
||||||
|
height=data["height"],
|
||||||
|
best_block_hash=data["bestBlockHash"],
|
||||||
|
difficulty=data["difficulty"],
|
||||||
|
median_time=data["medianTime"],
|
||||||
|
chain_work=data["chainWork"],
|
||||||
|
syncing=data["syncing"],
|
||||||
|
sync_progress=data["syncProgress"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_mempool_info(self) -> MempoolInfo:
|
||||||
|
"""Get mempool information."""
|
||||||
|
data = await self._request("GET", "/mempool")
|
||||||
|
return MempoolInfo(
|
||||||
|
size=data["size"],
|
||||||
|
bytes=data["bytes"],
|
||||||
|
usage=data["usage"],
|
||||||
|
max_mempool=data["maxMempool"],
|
||||||
|
min_fee=data["minFee"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_mempool_transactions(self, limit: int = 100) -> list[str]:
|
||||||
|
"""Get mempool transaction IDs."""
|
||||||
|
data = await self._request(
|
||||||
|
"GET", "/mempool/transactions", params={"limit": str(limit)}
|
||||||
|
)
|
||||||
|
return data.get("txids", [])
|
||||||
|
|
||||||
|
# ==================== UTXO Operations ====================
|
||||||
|
|
||||||
|
async def get_utxos(self, address: str) -> list[UTXO]:
|
||||||
|
"""Get UTXOs for an address."""
|
||||||
|
data = await self._request("GET", f"/addresses/{address}/utxos")
|
||||||
|
return [
|
||||||
|
UTXO(
|
||||||
|
txid=u["txid"],
|
||||||
|
vout=u["vout"],
|
||||||
|
amount=u["amount"],
|
||||||
|
address=u["address"],
|
||||||
|
confirmations=u["confirmations"],
|
||||||
|
script_pub_key=u.get("scriptPubKey"),
|
||||||
|
)
|
||||||
|
for u in data.get("utxos", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_balance(self, address: str) -> Balance:
|
||||||
|
"""Get balance for an address."""
|
||||||
|
data = await self._request("GET", f"/addresses/{address}/balance")
|
||||||
|
return Balance(
|
||||||
|
confirmed=data["confirmed"],
|
||||||
|
unconfirmed=data["unconfirmed"],
|
||||||
|
total=data["total"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== Subscriptions ====================
|
||||||
|
|
||||||
|
async def subscribe_blocks(
|
||||||
|
self, callback: Callable[[Block], None]
|
||||||
|
) -> Subscription:
|
||||||
|
"""Subscribe to new blocks."""
|
||||||
|
return await self._subscribe(
|
||||||
|
SubscriptionType.BLOCKS,
|
||||||
|
lambda data: callback(self._parse_block(data["block"])),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def subscribe_address(
|
||||||
|
self, address: str, callback: Callable[[Transaction], None]
|
||||||
|
) -> Subscription:
|
||||||
|
"""Subscribe to address transactions."""
|
||||||
|
return await self._subscribe(
|
||||||
|
SubscriptionType.ADDRESS,
|
||||||
|
lambda data: callback(self._parse_transaction(data["transaction"])),
|
||||||
|
{"address": address},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def subscribe_mempool(
|
||||||
|
self, callback: Callable[[Transaction], None]
|
||||||
|
) -> Subscription:
|
||||||
|
"""Subscribe to mempool transactions."""
|
||||||
|
return await self._subscribe(
|
||||||
|
SubscriptionType.MEMPOOL,
|
||||||
|
lambda data: callback(self._parse_transaction(data["transaction"])),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _subscribe(
|
||||||
|
self,
|
||||||
|
sub_type: SubscriptionType,
|
||||||
|
callback: Callable,
|
||||||
|
params: Optional[dict] = None,
|
||||||
|
) -> Subscription:
|
||||||
|
"""Create a subscription."""
|
||||||
|
await self._ensure_websocket()
|
||||||
|
|
||||||
|
sub_id = f"{sub_type.value}_{int(time.time() * 1000)}"
|
||||||
|
self._subscriptions[sub_id] = callback
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"type": "subscribe",
|
||||||
|
"id": sub_id,
|
||||||
|
"subscription": sub_type.value,
|
||||||
|
**(params or {}),
|
||||||
|
}
|
||||||
|
await self._ws.send(json.dumps(message))
|
||||||
|
|
||||||
|
def unsubscribe():
|
||||||
|
self._subscriptions.pop(sub_id, None)
|
||||||
|
if self._ws:
|
||||||
|
asyncio.create_task(
|
||||||
|
self._ws.send(json.dumps({"type": "unsubscribe", "id": sub_id}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return Subscription(
|
||||||
|
id=sub_id,
|
||||||
|
type=sub_type,
|
||||||
|
created_at=int(time.time() * 1000),
|
||||||
|
unsubscribe=unsubscribe,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _ensure_websocket(self) -> None:
|
||||||
|
"""Ensure WebSocket connection is established."""
|
||||||
|
if self._ws and self._ws.open:
|
||||||
|
return
|
||||||
|
|
||||||
|
ws_url = (
|
||||||
|
f"{self.config.ws_endpoint}"
|
||||||
|
f"?apiKey={self.config.api_key}"
|
||||||
|
f"&network={self.config.network.value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._ws = await websockets.connect(ws_url)
|
||||||
|
self._ws_task = asyncio.create_task(self._ws_listener())
|
||||||
|
|
||||||
|
async def _ws_listener(self) -> None:
|
||||||
|
"""Listen for WebSocket messages."""
|
||||||
|
try:
|
||||||
|
async for message in self._ws:
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
sub_id = data.get("subscriptionId")
|
||||||
|
if sub_id and sub_id in self._subscriptions:
|
||||||
|
self._subscriptions[sub_id](data.get("data", {}))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
except websockets.ConnectionClosed:
|
||||||
|
if self.config.debug:
|
||||||
|
print("[SynorRpc] WebSocket closed")
|
||||||
|
|
||||||
|
# ==================== HTTP Request ====================
|
||||||
|
|
||||||
|
async def _request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
data: Optional[dict[str, Any]] = None,
|
||||||
|
params: Optional[dict[str, str]] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Make an API request."""
|
||||||
|
if self.config.debug:
|
||||||
|
print(f"[SynorRpc] {method} {path}")
|
||||||
|
|
||||||
|
response = await self._client.request(
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
json=data,
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code >= 400:
|
||||||
|
error = (
|
||||||
|
response.json()
|
||||||
|
if response.content
|
||||||
|
else {"message": response.reason_phrase}
|
||||||
|
)
|
||||||
|
raise RpcError(
|
||||||
|
error.get("message", "Request failed"),
|
||||||
|
response.status_code,
|
||||||
|
error.get("code"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# ==================== Parsers ====================
|
||||||
|
|
||||||
|
def _parse_block_header(self, data: dict) -> BlockHeader:
|
||||||
|
"""Parse block header from API response."""
|
||||||
|
return BlockHeader(
|
||||||
|
hash=data["hash"],
|
||||||
|
height=data["height"],
|
||||||
|
version=data["version"],
|
||||||
|
previous_hash=data["previousHash"],
|
||||||
|
merkle_root=data["merkleRoot"],
|
||||||
|
timestamp=data["timestamp"],
|
||||||
|
difficulty=data["difficulty"],
|
||||||
|
nonce=data["nonce"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_block(self, data: dict) -> Block:
|
||||||
|
"""Parse block from API response."""
|
||||||
|
return Block(
|
||||||
|
hash=data["hash"],
|
||||||
|
height=data["height"],
|
||||||
|
version=data["version"],
|
||||||
|
previous_hash=data["previousHash"],
|
||||||
|
merkle_root=data["merkleRoot"],
|
||||||
|
timestamp=data["timestamp"],
|
||||||
|
difficulty=data["difficulty"],
|
||||||
|
nonce=data["nonce"],
|
||||||
|
transactions=data.get("transactions", []),
|
||||||
|
size=data.get("size", 0),
|
||||||
|
weight=data.get("weight", 0),
|
||||||
|
tx_count=data.get("txCount", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_transaction(self, data: dict) -> Transaction:
|
||||||
|
"""Parse transaction from API response."""
|
||||||
|
inputs = [
|
||||||
|
TxInput(
|
||||||
|
txid=i["txid"],
|
||||||
|
vout=i["vout"],
|
||||||
|
script_sig=i["scriptSig"],
|
||||||
|
sequence=i["sequence"],
|
||||||
|
)
|
||||||
|
for i in data.get("inputs", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
TxOutput(
|
||||||
|
value=o["value"],
|
||||||
|
n=o["n"],
|
||||||
|
script_pub_key=ScriptPubKey(
|
||||||
|
asm=o["scriptPubKey"]["asm"],
|
||||||
|
hex=o["scriptPubKey"]["hex"],
|
||||||
|
type=o["scriptPubKey"]["type"],
|
||||||
|
addresses=o["scriptPubKey"].get("addresses", []),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for o in data.get("outputs", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
return Transaction(
|
||||||
|
txid=data["txid"],
|
||||||
|
confirmations=data.get("confirmations", 0),
|
||||||
|
status=TransactionStatus(data.get("status", "pending")),
|
||||||
|
size=data.get("size", 0),
|
||||||
|
fee=data.get("fee", "0"),
|
||||||
|
inputs=inputs,
|
||||||
|
outputs=outputs,
|
||||||
|
block_hash=data.get("blockHash"),
|
||||||
|
block_height=data.get("blockHeight"),
|
||||||
|
timestamp=data.get("timestamp"),
|
||||||
|
raw=data.get("raw"),
|
||||||
|
)
|
||||||
170
sdk/python/synor_rpc/types.py
Normal file
170
sdk/python/synor_rpc/types.py
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
"""Synor RPC SDK Types."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
|
|
||||||
|
class Network(str, Enum):
|
||||||
|
"""Network type."""
|
||||||
|
MAINNET = "mainnet"
|
||||||
|
TESTNET = "testnet"
|
||||||
|
|
||||||
|
|
||||||
|
class Priority(str, Enum):
|
||||||
|
"""Transaction priority."""
|
||||||
|
LOW = "low"
|
||||||
|
MEDIUM = "medium"
|
||||||
|
HIGH = "high"
|
||||||
|
URGENT = "urgent"
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionStatus(str, Enum):
|
||||||
|
"""Transaction status."""
|
||||||
|
PENDING = "pending"
|
||||||
|
CONFIRMED = "confirmed"
|
||||||
|
FAILED = "failed"
|
||||||
|
REPLACED = "replaced"
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionType(str, Enum):
|
||||||
|
"""Subscription type."""
|
||||||
|
BLOCKS = "blocks"
|
||||||
|
TRANSACTIONS = "transactions"
|
||||||
|
ADDRESS = "address"
|
||||||
|
MEMPOOL = "mempool"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RpcConfig:
|
||||||
|
"""RPC SDK configuration."""
|
||||||
|
api_key: str
|
||||||
|
endpoint: str = "https://rpc.synor.cc/api/v1"
|
||||||
|
ws_endpoint: str = "wss://rpc.synor.cc/ws"
|
||||||
|
network: Network = Network.MAINNET
|
||||||
|
timeout: float = 30.0
|
||||||
|
debug: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlockHeader:
|
||||||
|
"""Block header."""
|
||||||
|
hash: str
|
||||||
|
height: int
|
||||||
|
version: int
|
||||||
|
previous_hash: str
|
||||||
|
merkle_root: str
|
||||||
|
timestamp: int
|
||||||
|
difficulty: str
|
||||||
|
nonce: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Block(BlockHeader):
|
||||||
|
"""Full block with transactions."""
|
||||||
|
transactions: list[str] = field(default_factory=list)
|
||||||
|
size: int = 0
|
||||||
|
weight: int = 0
|
||||||
|
tx_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TxInput:
|
||||||
|
"""Transaction input."""
|
||||||
|
txid: str
|
||||||
|
vout: int
|
||||||
|
script_sig: str
|
||||||
|
sequence: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ScriptPubKey:
|
||||||
|
"""Script pubkey."""
|
||||||
|
asm: str
|
||||||
|
hex: str
|
||||||
|
type: str
|
||||||
|
addresses: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TxOutput:
|
||||||
|
"""Transaction output."""
|
||||||
|
value: str
|
||||||
|
n: int
|
||||||
|
script_pub_key: ScriptPubKey
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Transaction:
|
||||||
|
"""Transaction."""
|
||||||
|
txid: str
|
||||||
|
confirmations: int
|
||||||
|
status: TransactionStatus
|
||||||
|
size: int
|
||||||
|
fee: str
|
||||||
|
inputs: list[TxInput] = field(default_factory=list)
|
||||||
|
outputs: list[TxOutput] = field(default_factory=list)
|
||||||
|
block_hash: Optional[str] = None
|
||||||
|
block_height: Optional[int] = None
|
||||||
|
timestamp: Optional[int] = None
|
||||||
|
raw: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FeeEstimate:
|
||||||
|
"""Fee estimation result."""
|
||||||
|
priority: Priority
|
||||||
|
fee_rate: str
|
||||||
|
estimated_blocks: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChainInfo:
|
||||||
|
"""Chain information."""
|
||||||
|
chain: str
|
||||||
|
network: str
|
||||||
|
height: int
|
||||||
|
best_block_hash: str
|
||||||
|
difficulty: str
|
||||||
|
median_time: int
|
||||||
|
chain_work: str
|
||||||
|
syncing: bool
|
||||||
|
sync_progress: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MempoolInfo:
|
||||||
|
"""Mempool information."""
|
||||||
|
size: int
|
||||||
|
bytes: int
|
||||||
|
usage: int
|
||||||
|
max_mempool: int
|
||||||
|
min_fee: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UTXO:
|
||||||
|
"""Unspent Transaction Output."""
|
||||||
|
txid: str
|
||||||
|
vout: int
|
||||||
|
amount: str
|
||||||
|
address: str
|
||||||
|
confirmations: int
|
||||||
|
script_pub_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Balance:
|
||||||
|
"""Balance information."""
|
||||||
|
confirmed: str
|
||||||
|
unconfirmed: str
|
||||||
|
total: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Subscription:
|
||||||
|
"""WebSocket subscription."""
|
||||||
|
id: str
|
||||||
|
type: SubscriptionType
|
||||||
|
created_at: int
|
||||||
|
unsubscribe: Callable[[], None]
|
||||||
72
sdk/python/synor_storage/__init__.py
Normal file
72
sdk/python/synor_storage/__init__.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"""
|
||||||
|
Synor Storage SDK
|
||||||
|
|
||||||
|
Decentralized storage, pinning, and content retrieval on the Synor network.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from synor_storage import SynorStorage
|
||||||
|
>>> async with SynorStorage(api_key="sk_...") as storage:
|
||||||
|
... result = await storage.upload(b"Hello, World!")
|
||||||
|
... print(f"CID: {result.cid}")
|
||||||
|
...
|
||||||
|
... # Get gateway URL
|
||||||
|
... gateway = storage.get_gateway_url(result.cid)
|
||||||
|
... print(f"URL: {gateway.url}")
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import SynorStorage, StorageError
|
||||||
|
from .types import (
|
||||||
|
StorageConfig,
|
||||||
|
PinStatus,
|
||||||
|
HashAlgorithm,
|
||||||
|
EntryType,
|
||||||
|
MatchType,
|
||||||
|
UploadOptions,
|
||||||
|
UploadProgress,
|
||||||
|
UploadResponse,
|
||||||
|
DownloadOptions,
|
||||||
|
DownloadProgress,
|
||||||
|
Pin,
|
||||||
|
PinRequest,
|
||||||
|
ListPinsOptions,
|
||||||
|
ListPinsResponse,
|
||||||
|
GatewayUrl,
|
||||||
|
CarBlock,
|
||||||
|
CarFile,
|
||||||
|
FileEntry,
|
||||||
|
DirectoryEntry,
|
||||||
|
ImportCarResponse,
|
||||||
|
StorageStats,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Client
|
||||||
|
"SynorStorage",
|
||||||
|
"StorageError",
|
||||||
|
# Config
|
||||||
|
"StorageConfig",
|
||||||
|
# Enums
|
||||||
|
"PinStatus",
|
||||||
|
"HashAlgorithm",
|
||||||
|
"EntryType",
|
||||||
|
"MatchType",
|
||||||
|
# Types
|
||||||
|
"UploadOptions",
|
||||||
|
"UploadProgress",
|
||||||
|
"UploadResponse",
|
||||||
|
"DownloadOptions",
|
||||||
|
"DownloadProgress",
|
||||||
|
"Pin",
|
||||||
|
"PinRequest",
|
||||||
|
"ListPinsOptions",
|
||||||
|
"ListPinsResponse",
|
||||||
|
"GatewayUrl",
|
||||||
|
"CarBlock",
|
||||||
|
"CarFile",
|
||||||
|
"FileEntry",
|
||||||
|
"DirectoryEntry",
|
||||||
|
"ImportCarResponse",
|
||||||
|
"StorageStats",
|
||||||
|
]
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
524
sdk/python/synor_storage/client.py
Normal file
524
sdk/python/synor_storage/client.py
Normal file
|
|
@ -0,0 +1,524 @@
|
||||||
|
"""Synor Storage SDK Client.
|
||||||
|
|
||||||
|
Decentralized storage, pinning, and content retrieval.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from synor_storage import SynorStorage
|
||||||
|
>>> async with SynorStorage(api_key="sk_...") as storage:
|
||||||
|
... result = await storage.upload(b"Hello, World!")
|
||||||
|
... print(f"CID: {result.cid}")
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from typing import Optional, List, AsyncIterator, Union
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .types import (
|
||||||
|
StorageConfig,
|
||||||
|
UploadOptions,
|
||||||
|
UploadResponse,
|
||||||
|
DownloadOptions,
|
||||||
|
Pin,
|
||||||
|
PinRequest,
|
||||||
|
ListPinsOptions,
|
||||||
|
ListPinsResponse,
|
||||||
|
GatewayUrl,
|
||||||
|
CarFile,
|
||||||
|
CarBlock,
|
||||||
|
FileEntry,
|
||||||
|
DirectoryEntry,
|
||||||
|
ImportCarResponse,
|
||||||
|
StorageStats,
|
||||||
|
PinStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StorageError(Exception):
|
||||||
|
"""Storage API error."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, status_code: int, code: Optional[str] = None):
|
||||||
|
super().__init__(message)
|
||||||
|
self.message = message
|
||||||
|
self.status_code = status_code
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
|
class SynorStorage:
|
||||||
|
"""Synor Storage SDK client."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
config: Optional[StorageConfig] = None,
|
||||||
|
):
|
||||||
|
"""Initialize the storage client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: API key for authentication.
|
||||||
|
config: Full configuration object (overrides api_key).
|
||||||
|
"""
|
||||||
|
if config:
|
||||||
|
self.config = config
|
||||||
|
elif api_key:
|
||||||
|
self.config = StorageConfig(api_key=api_key)
|
||||||
|
else:
|
||||||
|
raise ValueError("Either api_key or config must be provided")
|
||||||
|
|
||||||
|
self._client: Optional[httpx.AsyncClient] = None
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "SynorStorage":
|
||||||
|
"""Enter async context manager."""
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
timeout=self.config.timeout,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.config.api_key}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Exit async context manager."""
|
||||||
|
if self._client:
|
||||||
|
await self._client.aclose()
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> httpx.AsyncClient:
|
||||||
|
"""Get the HTTP client."""
|
||||||
|
if not self._client:
|
||||||
|
raise RuntimeError("Client not initialized. Use 'async with' context manager.")
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
async def upload(
|
||||||
|
self,
|
||||||
|
data: Union[bytes, str],
|
||||||
|
options: Optional[UploadOptions] = None,
|
||||||
|
) -> UploadResponse:
|
||||||
|
"""Upload content to storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Content to upload (bytes or string).
|
||||||
|
options: Upload options.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Upload response with CID and size.
|
||||||
|
"""
|
||||||
|
opts = options or UploadOptions()
|
||||||
|
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode()
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if opts.pin is not None:
|
||||||
|
params["pin"] = str(opts.pin).lower()
|
||||||
|
if opts.wrap_with_directory:
|
||||||
|
params["wrapWithDirectory"] = "true"
|
||||||
|
if opts.cid_version is not None:
|
||||||
|
params["cidVersion"] = str(opts.cid_version)
|
||||||
|
if opts.hash_algorithm:
|
||||||
|
params["hashAlgorithm"] = opts.hash_algorithm.value
|
||||||
|
|
||||||
|
files = {"file": ("file", data)}
|
||||||
|
response = await self.client.post(
|
||||||
|
f"{self.config.endpoint}/upload",
|
||||||
|
files=files,
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
self._check_response(response)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
return UploadResponse(
|
||||||
|
cid=result["cid"],
|
||||||
|
size=result["size"],
|
||||||
|
name=result.get("name"),
|
||||||
|
hash=result.get("hash"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def download(
|
||||||
|
self,
|
||||||
|
cid: str,
|
||||||
|
options: Optional[DownloadOptions] = None,
|
||||||
|
) -> bytes:
|
||||||
|
"""Download content by CID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Content ID.
|
||||||
|
options: Download options.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Content as bytes.
|
||||||
|
"""
|
||||||
|
opts = options or DownloadOptions()
|
||||||
|
params = {}
|
||||||
|
if opts.offset is not None:
|
||||||
|
params["offset"] = opts.offset
|
||||||
|
if opts.length is not None:
|
||||||
|
params["length"] = opts.length
|
||||||
|
|
||||||
|
response = await self.client.get(
|
||||||
|
f"{self.config.endpoint}/content/{cid}",
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
self._check_response(response)
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
async def download_stream(
|
||||||
|
self,
|
||||||
|
cid: str,
|
||||||
|
options: Optional[DownloadOptions] = None,
|
||||||
|
) -> AsyncIterator[bytes]:
|
||||||
|
"""Download content as a stream.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Content ID.
|
||||||
|
options: Download options.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Content chunks as bytes.
|
||||||
|
"""
|
||||||
|
opts = options or DownloadOptions()
|
||||||
|
params = {}
|
||||||
|
if opts.offset is not None:
|
||||||
|
params["offset"] = opts.offset
|
||||||
|
if opts.length is not None:
|
||||||
|
params["length"] = opts.length
|
||||||
|
|
||||||
|
async with self.client.stream(
|
||||||
|
"GET",
|
||||||
|
f"{self.config.endpoint}/content/{cid}/stream",
|
||||||
|
params=params,
|
||||||
|
) as response:
|
||||||
|
self._check_response(response)
|
||||||
|
async for chunk in response.aiter_bytes():
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
async def pin(self, request: PinRequest) -> Pin:
|
||||||
|
"""Pin content by CID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Pin request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pin information.
|
||||||
|
"""
|
||||||
|
body = {"cid": request.cid}
|
||||||
|
if request.name:
|
||||||
|
body["name"] = request.name
|
||||||
|
if request.duration:
|
||||||
|
body["duration"] = request.duration
|
||||||
|
if request.origins:
|
||||||
|
body["origins"] = request.origins
|
||||||
|
|
||||||
|
response = await self._request("POST", "/pins", body)
|
||||||
|
return self._parse_pin(response)
|
||||||
|
|
||||||
|
async def unpin(self, cid: str) -> None:
|
||||||
|
"""Unpin content by CID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Content ID.
|
||||||
|
"""
|
||||||
|
await self._request("DELETE", f"/pins/{cid}")
|
||||||
|
|
||||||
|
async def get_pin_status(self, cid: str) -> Pin:
|
||||||
|
"""Get pin status by CID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Content ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pin information.
|
||||||
|
"""
|
||||||
|
response = await self._request("GET", f"/pins/{cid}")
|
||||||
|
return self._parse_pin(response)
|
||||||
|
|
||||||
|
async def list_pins(
|
||||||
|
self,
|
||||||
|
options: Optional[ListPinsOptions] = None,
|
||||||
|
) -> ListPinsResponse:
|
||||||
|
"""List pins.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options: List options.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of pins with pagination info.
|
||||||
|
"""
|
||||||
|
opts = options or ListPinsOptions()
|
||||||
|
params = {}
|
||||||
|
if opts.status:
|
||||||
|
params["status"] = ",".join(s.value for s in opts.status)
|
||||||
|
if opts.match:
|
||||||
|
params["match"] = opts.match.value
|
||||||
|
if opts.name:
|
||||||
|
params["name"] = opts.name
|
||||||
|
if opts.limit is not None:
|
||||||
|
params["limit"] = opts.limit
|
||||||
|
if opts.offset is not None:
|
||||||
|
params["offset"] = opts.offset
|
||||||
|
|
||||||
|
response = await self._request("GET", "/pins", params=params)
|
||||||
|
pins = [self._parse_pin(p) for p in response.get("pins", [])]
|
||||||
|
return ListPinsResponse(
|
||||||
|
pins=pins,
|
||||||
|
total=response.get("total", len(pins)),
|
||||||
|
has_more=response.get("hasMore", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_gateway_url(self, cid: str, path: Optional[str] = None) -> GatewayUrl:
|
||||||
|
"""Get gateway URL for content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Content ID.
|
||||||
|
path: Optional path within content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Gateway URL information.
|
||||||
|
"""
|
||||||
|
full_path = f"/{cid}/{path}" if path else f"/{cid}"
|
||||||
|
return GatewayUrl(
|
||||||
|
url=f"{self.config.gateway}/ipfs{full_path}",
|
||||||
|
cid=cid,
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def create_car(self, files: List[FileEntry]) -> CarFile:
|
||||||
|
"""Create a CAR file from files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: Files to include in the CAR.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CAR file information.
|
||||||
|
"""
|
||||||
|
file_data = []
|
||||||
|
for f in files:
|
||||||
|
entry = {"name": f.name}
|
||||||
|
if f.content:
|
||||||
|
entry["content"] = base64.b64encode(f.content).decode()
|
||||||
|
if f.cid:
|
||||||
|
entry["cid"] = f.cid
|
||||||
|
file_data.append(entry)
|
||||||
|
|
||||||
|
response = await self._request("POST", "/car/create", {"files": file_data})
|
||||||
|
return CarFile(
|
||||||
|
version=response["version"],
|
||||||
|
roots=response["roots"],
|
||||||
|
blocks=[
|
||||||
|
CarBlock(
|
||||||
|
cid=b["cid"],
|
||||||
|
data=b["data"],
|
||||||
|
size=b.get("size"),
|
||||||
|
)
|
||||||
|
for b in response.get("blocks", [])
|
||||||
|
],
|
||||||
|
size=response.get("size"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def import_car(self, car_data: bytes, pin: bool = True) -> ImportCarResponse:
|
||||||
|
"""Import a CAR file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
car_data: CAR file data.
|
||||||
|
pin: Whether to pin imported content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Import result.
|
||||||
|
"""
|
||||||
|
encoded = base64.b64encode(car_data).decode()
|
||||||
|
response = await self._request("POST", "/car/import", {
|
||||||
|
"car": encoded,
|
||||||
|
"pin": pin,
|
||||||
|
})
|
||||||
|
return ImportCarResponse(
|
||||||
|
roots=response["roots"],
|
||||||
|
blocks_imported=response["blocksImported"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def export_car(self, cid: str) -> bytes:
|
||||||
|
"""Export content as a CAR file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Content ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CAR file data.
|
||||||
|
"""
|
||||||
|
response = await self.client.get(
|
||||||
|
f"{self.config.endpoint}/car/export/{cid}"
|
||||||
|
)
|
||||||
|
self._check_response(response)
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
async def create_directory(self, files: List[FileEntry]) -> UploadResponse:
|
||||||
|
"""Create a directory from files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: Files to include in the directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Upload response with directory CID.
|
||||||
|
"""
|
||||||
|
file_data = []
|
||||||
|
for f in files:
|
||||||
|
entry = {"name": f.name}
|
||||||
|
if f.content:
|
||||||
|
entry["content"] = base64.b64encode(f.content).decode()
|
||||||
|
if f.cid:
|
||||||
|
entry["cid"] = f.cid
|
||||||
|
file_data.append(entry)
|
||||||
|
|
||||||
|
response = await self._request("POST", "/directory", {"files": file_data})
|
||||||
|
return UploadResponse(
|
||||||
|
cid=response["cid"],
|
||||||
|
size=response["size"],
|
||||||
|
name=response.get("name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def list_directory(
|
||||||
|
self,
|
||||||
|
cid: str,
|
||||||
|
path: Optional[str] = None,
|
||||||
|
) -> List[DirectoryEntry]:
|
||||||
|
"""List directory contents.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Directory CID.
|
||||||
|
path: Optional path within directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of directory entries.
|
||||||
|
"""
|
||||||
|
params = {"path": path} if path else {}
|
||||||
|
response = await self._request("GET", f"/directory/{cid}", params=params)
|
||||||
|
from .types import EntryType
|
||||||
|
return [
|
||||||
|
DirectoryEntry(
|
||||||
|
name=e["name"],
|
||||||
|
cid=e["cid"],
|
||||||
|
type=EntryType(e["type"]),
|
||||||
|
size=e.get("size"),
|
||||||
|
)
|
||||||
|
for e in response.get("entries", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_stats(self) -> StorageStats:
|
||||||
|
"""Get storage statistics.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Storage statistics.
|
||||||
|
"""
|
||||||
|
response = await self._request("GET", "/stats")
|
||||||
|
bandwidth = response.get("bandwidth", {})
|
||||||
|
return StorageStats(
|
||||||
|
total_size=response["totalSize"],
|
||||||
|
pin_count=response["pinCount"],
|
||||||
|
upload_bandwidth=bandwidth.get("upload"),
|
||||||
|
download_bandwidth=bandwidth.get("download"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def exists(self, cid: str) -> bool:
|
||||||
|
"""Check if content exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Content ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if content exists.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = await self.client.head(
|
||||||
|
f"{self.config.endpoint}/content/{cid}"
|
||||||
|
)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_metadata(self, cid: str) -> dict:
|
||||||
|
"""Get content metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cid: Content ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Metadata dictionary.
|
||||||
|
"""
|
||||||
|
return await self._request("GET", f"/content/{cid}/metadata")
|
||||||
|
|
||||||
|
async def _request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
body: Optional[dict] = None,
|
||||||
|
params: Optional[dict] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Make an API request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: HTTP method.
|
||||||
|
path: API path.
|
||||||
|
body: Request body.
|
||||||
|
params: Query parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response data.
|
||||||
|
"""
|
||||||
|
url = f"{self.config.endpoint}{path}"
|
||||||
|
|
||||||
|
if self.config.debug:
|
||||||
|
print(f"[SynorStorage] {method} {path}")
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
if body:
|
||||||
|
kwargs["json"] = body
|
||||||
|
if params:
|
||||||
|
kwargs["params"] = params
|
||||||
|
|
||||||
|
response = await self.client.request(method, url, **kwargs)
|
||||||
|
self._check_response(response)
|
||||||
|
|
||||||
|
if response.status_code == 204:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def _check_response(self, response: httpx.Response) -> None:
|
||||||
|
"""Check response for errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: HTTP response.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
StorageError: If response indicates an error.
|
||||||
|
"""
|
||||||
|
if response.status_code >= 400:
|
||||||
|
try:
|
||||||
|
error = response.json()
|
||||||
|
message = error.get("message", "Unknown error")
|
||||||
|
code = error.get("code")
|
||||||
|
except Exception:
|
||||||
|
message = response.text or "Unknown error"
|
||||||
|
code = None
|
||||||
|
|
||||||
|
raise StorageError(message, response.status_code, code)
|
||||||
|
|
||||||
|
def _parse_pin(self, data: dict) -> Pin:
|
||||||
|
"""Parse pin from API response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: API response data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pin object.
|
||||||
|
"""
|
||||||
|
return Pin(
|
||||||
|
cid=data["cid"],
|
||||||
|
status=PinStatus(data["status"]),
|
||||||
|
name=data.get("name"),
|
||||||
|
size=data.get("size"),
|
||||||
|
created_at=data.get("createdAt"),
|
||||||
|
expires_at=data.get("expiresAt"),
|
||||||
|
delegates=data.get("delegates", []),
|
||||||
|
)
|
||||||
186
sdk/python/synor_storage/types.py
Normal file
186
sdk/python/synor_storage/types.py
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
"""Synor Storage SDK Types."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Callable, List
|
||||||
|
|
||||||
|
|
||||||
|
class PinStatus(str, Enum):
|
||||||
|
"""Pin status."""
|
||||||
|
QUEUED = "queued"
|
||||||
|
PINNING = "pinning"
|
||||||
|
PINNED = "pinned"
|
||||||
|
FAILED = "failed"
|
||||||
|
UNPINNED = "unpinned"
|
||||||
|
|
||||||
|
|
||||||
|
class HashAlgorithm(str, Enum):
|
||||||
|
"""Hash algorithm."""
|
||||||
|
SHA2_256 = "sha2-256"
|
||||||
|
BLAKE3 = "blake3"
|
||||||
|
|
||||||
|
|
||||||
|
class EntryType(str, Enum):
|
||||||
|
"""Entry type."""
|
||||||
|
FILE = "file"
|
||||||
|
DIRECTORY = "directory"
|
||||||
|
|
||||||
|
|
||||||
|
class MatchType(str, Enum):
|
||||||
|
"""Match type for listing pins."""
|
||||||
|
EXACT = "exact"
|
||||||
|
IEXACT = "iexact"
|
||||||
|
PARTIAL = "partial"
|
||||||
|
IPARTIAL = "ipartial"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StorageConfig:
|
||||||
|
"""Storage SDK configuration."""
|
||||||
|
api_key: str
|
||||||
|
endpoint: str = "https://storage.synor.cc/api/v1"
|
||||||
|
gateway: str = "https://gateway.synor.cc"
|
||||||
|
pinning_service: Optional[str] = None
|
||||||
|
chunk_size: int = 262144 # 256KB
|
||||||
|
timeout: float = 30.0
|
||||||
|
debug: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UploadProgress:
|
||||||
|
"""Upload progress."""
|
||||||
|
bytes_uploaded: int
|
||||||
|
total_bytes: int
|
||||||
|
percentage: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UploadOptions:
|
||||||
|
"""Upload options."""
|
||||||
|
pin: bool = True
|
||||||
|
wrap_with_directory: bool = False
|
||||||
|
cid_version: int = 1
|
||||||
|
hash_algorithm: HashAlgorithm = HashAlgorithm.SHA2_256
|
||||||
|
on_progress: Optional[Callable[[UploadProgress], None]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UploadResponse:
|
||||||
|
"""Upload response."""
|
||||||
|
cid: str
|
||||||
|
size: int
|
||||||
|
name: Optional[str] = None
|
||||||
|
hash: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DownloadProgress:
|
||||||
|
"""Download progress."""
|
||||||
|
bytes_downloaded: int
|
||||||
|
total_bytes: int
|
||||||
|
percentage: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DownloadOptions:
|
||||||
|
"""Download options."""
|
||||||
|
offset: Optional[int] = None
|
||||||
|
length: Optional[int] = None
|
||||||
|
on_progress: Optional[Callable[[DownloadProgress], None]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Pin:
|
||||||
|
"""Pin information."""
|
||||||
|
cid: str
|
||||||
|
status: PinStatus
|
||||||
|
name: Optional[str] = None
|
||||||
|
size: Optional[int] = None
|
||||||
|
created_at: Optional[int] = None
|
||||||
|
expires_at: Optional[int] = None
|
||||||
|
delegates: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PinRequest:
|
||||||
|
"""Pin request."""
|
||||||
|
cid: str
|
||||||
|
name: Optional[str] = None
|
||||||
|
duration: Optional[int] = None
|
||||||
|
origins: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ListPinsOptions:
|
||||||
|
"""List pins options."""
|
||||||
|
status: Optional[List[PinStatus]] = None
|
||||||
|
match: Optional[MatchType] = None
|
||||||
|
name: Optional[str] = None
|
||||||
|
limit: Optional[int] = None
|
||||||
|
offset: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ListPinsResponse:
|
||||||
|
"""List pins response."""
|
||||||
|
pins: List[Pin]
|
||||||
|
total: int
|
||||||
|
has_more: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GatewayUrl:
|
||||||
|
"""Gateway URL."""
|
||||||
|
url: str
|
||||||
|
cid: str
|
||||||
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CarBlock:
|
||||||
|
"""CAR block."""
|
||||||
|
cid: str
|
||||||
|
data: str # Base64-encoded
|
||||||
|
size: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CarFile:
|
||||||
|
"""CAR file."""
|
||||||
|
version: int
|
||||||
|
roots: List[str]
|
||||||
|
blocks: List[CarBlock] = field(default_factory=list)
|
||||||
|
size: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FileEntry:
|
||||||
|
"""File entry for directory creation."""
|
||||||
|
name: str
|
||||||
|
content: Optional[bytes] = None
|
||||||
|
cid: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DirectoryEntry:
|
||||||
|
"""Directory entry."""
|
||||||
|
name: str
|
||||||
|
cid: str
|
||||||
|
type: EntryType
|
||||||
|
size: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ImportCarResponse:
|
||||||
|
"""Import CAR response."""
|
||||||
|
roots: List[str]
|
||||||
|
blocks_imported: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StorageStats:
|
||||||
|
"""Storage statistics."""
|
||||||
|
total_size: int
|
||||||
|
pin_count: int
|
||||||
|
upload_bandwidth: Optional[int] = None
|
||||||
|
download_bandwidth: Optional[int] = None
|
||||||
68
sdk/python/synor_wallet/__init__.py
Normal file
68
sdk/python/synor_wallet/__init__.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
"""
|
||||||
|
Synor Wallet SDK
|
||||||
|
|
||||||
|
A Python SDK for wallet management, key operations, and transaction signing
|
||||||
|
on the Synor blockchain.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from synor_wallet import SynorWallet
|
||||||
|
>>> async with SynorWallet(api_key="sk_...") as wallet:
|
||||||
|
... result = await wallet.create_wallet()
|
||||||
|
... print(f"Address: {result.wallet.address}")
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import SynorWallet, WalletError
|
||||||
|
from .types import (
|
||||||
|
WalletConfig,
|
||||||
|
Network,
|
||||||
|
WalletType,
|
||||||
|
Priority,
|
||||||
|
Wallet,
|
||||||
|
CreateWalletResult,
|
||||||
|
StealthAddress,
|
||||||
|
TransactionInput,
|
||||||
|
TransactionOutput,
|
||||||
|
Transaction,
|
||||||
|
SignedTransaction,
|
||||||
|
SignedMessage,
|
||||||
|
UTXO,
|
||||||
|
Balance,
|
||||||
|
TokenBalance,
|
||||||
|
BalanceResponse,
|
||||||
|
FeeEstimate,
|
||||||
|
GetUtxosOptions,
|
||||||
|
ImportWalletOptions,
|
||||||
|
BuildTransactionOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Client
|
||||||
|
"SynorWallet",
|
||||||
|
"WalletError",
|
||||||
|
# Config
|
||||||
|
"WalletConfig",
|
||||||
|
# Enums
|
||||||
|
"Network",
|
||||||
|
"WalletType",
|
||||||
|
"Priority",
|
||||||
|
# Types
|
||||||
|
"Wallet",
|
||||||
|
"CreateWalletResult",
|
||||||
|
"StealthAddress",
|
||||||
|
"TransactionInput",
|
||||||
|
"TransactionOutput",
|
||||||
|
"Transaction",
|
||||||
|
"SignedTransaction",
|
||||||
|
"SignedMessage",
|
||||||
|
"UTXO",
|
||||||
|
"Balance",
|
||||||
|
"TokenBalance",
|
||||||
|
"BalanceResponse",
|
||||||
|
"FeeEstimate",
|
||||||
|
# Options
|
||||||
|
"GetUtxosOptions",
|
||||||
|
"ImportWalletOptions",
|
||||||
|
"BuildTransactionOptions",
|
||||||
|
]
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
547
sdk/python/synor_wallet/client.py
Normal file
547
sdk/python/synor_wallet/client.py
Normal file
|
|
@ -0,0 +1,547 @@
|
||||||
|
"""Synor Wallet Client."""
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .types import (
|
||||||
|
WalletConfig,
|
||||||
|
Network,
|
||||||
|
WalletType,
|
||||||
|
Wallet,
|
||||||
|
CreateWalletResult,
|
||||||
|
StealthAddress,
|
||||||
|
Transaction,
|
||||||
|
TransactionInput,
|
||||||
|
TransactionOutput,
|
||||||
|
SignedTransaction,
|
||||||
|
SignedMessage,
|
||||||
|
UTXO,
|
||||||
|
Balance,
|
||||||
|
TokenBalance,
|
||||||
|
BalanceResponse,
|
||||||
|
FeeEstimate,
|
||||||
|
Priority,
|
||||||
|
GetUtxosOptions,
|
||||||
|
ImportWalletOptions,
|
||||||
|
BuildTransactionOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WalletError(Exception):
|
||||||
|
"""Synor Wallet SDK error."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
status_code: Optional[int] = None,
|
||||||
|
code: Optional[str] = None,
|
||||||
|
):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
|
class SynorWallet:
|
||||||
|
"""
|
||||||
|
Synor Wallet SDK client.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> async with SynorWallet(api_key="sk_...") as wallet:
|
||||||
|
... result = await wallet.create_wallet()
|
||||||
|
... print(f"Address: {result.wallet.address}")
|
||||||
|
... print(f"Mnemonic: {result.mnemonic}") # Store securely!
|
||||||
|
...
|
||||||
|
... balance = await wallet.get_balance(result.wallet.address)
|
||||||
|
... print(f"Balance: {balance.native.total}")
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str,
|
||||||
|
endpoint: str = "https://wallet.synor.cc/api/v1",
|
||||||
|
network: Network = Network.MAINNET,
|
||||||
|
timeout: float = 30.0,
|
||||||
|
debug: bool = False,
|
||||||
|
derivation_path: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self.config = WalletConfig(
|
||||||
|
api_key=api_key,
|
||||||
|
endpoint=endpoint,
|
||||||
|
network=network,
|
||||||
|
timeout=timeout,
|
||||||
|
debug=debug,
|
||||||
|
derivation_path=derivation_path,
|
||||||
|
)
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=endpoint,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Network": network.value,
|
||||||
|
},
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "SynorWallet":
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: Any) -> None:
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""Close the client."""
|
||||||
|
await self._client.aclose()
|
||||||
|
|
||||||
|
async def create_wallet(
|
||||||
|
self,
|
||||||
|
wallet_type: WalletType = WalletType.STANDARD,
|
||||||
|
) -> CreateWalletResult:
|
||||||
|
"""
|
||||||
|
Create a new wallet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet_type: Type of wallet to create
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created wallet and mnemonic phrase
|
||||||
|
"""
|
||||||
|
response = await self._request(
|
||||||
|
"POST",
|
||||||
|
"/wallets",
|
||||||
|
{
|
||||||
|
"type": wallet_type.value,
|
||||||
|
"network": self.config.network.value,
|
||||||
|
"derivationPath": self.config.derivation_path,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
wallet = self._parse_wallet(response["wallet"])
|
||||||
|
return CreateWalletResult(wallet=wallet, mnemonic=response["mnemonic"])
|
||||||
|
|
||||||
|
async def import_wallet(self, options: ImportWalletOptions) -> Wallet:
|
||||||
|
"""
|
||||||
|
Import a wallet from mnemonic phrase.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options: Import options including mnemonic
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Imported wallet
|
||||||
|
"""
|
||||||
|
response = await self._request(
|
||||||
|
"POST",
|
||||||
|
"/wallets/import",
|
||||||
|
{
|
||||||
|
"mnemonic": options.mnemonic,
|
||||||
|
"passphrase": options.passphrase,
|
||||||
|
"type": options.type.value,
|
||||||
|
"network": self.config.network.value,
|
||||||
|
"derivationPath": self.config.derivation_path,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._parse_wallet(response["wallet"])
|
||||||
|
|
||||||
|
async def get_wallet(self, wallet_id: str) -> Wallet:
|
||||||
|
"""
|
||||||
|
Get wallet by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet_id: Wallet ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Wallet details
|
||||||
|
"""
|
||||||
|
response = await self._request("GET", f"/wallets/{wallet_id}")
|
||||||
|
return self._parse_wallet(response["wallet"])
|
||||||
|
|
||||||
|
async def list_wallets(self) -> list[Wallet]:
|
||||||
|
"""
|
||||||
|
List all wallets for this account.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of wallets
|
||||||
|
"""
|
||||||
|
response = await self._request("GET", "/wallets")
|
||||||
|
return [self._parse_wallet(w) for w in response["wallets"]]
|
||||||
|
|
||||||
|
async def get_address(self, wallet_id: str, index: int = 0) -> str:
|
||||||
|
"""
|
||||||
|
Get address at a specific index for a wallet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet_id: Wallet ID
|
||||||
|
index: Derivation index
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Address at the index
|
||||||
|
"""
|
||||||
|
response = await self._request(
|
||||||
|
"GET", f"/wallets/{wallet_id}/addresses/{index}"
|
||||||
|
)
|
||||||
|
return response["address"]
|
||||||
|
|
||||||
|
async def get_stealth_address(self, wallet_id: str) -> StealthAddress:
|
||||||
|
"""
|
||||||
|
Generate a stealth address for receiving private payments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet_id: Wallet ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Stealth address details
|
||||||
|
"""
|
||||||
|
response = await self._request("POST", f"/wallets/{wallet_id}/stealth")
|
||||||
|
sa = response["stealthAddress"]
|
||||||
|
return StealthAddress(
|
||||||
|
address=sa["address"],
|
||||||
|
view_key=sa["viewKey"],
|
||||||
|
spend_key=sa["spendKey"],
|
||||||
|
ephemeral_key=sa.get("ephemeralKey"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def sign_transaction(
|
||||||
|
self,
|
||||||
|
wallet_id: str,
|
||||||
|
transaction: Transaction,
|
||||||
|
) -> SignedTransaction:
|
||||||
|
"""
|
||||||
|
Sign a transaction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet_id: Wallet ID
|
||||||
|
transaction: Transaction to sign
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Signed transaction
|
||||||
|
"""
|
||||||
|
response = await self._request(
|
||||||
|
"POST",
|
||||||
|
f"/wallets/{wallet_id}/sign",
|
||||||
|
{"transaction": self._serialize_transaction(transaction)},
|
||||||
|
)
|
||||||
|
|
||||||
|
st = response["signedTransaction"]
|
||||||
|
return SignedTransaction(
|
||||||
|
raw=st["raw"],
|
||||||
|
txid=st["txid"],
|
||||||
|
size=st["size"],
|
||||||
|
weight=st.get("weight"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def sign_message(
|
||||||
|
self,
|
||||||
|
wallet_id: str,
|
||||||
|
message: str,
|
||||||
|
format: str = "text",
|
||||||
|
) -> SignedMessage:
|
||||||
|
"""
|
||||||
|
Sign a message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet_id: Wallet ID
|
||||||
|
message: Message to sign
|
||||||
|
format: Message format (text, hex, base64)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Signed message
|
||||||
|
"""
|
||||||
|
response = await self._request(
|
||||||
|
"POST",
|
||||||
|
f"/wallets/{wallet_id}/sign-message",
|
||||||
|
{"message": message, "format": format},
|
||||||
|
)
|
||||||
|
|
||||||
|
return SignedMessage(
|
||||||
|
signature=response["signature"],
|
||||||
|
public_key=response["publicKey"],
|
||||||
|
address=response["address"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def verify_message(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
signature: str,
|
||||||
|
address: str,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Verify a signed message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Original message
|
||||||
|
signature: Signature to verify
|
||||||
|
address: Expected signer address
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if signature is valid
|
||||||
|
"""
|
||||||
|
response = await self._request(
|
||||||
|
"POST",
|
||||||
|
"/verify-message",
|
||||||
|
{"message": message, "signature": signature, "address": address},
|
||||||
|
)
|
||||||
|
return response["valid"]
|
||||||
|
|
||||||
|
async def get_balance(
|
||||||
|
self,
|
||||||
|
address: str,
|
||||||
|
include_tokens: bool = False,
|
||||||
|
) -> BalanceResponse:
|
||||||
|
"""
|
||||||
|
Get balance for an address.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address: Address to check
|
||||||
|
include_tokens: Include token balances
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Balance information
|
||||||
|
"""
|
||||||
|
params = {"includeTokens": str(include_tokens).lower()}
|
||||||
|
response = await self._request(
|
||||||
|
"GET", f"/balances/{address}", params=params
|
||||||
|
)
|
||||||
|
|
||||||
|
native = Balance(
|
||||||
|
confirmed=response["native"]["confirmed"],
|
||||||
|
unconfirmed=response["native"]["unconfirmed"],
|
||||||
|
total=response["native"]["total"],
|
||||||
|
)
|
||||||
|
|
||||||
|
tokens = []
|
||||||
|
if "tokens" in response:
|
||||||
|
tokens = [
|
||||||
|
TokenBalance(
|
||||||
|
token=t["token"],
|
||||||
|
symbol=t["symbol"],
|
||||||
|
decimals=t["decimals"],
|
||||||
|
balance=t["balance"],
|
||||||
|
)
|
||||||
|
for t in response["tokens"]
|
||||||
|
]
|
||||||
|
|
||||||
|
return BalanceResponse(native=native, tokens=tokens)
|
||||||
|
|
||||||
|
async def get_utxos(
|
||||||
|
self,
|
||||||
|
address: str,
|
||||||
|
options: Optional[GetUtxosOptions] = None,
|
||||||
|
) -> list[UTXO]:
|
||||||
|
"""
|
||||||
|
Get UTXOs for an address.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address: Address to query
|
||||||
|
options: Query options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of UTXOs
|
||||||
|
"""
|
||||||
|
params: dict[str, str] = {}
|
||||||
|
if options:
|
||||||
|
if options.min_confirmations:
|
||||||
|
params["minConfirmations"] = str(options.min_confirmations)
|
||||||
|
if options.min_amount:
|
||||||
|
params["minAmount"] = options.min_amount
|
||||||
|
|
||||||
|
response = await self._request(
|
||||||
|
"GET", f"/utxos/{address}", params=params
|
||||||
|
)
|
||||||
|
|
||||||
|
return [self._parse_utxo(u) for u in response["utxos"]]
|
||||||
|
|
||||||
|
async def build_transaction(
|
||||||
|
self,
|
||||||
|
wallet_id: str,
|
||||||
|
options: BuildTransactionOptions,
|
||||||
|
) -> Transaction:
|
||||||
|
"""
|
||||||
|
Build a transaction (without signing).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet_id: Wallet ID
|
||||||
|
options: Transaction building options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Unsigned transaction
|
||||||
|
"""
|
||||||
|
data: dict[str, Any] = {
|
||||||
|
"to": options.to,
|
||||||
|
"amount": options.amount,
|
||||||
|
}
|
||||||
|
if options.fee_rate is not None:
|
||||||
|
data["feeRate"] = options.fee_rate
|
||||||
|
if options.utxos:
|
||||||
|
data["utxos"] = [
|
||||||
|
{"txid": u.txid, "vout": u.vout, "amount": u.amount}
|
||||||
|
for u in options.utxos
|
||||||
|
]
|
||||||
|
if options.change_address:
|
||||||
|
data["changeAddress"] = options.change_address
|
||||||
|
|
||||||
|
response = await self._request(
|
||||||
|
"POST", f"/wallets/{wallet_id}/build-tx", data
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._parse_transaction(response["transaction"])
|
||||||
|
|
||||||
|
async def send_transaction(
|
||||||
|
self,
|
||||||
|
wallet_id: str,
|
||||||
|
options: BuildTransactionOptions,
|
||||||
|
) -> SignedTransaction:
|
||||||
|
"""
|
||||||
|
Build and sign a transaction in one step.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wallet_id: Wallet ID
|
||||||
|
options: Transaction building options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Signed transaction
|
||||||
|
"""
|
||||||
|
tx = await self.build_transaction(wallet_id, options)
|
||||||
|
return await self.sign_transaction(wallet_id, tx)
|
||||||
|
|
||||||
|
async def estimate_fee(
|
||||||
|
self,
|
||||||
|
priority: Priority = Priority.MEDIUM,
|
||||||
|
) -> FeeEstimate:
|
||||||
|
"""
|
||||||
|
Estimate transaction fee.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
priority: Priority level
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Fee estimate
|
||||||
|
"""
|
||||||
|
response = await self._request(
|
||||||
|
"GET", "/fees/estimate", params={"priority": priority.value}
|
||||||
|
)
|
||||||
|
|
||||||
|
return FeeEstimate(
|
||||||
|
priority=Priority(response["priority"]),
|
||||||
|
fee_rate=response["feeRate"],
|
||||||
|
estimated_blocks=response["estimatedBlocks"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_all_fee_estimates(self) -> list[FeeEstimate]:
|
||||||
|
"""
|
||||||
|
Get all fee estimates.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Fee estimates for all priority levels
|
||||||
|
"""
|
||||||
|
response = await self._request("GET", "/fees/estimate/all")
|
||||||
|
|
||||||
|
return [
|
||||||
|
FeeEstimate(
|
||||||
|
priority=Priority(e["priority"]),
|
||||||
|
fee_rate=e["feeRate"],
|
||||||
|
estimated_blocks=e["estimatedBlocks"],
|
||||||
|
)
|
||||||
|
for e in response["estimates"]
|
||||||
|
]
|
||||||
|
|
||||||
|
async def _request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
data: Optional[dict[str, Any]] = None,
|
||||||
|
params: Optional[dict[str, str]] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Make an API request."""
|
||||||
|
if self.config.debug:
|
||||||
|
print(f"[SynorWallet] {method} {path}")
|
||||||
|
|
||||||
|
response = await self._client.request(
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
json=data,
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code >= 400:
|
||||||
|
error = (
|
||||||
|
response.json()
|
||||||
|
if response.content
|
||||||
|
else {"message": response.reason_phrase}
|
||||||
|
)
|
||||||
|
raise WalletError(
|
||||||
|
error.get("message", "Request failed"),
|
||||||
|
response.status_code,
|
||||||
|
error.get("code"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def _parse_wallet(self, data: dict[str, Any]) -> Wallet:
|
||||||
|
"""Parse wallet from API response."""
|
||||||
|
return Wallet(
|
||||||
|
id=data["id"],
|
||||||
|
address=data["address"],
|
||||||
|
public_key=data["publicKey"],
|
||||||
|
type=WalletType(data["type"]),
|
||||||
|
created_at=data["createdAt"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_utxo(self, data: dict[str, Any]) -> UTXO:
|
||||||
|
"""Parse UTXO from API response."""
|
||||||
|
return UTXO(
|
||||||
|
txid=data["txid"],
|
||||||
|
vout=data["vout"],
|
||||||
|
amount=data["amount"],
|
||||||
|
address=data["address"],
|
||||||
|
confirmations=data["confirmations"],
|
||||||
|
script_pub_key=data.get("scriptPubKey"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_transaction(self, data: dict[str, Any]) -> Transaction:
|
||||||
|
"""Parse transaction from API response."""
|
||||||
|
inputs = [
|
||||||
|
TransactionInput(
|
||||||
|
txid=i["txid"],
|
||||||
|
vout=i["vout"],
|
||||||
|
amount=i["amount"],
|
||||||
|
script_sig=i.get("scriptSig"),
|
||||||
|
)
|
||||||
|
for i in data["inputs"]
|
||||||
|
]
|
||||||
|
outputs = [
|
||||||
|
TransactionOutput(
|
||||||
|
address=o["address"],
|
||||||
|
amount=o["amount"],
|
||||||
|
script_pub_key=o.get("scriptPubKey"),
|
||||||
|
)
|
||||||
|
for o in data["outputs"]
|
||||||
|
]
|
||||||
|
return Transaction(
|
||||||
|
version=data["version"],
|
||||||
|
inputs=inputs,
|
||||||
|
outputs=outputs,
|
||||||
|
lock_time=data.get("lockTime", 0),
|
||||||
|
fee=data.get("fee"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _serialize_transaction(self, tx: Transaction) -> dict[str, Any]:
|
||||||
|
"""Serialize transaction for API request."""
|
||||||
|
return {
|
||||||
|
"version": tx.version,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txid": i.txid,
|
||||||
|
"vout": i.vout,
|
||||||
|
"amount": i.amount,
|
||||||
|
}
|
||||||
|
for i in tx.inputs
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"address": o.address,
|
||||||
|
"amount": o.amount,
|
||||||
|
}
|
||||||
|
for o in tx.outputs
|
||||||
|
],
|
||||||
|
"lockTime": tx.lock_time,
|
||||||
|
}
|
||||||
176
sdk/python/synor_wallet/types.py
Normal file
176
sdk/python/synor_wallet/types.py
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
"""Synor Wallet SDK Types."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Network(str, Enum):
|
||||||
|
"""Network type."""
|
||||||
|
MAINNET = "mainnet"
|
||||||
|
TESTNET = "testnet"
|
||||||
|
|
||||||
|
|
||||||
|
class WalletType(str, Enum):
|
||||||
|
"""Wallet type."""
|
||||||
|
STANDARD = "standard"
|
||||||
|
MULTISIG = "multisig"
|
||||||
|
STEALTH = "stealth"
|
||||||
|
HARDWARE = "hardware"
|
||||||
|
|
||||||
|
|
||||||
|
class Priority(str, Enum):
|
||||||
|
"""Transaction priority."""
|
||||||
|
LOW = "low"
|
||||||
|
MEDIUM = "medium"
|
||||||
|
HIGH = "high"
|
||||||
|
URGENT = "urgent"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WalletConfig:
|
||||||
|
"""Wallet SDK configuration."""
|
||||||
|
api_key: str
|
||||||
|
endpoint: str = "https://wallet.synor.cc/api/v1"
|
||||||
|
network: Network = Network.MAINNET
|
||||||
|
timeout: float = 30.0
|
||||||
|
debug: bool = False
|
||||||
|
derivation_path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Wallet:
|
||||||
|
"""Wallet instance."""
|
||||||
|
id: str
|
||||||
|
address: str
|
||||||
|
public_key: str
|
||||||
|
type: WalletType
|
||||||
|
created_at: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CreateWalletResult:
|
||||||
|
"""Wallet creation result."""
|
||||||
|
wallet: Wallet
|
||||||
|
mnemonic: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StealthAddress:
|
||||||
|
"""Stealth address for private payments."""
|
||||||
|
address: str
|
||||||
|
view_key: str
|
||||||
|
spend_key: str
|
||||||
|
ephemeral_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TransactionInput:
|
||||||
|
"""Transaction input."""
|
||||||
|
txid: str
|
||||||
|
vout: int
|
||||||
|
amount: str
|
||||||
|
script_sig: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TransactionOutput:
|
||||||
|
"""Transaction output."""
|
||||||
|
address: str
|
||||||
|
amount: str
|
||||||
|
script_pub_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Transaction:
|
||||||
|
"""Unsigned transaction."""
|
||||||
|
version: int
|
||||||
|
inputs: list[TransactionInput]
|
||||||
|
outputs: list[TransactionOutput]
|
||||||
|
lock_time: int = 0
|
||||||
|
fee: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SignedTransaction:
|
||||||
|
"""Signed transaction."""
|
||||||
|
raw: str
|
||||||
|
txid: str
|
||||||
|
size: int
|
||||||
|
weight: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SignedMessage:
|
||||||
|
"""Signed message."""
|
||||||
|
signature: str
|
||||||
|
public_key: str
|
||||||
|
address: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UTXO:
|
||||||
|
"""Unspent Transaction Output."""
|
||||||
|
txid: str
|
||||||
|
vout: int
|
||||||
|
amount: str
|
||||||
|
address: str
|
||||||
|
confirmations: int
|
||||||
|
script_pub_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Balance:
|
||||||
|
"""Balance information."""
|
||||||
|
confirmed: str
|
||||||
|
unconfirmed: str
|
||||||
|
total: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TokenBalance:
|
||||||
|
"""Token balance."""
|
||||||
|
token: str
|
||||||
|
symbol: str
|
||||||
|
decimals: int
|
||||||
|
balance: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BalanceResponse:
|
||||||
|
"""Full balance response."""
|
||||||
|
native: Balance
|
||||||
|
tokens: list[TokenBalance] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FeeEstimate:
|
||||||
|
"""Fee estimation result."""
|
||||||
|
priority: Priority
|
||||||
|
fee_rate: str
|
||||||
|
estimated_blocks: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GetUtxosOptions:
|
||||||
|
"""UTXO query options."""
|
||||||
|
min_confirmations: int = 1
|
||||||
|
min_amount: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ImportWalletOptions:
|
||||||
|
"""Import wallet options."""
|
||||||
|
mnemonic: str
|
||||||
|
passphrase: Optional[str] = None
|
||||||
|
type: WalletType = WalletType.STANDARD
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BuildTransactionOptions:
|
||||||
|
"""Transaction building options."""
|
||||||
|
to: str
|
||||||
|
amount: str
|
||||||
|
fee_rate: Optional[float] = None
|
||||||
|
utxos: Optional[list[UTXO]] = None
|
||||||
|
change_address: Optional[str] = None
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
[workspace]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "synor-compute"
|
name = "synor-compute"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -10,7 +12,7 @@ keywords = ["compute", "gpu", "ai", "ml", "distributed"]
|
||||||
categories = ["api-bindings", "asynchronous"]
|
categories = ["api-bindings", "asynchronous"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
reqwest = { version = "0.11", features = ["json", "stream", "multipart"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
@ -19,6 +21,8 @@ thiserror = "1"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
base64 = "0.21"
|
||||||
|
urlencoding = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ mod tensor;
|
||||||
mod client;
|
mod client;
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
|
pub mod wallet;
|
||||||
|
pub mod rpc;
|
||||||
|
pub mod storage;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
|
|
||||||
298
sdk/rust/src/rpc/client.rs
Normal file
298
sdk/rust/src/rpc/client.rs
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
//! Synor RPC SDK Client.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use reqwest::{Client, header};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::error::{RpcError, Result};
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Synor RPC client.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SynorRpc {
|
||||||
|
config: RpcConfig,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ApiError {
|
||||||
|
message: String,
|
||||||
|
code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct BlockResponse {
|
||||||
|
block: Block,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct BlockHeaderResponse {
|
||||||
|
header: BlockHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TransactionResponse {
|
||||||
|
transaction: Transaction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct SendTxResponse {
|
||||||
|
txid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct UtxosResponse {
|
||||||
|
utxos: Vec<Utxo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SynorRpc {
|
||||||
|
/// Create a new RPC client with the given API key.
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self::with_config(RpcConfig::new(api_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new RPC client with custom configuration.
|
||||||
|
pub fn with_config(config: RpcConfig) -> Self {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(config.timeout_secs))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
Self { config, client }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a block by hash.
|
||||||
|
pub async fn get_block_by_hash(&self, hash: &str) -> Result<Block> {
|
||||||
|
let path = format!("/blocks/{}", hash);
|
||||||
|
self.request("GET", &path, Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a block by height.
|
||||||
|
pub async fn get_block_by_height(&self, height: i64) -> Result<Block> {
|
||||||
|
let path = format!("/blocks/height/{}", height);
|
||||||
|
self.request("GET", &path, Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the latest block.
|
||||||
|
pub async fn get_latest_block(&self) -> Result<Block> {
|
||||||
|
self.request("GET", "/blocks/latest", Option::<&()>::None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a block header by hash.
|
||||||
|
pub async fn get_block_header_by_hash(&self, hash: &str) -> Result<BlockHeader> {
|
||||||
|
let path = format!("/blocks/{}/header", hash);
|
||||||
|
let resp: BlockHeaderResponse = self.request("GET", &path, Option::<&()>::None).await?;
|
||||||
|
Ok(resp.header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a block header by height.
|
||||||
|
pub async fn get_block_header_by_height(&self, height: i64) -> Result<BlockHeader> {
|
||||||
|
let path = format!("/blocks/height/{}/header", height);
|
||||||
|
let resp: BlockHeaderResponse = self.request("GET", &path, Option::<&()>::None).await?;
|
||||||
|
Ok(resp.header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a transaction by ID.
|
||||||
|
pub async fn get_transaction(&self, txid: &str) -> Result<Transaction> {
|
||||||
|
let path = format!("/transactions/{}", txid);
|
||||||
|
self.request("GET", &path, Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a raw transaction.
|
||||||
|
pub async fn send_raw_transaction(&self, raw_tx: &str) -> Result<String> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SendTxRequest<'a> {
|
||||||
|
raw: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp: SendTxResponse = self
|
||||||
|
.request("POST", "/transactions", Some(&SendTxRequest { raw: raw_tx }))
|
||||||
|
.await?;
|
||||||
|
Ok(resp.txid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimate transaction fee.
|
||||||
|
pub async fn estimate_fee(&self, priority: Priority) -> Result<FeeEstimate> {
|
||||||
|
let priority_str = match priority {
|
||||||
|
Priority::Low => "low",
|
||||||
|
Priority::Medium => "medium",
|
||||||
|
Priority::High => "high",
|
||||||
|
Priority::Urgent => "urgent",
|
||||||
|
};
|
||||||
|
let path = format!("/fees/estimate?priority={}", priority_str);
|
||||||
|
self.request("GET", &path, Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all fee estimates.
|
||||||
|
pub async fn get_all_fee_estimates(&self) -> Result<Vec<FeeEstimate>> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct EstimatesResponse {
|
||||||
|
estimates: Vec<FeeEstimate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp: EstimatesResponse = self
|
||||||
|
.request("GET", "/fees/estimate/all", Option::<&()>::None)
|
||||||
|
.await?;
|
||||||
|
Ok(resp.estimates)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get chain information.
|
||||||
|
pub async fn get_chain_info(&self) -> Result<ChainInfo> {
|
||||||
|
self.request("GET", "/chain", Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mempool information.
|
||||||
|
pub async fn get_mempool_info(&self) -> Result<MempoolInfo> {
|
||||||
|
self.request("GET", "/mempool", Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get UTXOs for an address.
|
||||||
|
pub async fn get_utxos(&self, address: &str) -> Result<Vec<Utxo>> {
|
||||||
|
let path = format!("/addresses/{}/utxos", address);
|
||||||
|
let resp: UtxosResponse = self.request("GET", &path, Option::<&()>::None).await?;
|
||||||
|
Ok(resp.utxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get balance for an address.
|
||||||
|
pub async fn get_balance(&self, address: &str) -> Result<Balance> {
|
||||||
|
let path = format!("/addresses/{}/balance", address);
|
||||||
|
self.request("GET", &path, Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get blocks in a range.
|
||||||
|
pub async fn get_blocks(&self, start_height: i64, end_height: i64) -> Result<Vec<Block>> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct BlocksResponse {
|
||||||
|
blocks: Vec<Block>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = format!("/blocks?start={}&end={}", start_height, end_height);
|
||||||
|
let resp: BlocksResponse = self.request("GET", &path, Option::<&()>::None).await?;
|
||||||
|
Ok(resp.blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get transactions for an address.
|
||||||
|
pub async fn get_address_transactions(
|
||||||
|
&self,
|
||||||
|
address: &str,
|
||||||
|
limit: Option<i32>,
|
||||||
|
offset: Option<i32>,
|
||||||
|
) -> Result<Vec<Transaction>> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TxsResponse {
|
||||||
|
transactions: Vec<Transaction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path = format!("/addresses/{}/transactions", address);
|
||||||
|
let mut params = Vec::new();
|
||||||
|
if let Some(l) = limit {
|
||||||
|
params.push(format!("limit={}", l));
|
||||||
|
}
|
||||||
|
if let Some(o) = offset {
|
||||||
|
params.push(format!("offset={}", o));
|
||||||
|
}
|
||||||
|
if !params.is_empty() {
|
||||||
|
path = format!("{}?{}", path, params.join("&"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp: TxsResponse = self.request("GET", &path, Option::<&()>::None).await?;
|
||||||
|
Ok(resp.transactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mempool transactions.
|
||||||
|
pub async fn get_mempool_transactions(&self, limit: Option<i32>) -> Result<Vec<String>> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MempoolTxsResponse {
|
||||||
|
transactions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = match limit {
|
||||||
|
Some(l) => format!("/mempool/transactions?limit={}", l),
|
||||||
|
None => "/mempool/transactions".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp: MempoolTxsResponse = self.request("GET", &path, Option::<&()>::None).await?;
|
||||||
|
Ok(resp.transactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal HTTP request method.
|
||||||
|
async fn request<T, B>(&self, method: &str, path: &str, body: Option<&B>) -> Result<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
B: Serialize,
|
||||||
|
{
|
||||||
|
let url = format!("{}{}", self.config.endpoint, path);
|
||||||
|
|
||||||
|
if self.config.debug {
|
||||||
|
println!("[SynorRpc] {} {}", method, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let network_str = match self.config.network {
|
||||||
|
Network::Mainnet => "mainnet",
|
||||||
|
Network::Testnet => "testnet",
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut request = match method {
|
||||||
|
"GET" => self.client.get(&url),
|
||||||
|
"POST" => self.client.post(&url),
|
||||||
|
"PUT" => self.client.put(&url),
|
||||||
|
"DELETE" => self.client.delete(&url),
|
||||||
|
_ => return Err(RpcError::Validation(format!("Invalid method: {}", method))),
|
||||||
|
};
|
||||||
|
|
||||||
|
request = request
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {}", self.config.api_key))
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.header("X-Network", network_str);
|
||||||
|
|
||||||
|
if let Some(b) = body {
|
||||||
|
request = request.json(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request.send().await?;
|
||||||
|
let status = response.status();
|
||||||
|
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
let error: ApiError = response.json().await.unwrap_or(ApiError {
|
||||||
|
message: "Unknown error".to_string(),
|
||||||
|
code: None,
|
||||||
|
});
|
||||||
|
return Err(RpcError::Api {
|
||||||
|
message: error.message,
|
||||||
|
status_code: status.as_u16(),
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: T = response.json().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_builder() {
|
||||||
|
let config = RpcConfig::new("test-key")
|
||||||
|
.endpoint("https://custom.endpoint")
|
||||||
|
.network(Network::Testnet)
|
||||||
|
.timeout(60)
|
||||||
|
.debug(true);
|
||||||
|
|
||||||
|
assert_eq!(config.api_key, "test-key");
|
||||||
|
assert_eq!(config.endpoint, "https://custom.endpoint");
|
||||||
|
assert_eq!(config.network, Network::Testnet);
|
||||||
|
assert_eq!(config.timeout_secs, 60);
|
||||||
|
assert!(config.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_creation() {
|
||||||
|
let client = SynorRpc::new("test-key");
|
||||||
|
assert_eq!(client.config.api_key, "test-key");
|
||||||
|
}
|
||||||
|
}
|
||||||
68
sdk/rust/src/rpc/error.rs
Normal file
68
sdk/rust/src/rpc/error.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
//! Synor RPC SDK Error Types.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// RPC SDK error type.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RpcError {
|
||||||
|
/// HTTP request failed.
|
||||||
|
Request(String),
|
||||||
|
/// API returned an error.
|
||||||
|
Api {
|
||||||
|
message: String,
|
||||||
|
status_code: u16,
|
||||||
|
code: Option<String>,
|
||||||
|
},
|
||||||
|
/// JSON serialization/deserialization failed.
|
||||||
|
Serialization(String),
|
||||||
|
/// WebSocket connection error.
|
||||||
|
WebSocket(String),
|
||||||
|
/// Invalid input provided.
|
||||||
|
Validation(String),
|
||||||
|
/// Request timed out.
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RpcError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
RpcError::Request(msg) => write!(f, "Request error: {}", msg),
|
||||||
|
RpcError::Api {
|
||||||
|
message,
|
||||||
|
status_code,
|
||||||
|
code,
|
||||||
|
} => {
|
||||||
|
if let Some(c) = code {
|
||||||
|
write!(f, "API error [{}]: {} (status {})", c, message, status_code)
|
||||||
|
} else {
|
||||||
|
write!(f, "API error: {} (status {})", message, status_code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RpcError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
|
||||||
|
RpcError::WebSocket(msg) => write!(f, "WebSocket error: {}", msg),
|
||||||
|
RpcError::Validation(msg) => write!(f, "Validation error: {}", msg),
|
||||||
|
RpcError::Timeout => write!(f, "Request timed out"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for RpcError {}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for RpcError {
|
||||||
|
fn from(err: reqwest::Error) -> Self {
|
||||||
|
if err.is_timeout() {
|
||||||
|
RpcError::Timeout
|
||||||
|
} else {
|
||||||
|
RpcError::Request(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for RpcError {
|
||||||
|
fn from(err: serde_json::Error) -> Self {
|
||||||
|
RpcError::Serialization(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type alias for RPC operations.
|
||||||
|
pub type Result<T> = std::result::Result<T, RpcError>;
|
||||||
36
sdk/rust/src/rpc/mod.rs
Normal file
36
sdk/rust/src/rpc/mod.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
//! Synor RPC SDK
|
||||||
|
//!
|
||||||
|
//! Query blocks, transactions, and chain state with WebSocket subscription support.
|
||||||
|
//!
|
||||||
|
//! # Quick Start
|
||||||
|
//!
|
||||||
|
//! ```rust,no_run
|
||||||
|
//! use synor_compute::rpc::{SynorRpc, Priority};
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let client = SynorRpc::new("your-api-key");
|
||||||
|
//!
|
||||||
|
//! // Get latest block
|
||||||
|
//! let block = client.get_latest_block().await?;
|
||||||
|
//! println!("Height: {}", block.height);
|
||||||
|
//!
|
||||||
|
//! // Get transaction
|
||||||
|
//! let tx = client.get_transaction("txid...").await?;
|
||||||
|
//! println!("Status: {:?}", tx.status);
|
||||||
|
//!
|
||||||
|
//! // Estimate fee
|
||||||
|
//! let fee = client.estimate_fee(Priority::Medium).await?;
|
||||||
|
//! println!("Fee rate: {}", fee.fee_rate);
|
||||||
|
//!
|
||||||
|
//! Ok(())
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
mod error;
|
||||||
|
mod client;
|
||||||
|
|
||||||
|
pub use types::*;
|
||||||
|
pub use error::{RpcError, Result};
|
||||||
|
pub use client::SynorRpc;
|
||||||
244
sdk/rust/src/rpc/types.rs
Normal file
244
sdk/rust/src/rpc/types.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
//! Synor RPC SDK Types.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Network type.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Network {
|
||||||
|
#[default]
|
||||||
|
Mainnet,
|
||||||
|
Testnet,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction priority levels.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Priority {
|
||||||
|
Low,
|
||||||
|
#[default]
|
||||||
|
Medium,
|
||||||
|
High,
|
||||||
|
Urgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction status.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum TransactionStatus {
|
||||||
|
Pending,
|
||||||
|
Confirmed,
|
||||||
|
Failed,
|
||||||
|
Replaced,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscription type.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SubscriptionType {
|
||||||
|
Blocks,
|
||||||
|
Transactions,
|
||||||
|
Address,
|
||||||
|
Mempool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RPC client configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RpcConfig {
|
||||||
|
pub api_key: String,
|
||||||
|
pub endpoint: String,
|
||||||
|
pub ws_endpoint: String,
|
||||||
|
pub network: Network,
|
||||||
|
pub timeout_secs: u64,
|
||||||
|
pub debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcConfig {
|
||||||
|
/// Create a new configuration with the given API key.
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
api_key: api_key.into(),
|
||||||
|
endpoint: "https://rpc.synor.cc/api/v1".to_string(),
|
||||||
|
ws_endpoint: "wss://rpc.synor.cc/ws".to_string(),
|
||||||
|
network: Network::default(),
|
||||||
|
timeout_secs: 30,
|
||||||
|
debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the endpoint.
|
||||||
|
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
|
||||||
|
self.endpoint = endpoint.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the WebSocket endpoint.
|
||||||
|
pub fn ws_endpoint(mut self, ws_endpoint: impl Into<String>) -> Self {
|
||||||
|
self.ws_endpoint = ws_endpoint.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the network.
|
||||||
|
pub fn network(mut self, network: Network) -> Self {
|
||||||
|
self.network = network;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the timeout in seconds.
|
||||||
|
pub fn timeout(mut self, secs: u64) -> Self {
|
||||||
|
self.timeout_secs = secs;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable debug mode.
|
||||||
|
pub fn debug(mut self, debug: bool) -> Self {
|
||||||
|
self.debug = debug;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block header.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BlockHeader {
|
||||||
|
pub hash: String,
|
||||||
|
pub height: i64,
|
||||||
|
pub version: i32,
|
||||||
|
pub previous_hash: String,
|
||||||
|
pub merkle_root: String,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub difficulty: String,
|
||||||
|
pub nonce: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full block with transactions.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Block {
|
||||||
|
pub hash: String,
|
||||||
|
pub height: i64,
|
||||||
|
pub version: i32,
|
||||||
|
pub previous_hash: String,
|
||||||
|
pub merkle_root: String,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub difficulty: String,
|
||||||
|
pub nonce: u64,
|
||||||
|
pub transactions: Vec<String>,
|
||||||
|
pub size: i32,
|
||||||
|
pub weight: i32,
|
||||||
|
pub tx_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction input.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TxInput {
|
||||||
|
pub txid: String,
|
||||||
|
pub vout: i32,
|
||||||
|
pub script_sig: String,
|
||||||
|
pub sequence: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Script pubkey.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ScriptPubKey {
|
||||||
|
pub asm: String,
|
||||||
|
pub hex: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub script_type: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub addresses: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction output.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TxOutput {
|
||||||
|
pub value: String,
|
||||||
|
pub n: i32,
|
||||||
|
pub script_pub_key: ScriptPubKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Transaction {
|
||||||
|
pub txid: String,
|
||||||
|
pub confirmations: i32,
|
||||||
|
pub status: TransactionStatus,
|
||||||
|
pub size: i32,
|
||||||
|
pub fee: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub inputs: Vec<TxInput>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub outputs: Vec<TxOutput>,
|
||||||
|
pub block_hash: Option<String>,
|
||||||
|
pub block_height: Option<i64>,
|
||||||
|
pub timestamp: Option<i64>,
|
||||||
|
pub raw: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fee estimation result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FeeEstimate {
|
||||||
|
pub priority: Priority,
|
||||||
|
pub fee_rate: String,
|
||||||
|
pub estimated_blocks: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chain information.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChainInfo {
|
||||||
|
pub chain: String,
|
||||||
|
pub network: String,
|
||||||
|
pub height: i64,
|
||||||
|
pub best_block_hash: String,
|
||||||
|
pub difficulty: String,
|
||||||
|
pub median_time: i64,
|
||||||
|
pub chain_work: String,
|
||||||
|
pub syncing: bool,
|
||||||
|
pub sync_progress: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mempool information.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MempoolInfo {
|
||||||
|
pub size: i32,
|
||||||
|
pub bytes: i64,
|
||||||
|
pub usage: i64,
|
||||||
|
pub max_mempool: i64,
|
||||||
|
pub min_fee: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unspent transaction output.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Utxo {
|
||||||
|
pub txid: String,
|
||||||
|
pub vout: i32,
|
||||||
|
pub amount: String,
|
||||||
|
pub address: String,
|
||||||
|
pub confirmations: i32,
|
||||||
|
pub script_pub_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Balance information.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Balance {
|
||||||
|
pub confirmed: String,
|
||||||
|
pub unconfirmed: String,
|
||||||
|
pub total: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket subscription.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Subscription {
|
||||||
|
pub id: String,
|
||||||
|
pub subscription_type: SubscriptionType,
|
||||||
|
pub created_at: i64,
|
||||||
|
}
|
||||||
464
sdk/rust/src/storage/client.rs
Normal file
464
sdk/rust/src/storage/client.rs
Normal file
|
|
@ -0,0 +1,464 @@
|
||||||
|
//! Synor Storage SDK Client.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
||||||
|
use reqwest::{header, multipart, Client};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::error::{Result, StorageError};
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Synor Storage client.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SynorStorage {
|
||||||
|
config: StorageConfig,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ApiError {
|
||||||
|
message: String,
|
||||||
|
code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct FileData {
|
||||||
|
name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
content: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DirectoryResponse {
|
||||||
|
entries: Vec<DirectoryEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SynorStorage {
|
||||||
|
/// Create a new Storage client with the given API key.
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self::with_config(StorageConfig::new(api_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Storage client with custom configuration.
|
||||||
|
pub fn with_config(config: StorageConfig) -> Self {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(config.timeout_secs))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
Self { config, client }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload content to storage.
|
||||||
|
pub async fn upload(&self, data: &[u8], options: Option<UploadOptions>) -> Result<UploadResponse> {
|
||||||
|
let opts = options.unwrap_or_default();
|
||||||
|
|
||||||
|
let part = multipart::Part::bytes(data.to_vec()).file_name("file");
|
||||||
|
let form = multipart::Form::new().part("file", part);
|
||||||
|
|
||||||
|
let mut url = format!("{}/upload", self.config.endpoint);
|
||||||
|
let mut params = Vec::new();
|
||||||
|
|
||||||
|
if opts.pin {
|
||||||
|
params.push("pin=true".to_string());
|
||||||
|
}
|
||||||
|
if opts.wrap_with_directory {
|
||||||
|
params.push("wrapWithDirectory=true".to_string());
|
||||||
|
}
|
||||||
|
if opts.cid_version != 0 {
|
||||||
|
params.push(format!("cidVersion={}", opts.cid_version));
|
||||||
|
}
|
||||||
|
let algo = match opts.hash_algorithm {
|
||||||
|
HashAlgorithm::Sha256 => "sha2-256",
|
||||||
|
HashAlgorithm::Blake3 => "blake3",
|
||||||
|
};
|
||||||
|
params.push(format!("hashAlgorithm={}", algo));
|
||||||
|
|
||||||
|
if !params.is_empty() {
|
||||||
|
url = format!("{}?{}", url, params.join("&"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config.debug {
|
||||||
|
println!("[SynorStorage] POST /upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(&url)
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {}", self.config.api_key))
|
||||||
|
.multipart(form)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
let error: ApiError = response.json().await.unwrap_or(ApiError {
|
||||||
|
message: "Unknown error".to_string(),
|
||||||
|
code: None,
|
||||||
|
});
|
||||||
|
return Err(StorageError::Api {
|
||||||
|
message: error.message,
|
||||||
|
status_code: status.as_u16(),
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: UploadResponse = response.json().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download content by CID.
|
||||||
|
pub async fn download(&self, cid: &str, options: Option<DownloadOptions>) -> Result<Vec<u8>> {
|
||||||
|
let opts = options.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut url = format!("{}/content/{}", self.config.endpoint, cid);
|
||||||
|
let mut params = Vec::new();
|
||||||
|
|
||||||
|
if let Some(offset) = opts.offset {
|
||||||
|
params.push(format!("offset={}", offset));
|
||||||
|
}
|
||||||
|
if let Some(length) = opts.length {
|
||||||
|
params.push(format!("length={}", length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !params.is_empty() {
|
||||||
|
url = format!("{}?{}", url, params.join("&"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config.debug {
|
||||||
|
println!("[SynorStorage] GET /content/{}", cid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {}", self.config.api_key))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
let error: ApiError = response.json().await.unwrap_or(ApiError {
|
||||||
|
message: "Unknown error".to_string(),
|
||||||
|
code: None,
|
||||||
|
});
|
||||||
|
return Err(StorageError::Api {
|
||||||
|
message: error.message,
|
||||||
|
status_code: status.as_u16(),
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = response.bytes().await?;
|
||||||
|
Ok(bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pin content by CID.
|
||||||
|
pub async fn pin(&self, request: PinRequest) -> Result<Pin> {
|
||||||
|
self.request("POST", "/pins", Some(&request)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unpin content by CID.
|
||||||
|
pub async fn unpin(&self, cid: &str) -> Result<()> {
|
||||||
|
let path = format!("/pins/{}", cid);
|
||||||
|
self.request::<(), _>("DELETE", &path, Option::<&()>::None)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pin status by CID.
|
||||||
|
pub async fn get_pin_status(&self, cid: &str) -> Result<Pin> {
|
||||||
|
let path = format!("/pins/{}", cid);
|
||||||
|
self.request("GET", &path, Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List pins.
|
||||||
|
pub async fn list_pins(&self, options: Option<ListPinsOptions>) -> Result<ListPinsResponse> {
|
||||||
|
let mut path = "/pins".to_string();
|
||||||
|
let mut params = Vec::new();
|
||||||
|
|
||||||
|
if let Some(opts) = options {
|
||||||
|
if !opts.status.is_empty() {
|
||||||
|
let statuses: Vec<&str> = opts
|
||||||
|
.status
|
||||||
|
.iter()
|
||||||
|
.map(|s| match s {
|
||||||
|
PinStatus::Queued => "queued",
|
||||||
|
PinStatus::Pinning => "pinning",
|
||||||
|
PinStatus::Pinned => "pinned",
|
||||||
|
PinStatus::Failed => "failed",
|
||||||
|
PinStatus::Unpinned => "unpinned",
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
params.push(format!("status={}", statuses.join(",")));
|
||||||
|
}
|
||||||
|
if let Some(match_type) = opts.match_type {
|
||||||
|
let m = match match_type {
|
||||||
|
MatchType::Exact => "exact",
|
||||||
|
MatchType::IExact => "iexact",
|
||||||
|
MatchType::Partial => "partial",
|
||||||
|
MatchType::IPartial => "ipartial",
|
||||||
|
};
|
||||||
|
params.push(format!("match={}", m));
|
||||||
|
}
|
||||||
|
if let Some(name) = opts.name {
|
||||||
|
params.push(format!("name={}", name));
|
||||||
|
}
|
||||||
|
if let Some(limit) = opts.limit {
|
||||||
|
params.push(format!("limit={}", limit));
|
||||||
|
}
|
||||||
|
if let Some(offset) = opts.offset {
|
||||||
|
params.push(format!("offset={}", offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !params.is_empty() {
|
||||||
|
path = format!("{}?{}", path, params.join("&"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.request("GET", &path, Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get gateway URL for content.
|
||||||
|
pub fn get_gateway_url(&self, cid: &str, path: Option<&str>) -> GatewayUrl {
|
||||||
|
let full_path = match path {
|
||||||
|
Some(p) => format!("/{}/{}", cid, p),
|
||||||
|
None => format!("/{}", cid),
|
||||||
|
};
|
||||||
|
GatewayUrl {
|
||||||
|
url: format!("{}/ipfs{}", self.config.gateway, full_path),
|
||||||
|
cid: cid.to_string(),
|
||||||
|
path: path.map(|s| s.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a CAR file from files.
|
||||||
|
pub async fn create_car(&self, files: Vec<FileEntry>) -> Result<CarFile> {
|
||||||
|
let file_data: Vec<FileData> = files
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| FileData {
|
||||||
|
name: f.name,
|
||||||
|
content: f.content.map(|c| BASE64.encode(c)),
|
||||||
|
cid: f.cid,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CreateCarRequest {
|
||||||
|
files: Vec<FileData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.request("POST", "/car/create", Some(&CreateCarRequest { files: file_data }))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import a CAR file.
|
||||||
|
pub async fn import_car(&self, car_data: &[u8], pin: bool) -> Result<ImportCarResponse> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ImportCarRequest {
|
||||||
|
car: String,
|
||||||
|
pin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = ImportCarRequest {
|
||||||
|
car: BASE64.encode(car_data),
|
||||||
|
pin,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.request("POST", "/car/import", Some(&request)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export content as a CAR file.
|
||||||
|
pub async fn export_car(&self, cid: &str) -> Result<Vec<u8>> {
|
||||||
|
let url = format!("{}/car/export/{}", self.config.endpoint, cid);
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {}", self.config.api_key))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
let error: ApiError = response.json().await.unwrap_or(ApiError {
|
||||||
|
message: "Unknown error".to_string(),
|
||||||
|
code: None,
|
||||||
|
});
|
||||||
|
return Err(StorageError::Api {
|
||||||
|
message: error.message,
|
||||||
|
status_code: status.as_u16(),
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = response.bytes().await?;
|
||||||
|
Ok(bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a directory from files.
|
||||||
|
pub async fn create_directory(&self, files: Vec<FileEntry>) -> Result<UploadResponse> {
|
||||||
|
let file_data: Vec<FileData> = files
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| FileData {
|
||||||
|
name: f.name,
|
||||||
|
content: f.content.map(|c| BASE64.encode(c)),
|
||||||
|
cid: f.cid,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CreateDirRequest {
|
||||||
|
files: Vec<FileData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.request("POST", "/directory", Some(&CreateDirRequest { files: file_data }))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List directory contents.
|
||||||
|
pub async fn list_directory(&self, cid: &str, path: Option<&str>) -> Result<Vec<DirectoryEntry>> {
|
||||||
|
let mut api_path = format!("/directory/{}", cid);
|
||||||
|
if let Some(p) = path {
|
||||||
|
api_path = format!("{}?path={}", api_path, urlencoding::encode(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: DirectoryResponse = self.request("GET", &api_path, Option::<&()>::None).await?;
|
||||||
|
Ok(response.entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get storage statistics.
|
||||||
|
pub async fn get_stats(&self) -> Result<StorageStats> {
|
||||||
|
self.request("GET", "/stats", Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if content exists.
|
||||||
|
pub async fn exists(&self, cid: &str) -> Result<bool> {
|
||||||
|
let url = format!("{}/content/{}", self.config.endpoint, cid);
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.head(&url)
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {}", self.config.api_key))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Ok(resp) => Ok(resp.status().is_success()),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get content metadata.
|
||||||
|
pub async fn get_metadata(&self, cid: &str) -> Result<serde_json::Value> {
|
||||||
|
let path = format!("/content/{}/metadata", cid);
|
||||||
|
self.request("GET", &path, Option::<&()>::None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal HTTP request method.
|
||||||
|
async fn request<T, B>(&self, method: &str, path: &str, body: Option<&B>) -> Result<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
B: Serialize,
|
||||||
|
{
|
||||||
|
let url = format!("{}{}", self.config.endpoint, path);
|
||||||
|
|
||||||
|
if self.config.debug {
|
||||||
|
println!("[SynorStorage] {} {}", method, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut request = match method {
|
||||||
|
"GET" => self.client.get(&url),
|
||||||
|
"POST" => self.client.post(&url),
|
||||||
|
"PUT" => self.client.put(&url),
|
||||||
|
"DELETE" => self.client.delete(&url),
|
||||||
|
"HEAD" => self.client.head(&url),
|
||||||
|
_ => {
|
||||||
|
return Err(StorageError::Validation(format!(
|
||||||
|
"Invalid method: {}",
|
||||||
|
method
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request = request
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {}", self.config.api_key))
|
||||||
|
.header(header::CONTENT_TYPE, "application/json");
|
||||||
|
|
||||||
|
if let Some(b) = body {
|
||||||
|
request = request.json(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request.send().await?;
|
||||||
|
let status = response.status();
|
||||||
|
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
let error: ApiError = response.json().await.unwrap_or(ApiError {
|
||||||
|
message: "Unknown error".to_string(),
|
||||||
|
code: None,
|
||||||
|
});
|
||||||
|
return Err(StorageError::Api {
|
||||||
|
message: error.message,
|
||||||
|
status_code: status.as_u16(),
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == reqwest::StatusCode::NO_CONTENT {
|
||||||
|
// For DELETE operations that return 204
|
||||||
|
return serde_json::from_str("{}").map_err(StorageError::from);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: T = response.json().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_builder() {
|
||||||
|
let config = StorageConfig::new("test-key")
|
||||||
|
.endpoint("https://custom.endpoint")
|
||||||
|
.gateway("https://custom.gateway")
|
||||||
|
.chunk_size(512000)
|
||||||
|
.timeout(60)
|
||||||
|
.debug(true);
|
||||||
|
|
||||||
|
assert_eq!(config.api_key, "test-key");
|
||||||
|
assert_eq!(config.endpoint, "https://custom.endpoint");
|
||||||
|
assert_eq!(config.gateway, "https://custom.gateway");
|
||||||
|
assert_eq!(config.chunk_size, 512000);
|
||||||
|
assert_eq!(config.timeout_secs, 60);
|
||||||
|
assert!(config.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_creation() {
|
||||||
|
let client = SynorStorage::new("test-key");
|
||||||
|
assert_eq!(client.config.api_key, "test-key");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gateway_url() {
|
||||||
|
let client = SynorStorage::new("test-key");
|
||||||
|
|
||||||
|
let url = client.get_gateway_url("QmTest123", None);
|
||||||
|
assert_eq!(url.url, "https://gateway.synor.cc/ipfs/QmTest123");
|
||||||
|
assert_eq!(url.cid, "QmTest123");
|
||||||
|
assert!(url.path.is_none());
|
||||||
|
|
||||||
|
let url_with_path = client.get_gateway_url("QmTest123", Some("subdir/file.txt"));
|
||||||
|
assert_eq!(
|
||||||
|
url_with_path.url,
|
||||||
|
"https://gateway.synor.cc/ipfs/QmTest123/subdir/file.txt"
|
||||||
|
);
|
||||||
|
assert_eq!(url_with_path.path, Some("subdir/file.txt".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
74
sdk/rust/src/storage/error.rs
Normal file
74
sdk/rust/src/storage/error.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
//! Synor Storage SDK Error Types.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Storage SDK error type.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StorageError {
|
||||||
|
/// HTTP request failed.
|
||||||
|
Request(String),
|
||||||
|
/// API returned an error.
|
||||||
|
Api {
|
||||||
|
message: String,
|
||||||
|
status_code: u16,
|
||||||
|
code: Option<String>,
|
||||||
|
},
|
||||||
|
/// JSON serialization/deserialization failed.
|
||||||
|
Serialization(String),
|
||||||
|
/// Invalid input provided.
|
||||||
|
Validation(String),
|
||||||
|
/// Request timed out.
|
||||||
|
Timeout,
|
||||||
|
/// IO error.
|
||||||
|
Io(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for StorageError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
StorageError::Request(msg) => write!(f, "Request error: {}", msg),
|
||||||
|
StorageError::Api {
|
||||||
|
message,
|
||||||
|
status_code,
|
||||||
|
code,
|
||||||
|
} => {
|
||||||
|
if let Some(c) = code {
|
||||||
|
write!(f, "API error [{}]: {} (status {})", c, message, status_code)
|
||||||
|
} else {
|
||||||
|
write!(f, "API error: {} (status {})", message, status_code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StorageError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
|
||||||
|
StorageError::Validation(msg) => write!(f, "Validation error: {}", msg),
|
||||||
|
StorageError::Timeout => write!(f, "Request timed out"),
|
||||||
|
StorageError::Io(msg) => write!(f, "IO error: {}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for StorageError {}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for StorageError {
|
||||||
|
fn from(err: reqwest::Error) -> Self {
|
||||||
|
if err.is_timeout() {
|
||||||
|
StorageError::Timeout
|
||||||
|
} else {
|
||||||
|
StorageError::Request(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for StorageError {
|
||||||
|
fn from(err: serde_json::Error) -> Self {
|
||||||
|
StorageError::Serialization(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for StorageError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
StorageError::Io(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type alias for Storage operations.
|
||||||
|
pub type Result<T> = std::result::Result<T, StorageError>;
|
||||||
40
sdk/rust/src/storage/mod.rs
Normal file
40
sdk/rust/src/storage/mod.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
//! Synor Storage SDK
|
||||||
|
//!
|
||||||
|
//! Decentralized storage, pinning, and content retrieval on the Synor network.
|
||||||
|
//!
|
||||||
|
//! # Quick Start
|
||||||
|
//!
|
||||||
|
//! ```rust,no_run
|
||||||
|
//! use synor_compute::storage::{SynorStorage, UploadOptions, PinRequest};
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let client = SynorStorage::new("your-api-key");
|
||||||
|
//!
|
||||||
|
//! // Upload content
|
||||||
|
//! let result = client.upload(b"Hello, World!", None).await?;
|
||||||
|
//! println!("CID: {}", result.cid);
|
||||||
|
//!
|
||||||
|
//! // Pin content
|
||||||
|
//! let pin = client.pin(PinRequest::new(&result.cid).name("my-file")).await?;
|
||||||
|
//! println!("Pin status: {:?}", pin.status);
|
||||||
|
//!
|
||||||
|
//! // Get gateway URL
|
||||||
|
//! let gateway = client.get_gateway_url(&result.cid, None);
|
||||||
|
//! println!("URL: {}", gateway.url);
|
||||||
|
//!
|
||||||
|
//! // Download content
|
||||||
|
//! let data = client.download(&result.cid, None).await?;
|
||||||
|
//! println!("Content: {}", String::from_utf8_lossy(&data));
|
||||||
|
//!
|
||||||
|
//! Ok(())
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
mod error;
|
||||||
|
mod client;
|
||||||
|
|
||||||
|
pub use types::*;
|
||||||
|
pub use error::{StorageError, Result};
|
||||||
|
pub use client::SynorStorage;
|
||||||
356
sdk/rust/src/storage/types.rs
Normal file
356
sdk/rust/src/storage/types.rs
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
//! Synor Storage SDK Types.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Pin status.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum PinStatus {
|
||||||
|
Queued,
|
||||||
|
Pinning,
|
||||||
|
Pinned,
|
||||||
|
Failed,
|
||||||
|
Unpinned,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash algorithm.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
pub enum HashAlgorithm {
|
||||||
|
#[default]
|
||||||
|
#[serde(rename = "sha2-256")]
|
||||||
|
Sha256,
|
||||||
|
#[serde(rename = "blake3")]
|
||||||
|
Blake3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entry type.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum EntryType {
|
||||||
|
File,
|
||||||
|
Directory,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match type for listing pins.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum MatchType {
|
||||||
|
Exact,
|
||||||
|
IExact,
|
||||||
|
Partial,
|
||||||
|
IPartial,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Storage SDK configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StorageConfig {
|
||||||
|
pub api_key: String,
|
||||||
|
pub endpoint: String,
|
||||||
|
pub gateway: String,
|
||||||
|
pub pinning_service: Option<String>,
|
||||||
|
pub chunk_size: usize,
|
||||||
|
pub timeout_secs: u64,
|
||||||
|
pub debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageConfig {
|
||||||
|
/// Create a new configuration with the given API key.
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
api_key: api_key.into(),
|
||||||
|
endpoint: "https://storage.synor.cc/api/v1".to_string(),
|
||||||
|
gateway: "https://gateway.synor.cc".to_string(),
|
||||||
|
pinning_service: None,
|
||||||
|
chunk_size: 262144, // 256KB
|
||||||
|
timeout_secs: 30,
|
||||||
|
debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the endpoint.
|
||||||
|
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
|
||||||
|
self.endpoint = endpoint.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the gateway.
|
||||||
|
pub fn gateway(mut self, gateway: impl Into<String>) -> Self {
|
||||||
|
self.gateway = gateway.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the pinning service.
|
||||||
|
pub fn pinning_service(mut self, service: impl Into<String>) -> Self {
|
||||||
|
self.pinning_service = Some(service.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the chunk size.
|
||||||
|
pub fn chunk_size(mut self, size: usize) -> Self {
|
||||||
|
self.chunk_size = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the timeout in seconds.
|
||||||
|
pub fn timeout(mut self, secs: u64) -> Self {
|
||||||
|
self.timeout_secs = secs;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable debug mode.
|
||||||
|
pub fn debug(mut self, debug: bool) -> Self {
|
||||||
|
self.debug = debug;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload options.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct UploadOptions {
|
||||||
|
pub pin: bool,
|
||||||
|
pub wrap_with_directory: bool,
|
||||||
|
pub cid_version: u8,
|
||||||
|
pub hash_algorithm: HashAlgorithm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UploadOptions {
|
||||||
|
/// Create new upload options with pinning enabled.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pin: true,
|
||||||
|
cid_version: 1,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set whether to pin content.
|
||||||
|
pub fn pin(mut self, pin: bool) -> Self {
|
||||||
|
self.pin = pin;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set whether to wrap with directory.
|
||||||
|
pub fn wrap_with_directory(mut self, wrap: bool) -> Self {
|
||||||
|
self.wrap_with_directory = wrap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the CID version.
|
||||||
|
pub fn cid_version(mut self, version: u8) -> Self {
|
||||||
|
self.cid_version = version;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the hash algorithm.
|
||||||
|
pub fn hash_algorithm(mut self, algorithm: HashAlgorithm) -> Self {
|
||||||
|
self.hash_algorithm = algorithm;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload response.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UploadResponse {
|
||||||
|
pub cid: String,
|
||||||
|
pub size: i64,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub hash: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download options.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct DownloadOptions {
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
pub length: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DownloadOptions {
|
||||||
|
/// Create new download options.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the offset.
|
||||||
|
pub fn offset(mut self, offset: i64) -> Self {
|
||||||
|
self.offset = Some(offset);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the length.
|
||||||
|
pub fn length(mut self, length: i64) -> Self {
|
||||||
|
self.length = Some(length);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pin information.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Pin {
|
||||||
|
pub cid: String,
|
||||||
|
pub status: PinStatus,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub size: Option<i64>,
|
||||||
|
pub created_at: Option<i64>,
|
||||||
|
pub expires_at: Option<i64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub delegates: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pin request.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PinRequest {
|
||||||
|
pub cid: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub duration: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub origins: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PinRequest {
|
||||||
|
/// Create a new pin request.
|
||||||
|
pub fn new(cid: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
cid: cid.into(),
|
||||||
|
name: None,
|
||||||
|
duration: None,
|
||||||
|
origins: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the name.
|
||||||
|
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||||
|
self.name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the duration.
|
||||||
|
pub fn duration(mut self, duration: i64) -> Self {
|
||||||
|
self.duration = Some(duration);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the origins.
|
||||||
|
pub fn origins(mut self, origins: Vec<String>) -> Self {
|
||||||
|
self.origins = origins;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List pins options.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ListPinsOptions {
|
||||||
|
pub status: Vec<PinStatus>,
|
||||||
|
pub match_type: Option<MatchType>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub limit: Option<i32>,
|
||||||
|
pub offset: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List pins response.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ListPinsResponse {
|
||||||
|
pub pins: Vec<Pin>,
|
||||||
|
pub total: i32,
|
||||||
|
pub has_more: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gateway URL.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GatewayUrl {
|
||||||
|
pub url: String,
|
||||||
|
pub cid: String,
|
||||||
|
pub path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CAR block.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CarBlock {
|
||||||
|
pub cid: String,
|
||||||
|
pub data: String, // Base64-encoded
|
||||||
|
pub size: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CAR file.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CarFile {
|
||||||
|
pub version: i32,
|
||||||
|
pub roots: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub blocks: Vec<CarBlock>,
|
||||||
|
pub size: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File entry for directory creation.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileEntry {
|
||||||
|
pub name: String,
|
||||||
|
pub content: Option<Vec<u8>>,
|
||||||
|
pub cid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileEntry {
|
||||||
|
/// Create a new file entry.
|
||||||
|
pub fn new(name: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
content: None,
|
||||||
|
cid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the content.
|
||||||
|
pub fn content(mut self, content: Vec<u8>) -> Self {
|
||||||
|
self.content = Some(content);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the CID.
|
||||||
|
pub fn cid(mut self, cid: impl Into<String>) -> Self {
|
||||||
|
self.cid = Some(cid.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Directory entry.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DirectoryEntry {
|
||||||
|
pub name: String,
|
||||||
|
pub cid: String,
|
||||||
|
pub size: Option<i64>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub entry_type: EntryType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import CAR response.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ImportCarResponse {
|
||||||
|
pub roots: Vec<String>,
|
||||||
|
pub blocks_imported: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Storage statistics.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct StorageStats {
|
||||||
|
pub total_size: i64,
|
||||||
|
pub pin_count: i32,
|
||||||
|
pub bandwidth: Option<BandwidthStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bandwidth statistics.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BandwidthStats {
|
||||||
|
pub upload: i64,
|
||||||
|
pub download: i64,
|
||||||
|
}
|
||||||
|
|
@ -22,10 +22,11 @@ use std::f64::consts::PI;
|
||||||
/// let mean = random.mean();
|
/// let mean = random.mean();
|
||||||
/// let transposed = matrix.transpose();
|
/// let transposed = matrix.transpose();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||||
pub struct Tensor {
|
pub struct Tensor {
|
||||||
shape: Vec<usize>,
|
shape: Vec<usize>,
|
||||||
data: Vec<f64>,
|
data: Vec<f64>,
|
||||||
|
#[serde(default)]
|
||||||
dtype: Precision,
|
dtype: Precision,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ impl Default for AttentionOptions {
|
||||||
num_heads: 8,
|
num_heads: 8,
|
||||||
flash: true,
|
flash: true,
|
||||||
precision: Precision::FP16,
|
precision: Precision::FP16,
|
||||||
processor: ProcessorType::GPU,
|
processor: ProcessorType::Gpu,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
379
sdk/rust/src/wallet/client.rs
Normal file
379
sdk/rust/src/wallet/client.rs
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
//! Synor Wallet client implementation.
|
||||||
|
|
||||||
|
use crate::wallet::error::{Result, WalletError};
|
||||||
|
use crate::wallet::types::*;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Synor Wallet SDK client.
|
||||||
|
pub struct SynorWallet {
|
||||||
|
config: WalletConfig,
|
||||||
|
client: Client,
|
||||||
|
closed: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SynorWallet {
|
||||||
|
/// Create a new client with an API key.
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self::with_config(WalletConfig::new(api_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new client with configuration.
|
||||||
|
pub fn with_config(config: WalletConfig) -> Self {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(config.timeout_secs))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
client,
|
||||||
|
closed: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Wallet Management ====================
|
||||||
|
|
||||||
|
/// Create a new wallet.
|
||||||
|
pub async fn create_wallet(&self, wallet_type: WalletType) -> Result<CreateWalletResult> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let mut body = json!({
|
||||||
|
"type": wallet_type,
|
||||||
|
"network": self.config.network,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref path) = self.config.derivation_path {
|
||||||
|
body["derivationPath"] = json!(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.post("/wallets", body).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import a wallet from mnemonic phrase.
|
||||||
|
pub async fn import_wallet(&self, options: ImportWalletOptions) -> Result<Wallet> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let mut body = json!({
|
||||||
|
"mnemonic": options.mnemonic,
|
||||||
|
"type": options.wallet_type,
|
||||||
|
"network": self.config.network,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(passphrase) = options.passphrase {
|
||||||
|
body["passphrase"] = json!(passphrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref path) = self.config.derivation_path {
|
||||||
|
body["derivationPath"] = json!(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp: Value = self.post("/wallets/import", body).await?;
|
||||||
|
Ok(serde_json::from_value(resp["wallet"].clone())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a wallet by ID.
|
||||||
|
pub async fn get_wallet(&self, wallet_id: &str) -> Result<Wallet> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let resp: Value = self.get(&format!("/wallets/{}", wallet_id)).await?;
|
||||||
|
Ok(serde_json::from_value(resp["wallet"].clone())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all wallets for this account.
|
||||||
|
pub async fn list_wallets(&self) -> Result<Vec<Wallet>> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let resp: Value = self.get("/wallets").await?;
|
||||||
|
let wallets = resp["wallets"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap_or(&vec![])
|
||||||
|
.iter()
|
||||||
|
.filter_map(|w| serde_json::from_value(w.clone()).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(wallets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get address at a specific index for a wallet.
|
||||||
|
pub async fn get_address(&self, wallet_id: &str, index: u32) -> Result<String> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let resp: Value = self
|
||||||
|
.get(&format!("/wallets/{}/addresses/{}", wallet_id, index))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(resp["address"].as_str().unwrap_or_default().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a stealth address for receiving private payments.
|
||||||
|
pub async fn get_stealth_address(&self, wallet_id: &str) -> Result<StealthAddress> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let resp: Value = self
|
||||||
|
.post(&format!("/wallets/{}/stealth", wallet_id), json!({}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_value(resp["stealthAddress"].clone())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Transaction Operations ====================
|
||||||
|
|
||||||
|
/// Sign a transaction.
|
||||||
|
pub async fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
wallet_id: &str,
|
||||||
|
transaction: &Transaction,
|
||||||
|
) -> Result<SignedTransaction> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let body = json!({
|
||||||
|
"transaction": transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp: Value = self
|
||||||
|
.post(&format!("/wallets/{}/sign", wallet_id), body)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_value(resp["signedTransaction"].clone())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign a message.
|
||||||
|
pub async fn sign_message(
|
||||||
|
&self,
|
||||||
|
wallet_id: &str,
|
||||||
|
message: &str,
|
||||||
|
format: Option<&str>,
|
||||||
|
) -> Result<SignedMessage> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let body = json!({
|
||||||
|
"message": message,
|
||||||
|
"format": format.unwrap_or("text"),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.post(&format!("/wallets/{}/sign-message", wallet_id), body)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a signed message.
|
||||||
|
pub async fn verify_message(
|
||||||
|
&self,
|
||||||
|
message: &str,
|
||||||
|
signature: &str,
|
||||||
|
address: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let body = json!({
|
||||||
|
"message": message,
|
||||||
|
"signature": signature,
|
||||||
|
"address": address,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp: Value = self.post("/verify-message", body).await?;
|
||||||
|
Ok(resp["valid"].as_bool().unwrap_or(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Balance & UTXOs ====================
|
||||||
|
|
||||||
|
/// Get balance for an address.
|
||||||
|
pub async fn get_balance(&self, address: &str, include_tokens: bool) -> Result<BalanceResponse> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let url = format!("/balances/{}?includeTokens={}", address, include_tokens);
|
||||||
|
self.get(&url).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get UTXOs for an address.
|
||||||
|
pub async fn get_utxos(&self, address: &str, options: Option<GetUtxosOptions>) -> Result<Vec<Utxo>> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let mut url = format!("/utxos/{}", address);
|
||||||
|
|
||||||
|
if let Some(opts) = options {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
|
||||||
|
if let Some(min_conf) = opts.min_confirmations {
|
||||||
|
params.push(format!("minConfirmations={}", min_conf));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(min_amount) = opts.min_amount {
|
||||||
|
params.push(format!("minAmount={}", min_amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !params.is_empty() {
|
||||||
|
url.push('?');
|
||||||
|
url.push_str(¶ms.join("&"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp: Value = self.get(&url).await?;
|
||||||
|
let utxos = resp["utxos"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap_or(&vec![])
|
||||||
|
.iter()
|
||||||
|
.filter_map(|u| serde_json::from_value(u.clone()).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(utxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Transaction Building ====================
|
||||||
|
|
||||||
|
/// Build a transaction without signing.
|
||||||
|
pub async fn build_transaction(
|
||||||
|
&self,
|
||||||
|
wallet_id: &str,
|
||||||
|
options: BuildTransactionOptions,
|
||||||
|
) -> Result<Transaction> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let mut body = json!({
|
||||||
|
"to": options.to,
|
||||||
|
"amount": options.amount,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(fee_rate) = options.fee_rate {
|
||||||
|
body["feeRate"] = json!(fee_rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(utxos) = options.utxos {
|
||||||
|
body["utxos"] = json!(utxos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(change_address) = options.change_address {
|
||||||
|
body["changeAddress"] = json!(change_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp: Value = self
|
||||||
|
.post(&format!("/wallets/{}/build-tx", wallet_id), body)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_value(resp["transaction"].clone())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build and sign a transaction in one step.
|
||||||
|
pub async fn send_transaction(
|
||||||
|
&self,
|
||||||
|
wallet_id: &str,
|
||||||
|
options: BuildTransactionOptions,
|
||||||
|
) -> Result<SignedTransaction> {
|
||||||
|
let tx = self.build_transaction(wallet_id, options).await?;
|
||||||
|
self.sign_transaction(wallet_id, &tx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Fee Estimation ====================
|
||||||
|
|
||||||
|
/// Estimate transaction fee.
|
||||||
|
pub async fn estimate_fee(&self, priority: Priority) -> Result<FeeEstimate> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let url = format!("/fees/estimate?priority={:?}", priority).to_lowercase();
|
||||||
|
self.get(&url).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all fee estimates.
|
||||||
|
pub async fn get_all_fee_estimates(&self) -> Result<Vec<FeeEstimate>> {
|
||||||
|
self.check_closed()?;
|
||||||
|
|
||||||
|
let resp: Value = self.get("/fees/estimate/all").await?;
|
||||||
|
let estimates = resp["estimates"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap_or(&vec![])
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| serde_json::from_value(e.clone()).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(estimates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
/// Close the client.
|
||||||
|
pub fn close(&self) {
|
||||||
|
self.closed.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the client is closed.
|
||||||
|
pub fn is_closed(&self) -> bool {
|
||||||
|
self.closed.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Internal Methods ====================
|
||||||
|
|
||||||
|
fn check_closed(&self) -> Result<()> {
|
||||||
|
if self.is_closed() {
|
||||||
|
Err(WalletError::ClientClosed)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
|
||||||
|
let url = format!("{}{}", self.config.endpoint, path);
|
||||||
|
|
||||||
|
if self.config.debug {
|
||||||
|
println!("[SynorWallet] GET {}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("X-Network", format!("{:?}", self.config.network).to_lowercase())
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status_code = response.status().as_u16();
|
||||||
|
let body: Value = response.json().await.unwrap_or_default();
|
||||||
|
|
||||||
|
return Err(WalletError::Api {
|
||||||
|
status_code,
|
||||||
|
message: body["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("Request failed")
|
||||||
|
.to_string(),
|
||||||
|
code: body["code"].as_str().map(String::from),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.json().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post<T: serde::de::DeserializeOwned>(&self, path: &str, body: Value) -> Result<T> {
|
||||||
|
let url = format!("{}{}", self.config.endpoint, path);
|
||||||
|
|
||||||
|
if self.config.debug {
|
||||||
|
println!("[SynorWallet] POST {}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(&url)
|
||||||
|
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||||
|
.header("X-Network", format!("{:?}", self.config.network).to_lowercase())
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status_code = response.status().as_u16();
|
||||||
|
let body: Value = response.json().await.unwrap_or_default();
|
||||||
|
|
||||||
|
return Err(WalletError::Api {
|
||||||
|
status_code,
|
||||||
|
message: body["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("Request failed")
|
||||||
|
.to_string(),
|
||||||
|
code: body["code"].as_str().map(String::from),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.json().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
72
sdk/rust/src/wallet/error.rs
Normal file
72
sdk/rust/src/wallet/error.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
//! Synor Wallet SDK error types.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Result type for wallet operations.
|
||||||
|
pub type Result<T> = std::result::Result<T, WalletError>;
|
||||||
|
|
||||||
|
/// Wallet SDK error.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum WalletError {
|
||||||
|
/// HTTP request error.
|
||||||
|
Http(reqwest::Error),
|
||||||
|
/// API error response.
|
||||||
|
Api {
|
||||||
|
/// HTTP status code.
|
||||||
|
status_code: u16,
|
||||||
|
/// Error message.
|
||||||
|
message: String,
|
||||||
|
/// Error code.
|
||||||
|
code: Option<String>,
|
||||||
|
},
|
||||||
|
/// Client is closed.
|
||||||
|
ClientClosed,
|
||||||
|
/// Invalid configuration.
|
||||||
|
InvalidConfig(String),
|
||||||
|
/// Serialization error.
|
||||||
|
Serialization(serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for WalletError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
WalletError::Http(e) => write!(f, "HTTP error: {}", e),
|
||||||
|
WalletError::Api {
|
||||||
|
status_code,
|
||||||
|
message,
|
||||||
|
code,
|
||||||
|
} => {
|
||||||
|
if let Some(code) = code {
|
||||||
|
write!(f, "API error [{}]: {} (status {})", code, message, status_code)
|
||||||
|
} else {
|
||||||
|
write!(f, "API error: {} (status {})", message, status_code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WalletError::ClientClosed => write!(f, "Client is closed"),
|
||||||
|
WalletError::InvalidConfig(msg) => write!(f, "Invalid configuration: {}", msg),
|
||||||
|
WalletError::Serialization(e) => write!(f, "Serialization error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for WalletError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
WalletError::Http(e) => Some(e),
|
||||||
|
WalletError::Serialization(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for WalletError {
|
||||||
|
fn from(error: reqwest::Error) -> Self {
|
||||||
|
WalletError::Http(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for WalletError {
|
||||||
|
fn from(error: serde_json::Error) -> Self {
|
||||||
|
WalletError::Serialization(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
34
sdk/rust/src/wallet/mod.rs
Normal file
34
sdk/rust/src/wallet/mod.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
//! Synor Wallet SDK
|
||||||
|
//!
|
||||||
|
//! A Rust SDK for wallet management, key operations, and transaction signing
|
||||||
|
//! on the Synor blockchain.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use synor::wallet::{SynorWallet, WalletType};
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let client = SynorWallet::new("your-api-key");
|
||||||
|
//!
|
||||||
|
//! // Create a new wallet
|
||||||
|
//! let result = client.create_wallet(WalletType::Standard).await?;
|
||||||
|
//! println!("Address: {}", result.wallet.address);
|
||||||
|
//! println!("Mnemonic: {}", result.mnemonic); // Store securely!
|
||||||
|
//!
|
||||||
|
//! // Get balance
|
||||||
|
//! let balance = client.get_balance(&result.wallet.address, false).await?;
|
||||||
|
//! println!("Balance: {}", balance.native.total);
|
||||||
|
//!
|
||||||
|
//! Ok(())
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod client;
|
||||||
|
mod error;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
pub use client::SynorWallet;
|
||||||
|
pub use error::{Result, WalletError};
|
||||||
|
pub use types::*;
|
||||||
386
sdk/rust/src/wallet/types.rs
Normal file
386
sdk/rust/src/wallet/types.rs
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
//! Synor Wallet SDK types.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Network type.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Network {
|
||||||
|
Mainnet,
|
||||||
|
Testnet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Network {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Mainnet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wallet type.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum WalletType {
|
||||||
|
Standard,
|
||||||
|
Multisig,
|
||||||
|
Stealth,
|
||||||
|
Hardware,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WalletType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Standard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction priority.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Priority {
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High,
|
||||||
|
Urgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Priority {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wallet SDK configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WalletConfig {
|
||||||
|
/// API key for authentication.
|
||||||
|
pub api_key: String,
|
||||||
|
/// API endpoint.
|
||||||
|
pub endpoint: String,
|
||||||
|
/// Network type.
|
||||||
|
pub network: Network,
|
||||||
|
/// Request timeout in seconds.
|
||||||
|
pub timeout_secs: u64,
|
||||||
|
/// Enable debug logging.
|
||||||
|
pub debug: bool,
|
||||||
|
/// BIP44 derivation path.
|
||||||
|
pub derivation_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletConfig {
|
||||||
|
/// Create a new configuration with an API key.
|
||||||
|
pub fn new(api_key: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
api_key: api_key.into(),
|
||||||
|
endpoint: "https://wallet.synor.cc/api/v1".to_string(),
|
||||||
|
network: Network::default(),
|
||||||
|
timeout_secs: 30,
|
||||||
|
debug: false,
|
||||||
|
derivation_path: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the API endpoint.
|
||||||
|
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
|
||||||
|
self.endpoint = endpoint.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the network.
|
||||||
|
pub fn network(mut self, network: Network) -> Self {
|
||||||
|
self.network = network;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the timeout in seconds.
|
||||||
|
pub fn timeout(mut self, secs: u64) -> Self {
|
||||||
|
self.timeout_secs = secs;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable debug mode.
|
||||||
|
pub fn debug(mut self, debug: bool) -> Self {
|
||||||
|
self.debug = debug;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the derivation path.
|
||||||
|
pub fn derivation_path(mut self, path: impl Into<String>) -> Self {
|
||||||
|
self.derivation_path = Some(path.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wallet instance.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Wallet {
|
||||||
|
/// Unique wallet ID.
|
||||||
|
pub id: String,
|
||||||
|
/// Primary address.
|
||||||
|
pub address: String,
|
||||||
|
/// Compressed public key (hex).
|
||||||
|
pub public_key: String,
|
||||||
|
/// Wallet type.
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub wallet_type: WalletType,
|
||||||
|
/// Creation timestamp.
|
||||||
|
pub created_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wallet creation result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateWalletResult {
|
||||||
|
/// Created wallet.
|
||||||
|
pub wallet: Wallet,
|
||||||
|
/// BIP39 mnemonic phrase (24 words).
|
||||||
|
pub mnemonic: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stealth address for private payments.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct StealthAddress {
|
||||||
|
/// One-time address.
|
||||||
|
pub address: String,
|
||||||
|
/// View public key.
|
||||||
|
pub view_key: String,
|
||||||
|
/// Spend public key.
|
||||||
|
pub spend_key: String,
|
||||||
|
/// Ephemeral public key.
|
||||||
|
pub ephemeral_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction input.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TransactionInput {
|
||||||
|
/// Previous transaction hash.
|
||||||
|
pub txid: String,
|
||||||
|
/// Output index.
|
||||||
|
pub vout: u32,
|
||||||
|
/// Amount.
|
||||||
|
pub amount: String,
|
||||||
|
/// Script signature.
|
||||||
|
pub script_sig: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction output.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TransactionOutput {
|
||||||
|
/// Recipient address.
|
||||||
|
pub address: String,
|
||||||
|
/// Amount.
|
||||||
|
pub amount: String,
|
||||||
|
/// Script pubkey.
|
||||||
|
pub script_pub_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unsigned transaction.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Transaction {
|
||||||
|
/// Version.
|
||||||
|
pub version: u32,
|
||||||
|
/// Inputs.
|
||||||
|
pub inputs: Vec<TransactionInput>,
|
||||||
|
/// Outputs.
|
||||||
|
pub outputs: Vec<TransactionOutput>,
|
||||||
|
/// Lock time.
|
||||||
|
#[serde(default)]
|
||||||
|
pub lock_time: u32,
|
||||||
|
/// Fee.
|
||||||
|
pub fee: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signed transaction.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SignedTransaction {
|
||||||
|
/// Raw hex-encoded transaction.
|
||||||
|
pub raw: String,
|
||||||
|
/// Transaction hash.
|
||||||
|
pub txid: String,
|
||||||
|
/// Transaction size in bytes.
|
||||||
|
pub size: u32,
|
||||||
|
/// Transaction weight.
|
||||||
|
pub weight: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signed message.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SignedMessage {
|
||||||
|
/// Signature (hex).
|
||||||
|
pub signature: String,
|
||||||
|
/// Public key used for signing.
|
||||||
|
pub public_key: String,
|
||||||
|
/// Address that signed.
|
||||||
|
pub address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unspent Transaction Output.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Utxo {
|
||||||
|
/// Transaction hash.
|
||||||
|
pub txid: String,
|
||||||
|
/// Output index.
|
||||||
|
pub vout: u32,
|
||||||
|
/// Amount.
|
||||||
|
pub amount: String,
|
||||||
|
/// Address.
|
||||||
|
pub address: String,
|
||||||
|
/// Number of confirmations.
|
||||||
|
pub confirmations: u32,
|
||||||
|
/// Script pubkey.
|
||||||
|
pub script_pub_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Balance information.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Balance {
|
||||||
|
/// Confirmed balance.
|
||||||
|
pub confirmed: String,
|
||||||
|
/// Unconfirmed balance.
|
||||||
|
pub unconfirmed: String,
|
||||||
|
/// Total balance.
|
||||||
|
pub total: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Token balance.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TokenBalance {
|
||||||
|
/// Token contract address.
|
||||||
|
pub token: String,
|
||||||
|
/// Token symbol.
|
||||||
|
pub symbol: String,
|
||||||
|
/// Token decimals.
|
||||||
|
pub decimals: u8,
|
||||||
|
/// Balance.
|
||||||
|
pub balance: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full balance response.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BalanceResponse {
|
||||||
|
/// Native token balance.
|
||||||
|
pub native: Balance,
|
||||||
|
/// Token balances.
|
||||||
|
#[serde(default)]
|
||||||
|
pub tokens: Vec<TokenBalance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fee estimation result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FeeEstimate {
|
||||||
|
/// Priority level.
|
||||||
|
pub priority: Priority,
|
||||||
|
/// Fee rate.
|
||||||
|
pub fee_rate: String,
|
||||||
|
/// Estimated blocks until confirmation.
|
||||||
|
pub estimated_blocks: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for importing a wallet.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ImportWalletOptions {
|
||||||
|
/// BIP39 mnemonic phrase.
|
||||||
|
pub mnemonic: String,
|
||||||
|
/// Optional passphrase.
|
||||||
|
pub passphrase: Option<String>,
|
||||||
|
/// Wallet type.
|
||||||
|
pub wallet_type: WalletType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportWalletOptions {
|
||||||
|
/// Create new import options with a mnemonic.
|
||||||
|
pub fn new(mnemonic: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
mnemonic: mnemonic.into(),
|
||||||
|
passphrase: None,
|
||||||
|
wallet_type: WalletType::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the passphrase.
|
||||||
|
pub fn passphrase(mut self, passphrase: impl Into<String>) -> Self {
|
||||||
|
self.passphrase = Some(passphrase.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the wallet type.
|
||||||
|
pub fn wallet_type(mut self, wallet_type: WalletType) -> Self {
|
||||||
|
self.wallet_type = wallet_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for building a transaction.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct BuildTransactionOptions {
|
||||||
|
/// Recipient address.
|
||||||
|
pub to: String,
|
||||||
|
/// Amount to send.
|
||||||
|
pub amount: String,
|
||||||
|
/// Fee rate (satoshis per byte).
|
||||||
|
pub fee_rate: Option<f64>,
|
||||||
|
/// Specific UTXOs to use.
|
||||||
|
pub utxos: Option<Vec<Utxo>>,
|
||||||
|
/// Change address.
|
||||||
|
pub change_address: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuildTransactionOptions {
|
||||||
|
/// Create new build options.
|
||||||
|
pub fn new(to: impl Into<String>, amount: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
to: to.into(),
|
||||||
|
amount: amount.into(),
|
||||||
|
fee_rate: None,
|
||||||
|
utxos: None,
|
||||||
|
change_address: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the fee rate.
|
||||||
|
pub fn fee_rate(mut self, fee_rate: f64) -> Self {
|
||||||
|
self.fee_rate = Some(fee_rate);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set specific UTXOs to use.
|
||||||
|
pub fn utxos(mut self, utxos: Vec<Utxo>) -> Self {
|
||||||
|
self.utxos = Some(utxos);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the change address.
|
||||||
|
pub fn change_address(mut self, address: impl Into<String>) -> Self {
|
||||||
|
self.change_address = Some(address.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for querying UTXOs.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct GetUtxosOptions {
|
||||||
|
/// Minimum confirmations.
|
||||||
|
pub min_confirmations: Option<u32>,
|
||||||
|
/// Minimum amount.
|
||||||
|
pub min_amount: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetUtxosOptions {
|
||||||
|
/// Set minimum confirmations.
|
||||||
|
pub fn min_confirmations(mut self, confirmations: u32) -> Self {
|
||||||
|
self.min_confirmations = Some(confirmations);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set minimum amount.
|
||||||
|
pub fn min_amount(mut self, amount: impl Into<String>) -> Self {
|
||||||
|
self.min_amount = Some(amount.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
209
sdk/shared/schemas/bridge.json
Normal file
209
sdk/shared/schemas/bridge.json
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/bridge.json",
|
||||||
|
"title": "Synor Bridge SDK",
|
||||||
|
"description": "Cross-chain asset transfers and bridging operations",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"BridgeConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Asset": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"chain": { "$ref": "common.json#/$defs/ChainId" },
|
||||||
|
"address": { "type": "string" },
|
||||||
|
"symbol": { "type": "string" },
|
||||||
|
"decimals": { "type": "integer" },
|
||||||
|
"isNative": { "type": "boolean" },
|
||||||
|
"isWrapped": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["chain", "symbol", "decimals"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Chain": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "$ref": "common.json#/$defs/ChainId" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"rpcUrl": { "type": "string", "format": "uri" },
|
||||||
|
"explorerUrl": { "type": "string", "format": "uri" },
|
||||||
|
"nativeCurrency": { "$ref": "#/$defs/Asset" },
|
||||||
|
"confirmationsRequired": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["id", "name"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TransferStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "locked", "minting", "minted", "burning", "unlocking", "completed", "failed", "refunded"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Transfer": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"status": { "$ref": "#/$defs/TransferStatus" },
|
||||||
|
"sourceChain": { "$ref": "common.json#/$defs/ChainId" },
|
||||||
|
"targetChain": { "$ref": "common.json#/$defs/ChainId" },
|
||||||
|
"sourceAddress": { "type": "string" },
|
||||||
|
"targetAddress": { "type": "string" },
|
||||||
|
"asset": { "$ref": "#/$defs/Asset" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"fee": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"sourceTxHash": { "type": "string" },
|
||||||
|
"targetTxHash": { "type": "string" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"completedAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["id", "status", "sourceChain", "targetChain", "asset", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"LockRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"asset": { "$ref": "#/$defs/Asset" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"targetChain": { "$ref": "common.json#/$defs/ChainId" },
|
||||||
|
"targetAddress": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["asset", "amount", "targetChain", "targetAddress"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"LockReceipt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"transferId": { "type": "string" },
|
||||||
|
"txHash": { "type": "string" },
|
||||||
|
"lockProof": { "$ref": "#/$defs/LockProof" },
|
||||||
|
"estimatedCompletion": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["transferId", "txHash"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"LockProof": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"blockHash": { "type": "string" },
|
||||||
|
"blockNumber": { "type": "integer" },
|
||||||
|
"txHash": { "type": "string" },
|
||||||
|
"logIndex": { "type": "integer" },
|
||||||
|
"proof": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"signatures": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "common.json#/$defs/Signature" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["blockHash", "blockNumber", "txHash", "proof"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"MintRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"proof": { "$ref": "#/$defs/LockProof" },
|
||||||
|
"targetAddress": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["proof", "targetAddress"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"BurnRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"wrappedAsset": { "$ref": "#/$defs/Asset" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["wrappedAsset", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"BurnReceipt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"transferId": { "type": "string" },
|
||||||
|
"txHash": { "type": "string" },
|
||||||
|
"burnProof": { "$ref": "#/$defs/BurnProof" }
|
||||||
|
},
|
||||||
|
"required": ["transferId", "txHash"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"BurnProof": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"blockHash": { "type": "string" },
|
||||||
|
"blockNumber": { "type": "integer" },
|
||||||
|
"txHash": { "type": "string" },
|
||||||
|
"proof": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["blockHash", "txHash", "proof"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"UnlockRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"proof": { "$ref": "#/$defs/BurnProof" }
|
||||||
|
},
|
||||||
|
"required": ["proof"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TransferFilter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": { "$ref": "#/$defs/TransferStatus" },
|
||||||
|
"sourceChain": { "$ref": "common.json#/$defs/ChainId" },
|
||||||
|
"targetChain": { "$ref": "common.json#/$defs/ChainId" },
|
||||||
|
"address": { "type": "string" }
|
||||||
|
},
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/PaginationParams" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ExchangeRate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"from": { "$ref": "#/$defs/Asset" },
|
||||||
|
"to": { "$ref": "#/$defs/Asset" },
|
||||||
|
"rate": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"fee": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"minAmount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"maxAmount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"estimatedTime": { "$ref": "common.json#/$defs/Duration" }
|
||||||
|
},
|
||||||
|
"required": ["from", "to", "rate", "fee"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetSupportedChainsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"chains": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/Chain" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["chains"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetSupportedAssetsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"chain": { "$ref": "common.json#/$defs/ChainId" }
|
||||||
|
},
|
||||||
|
"required": ["chain"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetExchangeRateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"from": { "$ref": "#/$defs/Asset" },
|
||||||
|
"to": { "$ref": "#/$defs/Asset" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["from", "to"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
sdk/shared/schemas/common.json
Normal file
186
sdk/shared/schemas/common.json
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/common.json",
|
||||||
|
"title": "Synor Common Types",
|
||||||
|
"description": "Shared type definitions used across all Synor SDKs",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"SynorConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Base configuration for all Synor SDK clients",
|
||||||
|
"properties": {
|
||||||
|
"apiKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "API key for authentication"
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "API endpoint URL"
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000,
|
||||||
|
"default": 30000,
|
||||||
|
"description": "Request timeout in milliseconds"
|
||||||
|
},
|
||||||
|
"retries": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 10,
|
||||||
|
"default": 3,
|
||||||
|
"description": "Number of retry attempts"
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Enable debug logging"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["apiKey"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Address": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^synor1[a-z0-9]{38}$",
|
||||||
|
"description": "Synor blockchain address (bech32 encoded)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"TxHash": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{64}$",
|
||||||
|
"description": "Transaction hash (32 bytes hex)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"BlockHash": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{64}$",
|
||||||
|
"description": "Block hash (32 bytes hex)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"ContentId": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[a-z0-9]{55})$",
|
||||||
|
"description": "Content identifier (IPFS CID v0 or v1)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Amount": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$",
|
||||||
|
"description": "Decimal amount as string for precision"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Timestamp": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Unix timestamp in milliseconds"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Duration": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Duration in seconds"
|
||||||
|
},
|
||||||
|
|
||||||
|
"PublicKey": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{66}$",
|
||||||
|
"description": "Compressed public key (33 bytes hex)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Signature": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{128,144}$",
|
||||||
|
"description": "ECDSA or Schnorr signature"
|
||||||
|
},
|
||||||
|
|
||||||
|
"UTXO": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txid": { "$ref": "#/$defs/TxHash" },
|
||||||
|
"vout": { "type": "integer", "minimum": 0 },
|
||||||
|
"amount": { "$ref": "#/$defs/Amount" },
|
||||||
|
"address": { "$ref": "#/$defs/Address" },
|
||||||
|
"confirmations": { "type": "integer", "minimum": 0 },
|
||||||
|
"scriptPubKey": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["txid", "vout", "amount", "address"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Balance": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"confirmed": { "$ref": "#/$defs/Amount" },
|
||||||
|
"unconfirmed": { "$ref": "#/$defs/Amount" },
|
||||||
|
"total": { "$ref": "#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["confirmed", "unconfirmed", "total"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ChainId": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["synor-mainnet", "synor-testnet", "ethereum", "bitcoin", "solana", "polygon", "arbitrum", "optimism"],
|
||||||
|
"description": "Blockchain network identifier"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Priority": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["low", "medium", "high", "urgent"],
|
||||||
|
"description": "Transaction priority level"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "processing", "completed", "failed", "cancelled"],
|
||||||
|
"description": "Generic operation status"
|
||||||
|
},
|
||||||
|
|
||||||
|
"PaginationParams": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 },
|
||||||
|
"offset": { "type": "integer", "minimum": 0, "default": 0 },
|
||||||
|
"cursor": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PaginatedResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": { "type": "array" },
|
||||||
|
"total": { "type": "integer" },
|
||||||
|
"hasMore": { "type": "boolean" },
|
||||||
|
"nextCursor": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["items", "hasMore"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ErrorCode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"INVALID_REQUEST",
|
||||||
|
"AUTHENTICATION_FAILED",
|
||||||
|
"AUTHORIZATION_DENIED",
|
||||||
|
"RESOURCE_NOT_FOUND",
|
||||||
|
"RATE_LIMITED",
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
"SERVICE_UNAVAILABLE",
|
||||||
|
"TIMEOUT",
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
"INSUFFICIENT_FUNDS"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": { "$ref": "#/$defs/ErrorCode" },
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"details": { "type": "object" },
|
||||||
|
"requestId": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["code", "message"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
320
sdk/shared/schemas/contract.json
Normal file
320
sdk/shared/schemas/contract.json
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/contract.json",
|
||||||
|
"title": "Synor Contract SDK",
|
||||||
|
"description": "Smart contract deployment, interaction, and ABI management",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"ContractConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ContractAddress": {
|
||||||
|
"$ref": "common.json#/$defs/Address"
|
||||||
|
},
|
||||||
|
|
||||||
|
"AbiType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"uint8", "uint16", "uint32", "uint64", "uint128", "uint256",
|
||||||
|
"int8", "int16", "int32", "int64", "int128", "int256",
|
||||||
|
"bool", "string", "bytes", "bytes32", "address",
|
||||||
|
"tuple", "array"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"AbiParameter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"type": { "$ref": "#/$defs/AbiType" },
|
||||||
|
"indexed": { "type": "boolean" },
|
||||||
|
"components": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/AbiParameter" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "type"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"AbiFunction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["function", "constructor", "fallback", "receive"]
|
||||||
|
},
|
||||||
|
"stateMutability": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pure", "view", "nonpayable", "payable"]
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/AbiParameter" }
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/AbiParameter" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "type", "inputs"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"AbiEvent": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"type": { "const": "event" },
|
||||||
|
"anonymous": { "type": "boolean" },
|
||||||
|
"inputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/AbiParameter" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "type", "inputs"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"AbiError": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"type": { "const": "error" },
|
||||||
|
"inputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/AbiParameter" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "type"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ABI": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "#/$defs/AbiFunction" },
|
||||||
|
{ "$ref": "#/$defs/AbiEvent" },
|
||||||
|
{ "$ref": "#/$defs/AbiError" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"ContractInterface": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "#/$defs/ContractAddress" },
|
||||||
|
"abi": { "$ref": "#/$defs/ABI" },
|
||||||
|
"functions": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "$ref": "#/$defs/AbiFunction" }
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "$ref": "#/$defs/AbiEvent" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["abi"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DeployRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bytecode": { "type": "string", "description": "Contract bytecode (hex)" },
|
||||||
|
"abi": { "$ref": "#/$defs/ABI" },
|
||||||
|
"args": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Constructor arguments"
|
||||||
|
},
|
||||||
|
"value": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"gasLimit": { "type": "integer" },
|
||||||
|
"gasPrice": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["bytecode"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DeployResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "#/$defs/ContractAddress" },
|
||||||
|
"txHash": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"blockNumber": { "type": "integer" },
|
||||||
|
"gasUsed": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["address", "txHash"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CallRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contract": { "$ref": "#/$defs/ContractAddress" },
|
||||||
|
"method": { "type": "string" },
|
||||||
|
"args": { "type": "array" },
|
||||||
|
"blockNumber": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "integer" },
|
||||||
|
{ "type": "string", "enum": ["latest", "pending", "earliest"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["contract", "method"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CallResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"result": {},
|
||||||
|
"decoded": { "type": "object" }
|
||||||
|
},
|
||||||
|
"required": ["result"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SendRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contract": { "$ref": "#/$defs/ContractAddress" },
|
||||||
|
"method": { "type": "string" },
|
||||||
|
"args": { "type": "array" },
|
||||||
|
"value": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"gasLimit": { "type": "integer" },
|
||||||
|
"gasPrice": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"nonce": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["contract", "method"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SendResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txHash": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"blockNumber": { "type": "integer" },
|
||||||
|
"gasUsed": { "type": "integer" },
|
||||||
|
"status": { "type": "boolean" },
|
||||||
|
"events": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/Event" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["txHash"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Event": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "#/$defs/ContractAddress" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"signature": { "type": "string" },
|
||||||
|
"topics": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"data": { "type": "string" },
|
||||||
|
"decoded": { "type": "object" },
|
||||||
|
"blockNumber": { "type": "integer" },
|
||||||
|
"txHash": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"logIndex": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["address", "topics", "data"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"EventFilter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "#/$defs/ContractAddress" },
|
||||||
|
"topics": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "string" },
|
||||||
|
{ "type": "array", "items": { "type": "string" } },
|
||||||
|
{ "type": "null" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fromBlock": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "integer" },
|
||||||
|
{ "type": "string", "enum": ["latest", "pending", "earliest"] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"toBlock": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "integer" },
|
||||||
|
{ "type": "string", "enum": ["latest", "pending", "earliest"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetEventsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contract": { "$ref": "#/$defs/ContractAddress" },
|
||||||
|
"filter": { "$ref": "#/$defs/EventFilter" },
|
||||||
|
"eventName": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["contract"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetEventsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"events": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/Event" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["events"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"EncodeCallRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"method": { "type": "string" },
|
||||||
|
"args": { "type": "array" },
|
||||||
|
"abi": { "$ref": "#/$defs/ABI" }
|
||||||
|
},
|
||||||
|
"required": ["method", "abi"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"EncodeCallResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": { "type": "string", "description": "Encoded calldata (hex)" },
|
||||||
|
"selector": { "type": "string", "description": "Function selector (4 bytes hex)" }
|
||||||
|
},
|
||||||
|
"required": ["data", "selector"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DecodeResultRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": { "type": "string" },
|
||||||
|
"method": { "type": "string" },
|
||||||
|
"abi": { "$ref": "#/$defs/ABI" }
|
||||||
|
},
|
||||||
|
"required": ["data", "method", "abi"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DecodeResultResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"decoded": {},
|
||||||
|
"raw": { "type": "array" }
|
||||||
|
},
|
||||||
|
"required": ["decoded"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Subscription": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"contract": { "$ref": "#/$defs/ContractAddress" },
|
||||||
|
"eventName": { "type": "string" },
|
||||||
|
"filter": { "$ref": "#/$defs/EventFilter" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["id", "contract"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
275
sdk/shared/schemas/database.json
Normal file
275
sdk/shared/schemas/database.json
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/database.json",
|
||||||
|
"title": "Synor Database SDK",
|
||||||
|
"description": "Key-value, document, vector, and time-series database operations",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"DatabaseConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }],
|
||||||
|
"properties": {
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database namespace for isolation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Value": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "string" },
|
||||||
|
{ "type": "number" },
|
||||||
|
{ "type": "boolean" },
|
||||||
|
{ "type": "object" },
|
||||||
|
{ "type": "array" },
|
||||||
|
{ "type": "null" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"KeyValue": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"key": { "type": "string" },
|
||||||
|
"value": { "$ref": "#/$defs/Value" },
|
||||||
|
"ttl": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"updatedAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["key", "value"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"KvGetRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"key": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["key"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"KvSetRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"key": { "type": "string" },
|
||||||
|
"value": { "$ref": "#/$defs/Value" },
|
||||||
|
"ttl": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"nx": { "type": "boolean", "description": "Only set if key does not exist" },
|
||||||
|
"xx": { "type": "boolean", "description": "Only set if key exists" }
|
||||||
|
},
|
||||||
|
"required": ["key", "value"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"KvListRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"prefix": { "type": "string" },
|
||||||
|
"cursor": { "type": "string" },
|
||||||
|
"limit": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100 }
|
||||||
|
},
|
||||||
|
"required": ["prefix"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Document": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"_id": { "type": "string" },
|
||||||
|
"_rev": { "type": "string" },
|
||||||
|
"_createdAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"_updatedAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["_id"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DocumentQuery": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filter": { "type": "object" },
|
||||||
|
"sort": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"field": { "type": "string" },
|
||||||
|
"order": { "type": "string", "enum": ["asc", "desc"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projection": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/PaginationParams" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DocCreateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"collection": { "type": "string" },
|
||||||
|
"document": { "type": "object" }
|
||||||
|
},
|
||||||
|
"required": ["collection", "document"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DocGetRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"collection": { "type": "string" },
|
||||||
|
"id": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["collection", "id"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DocUpdateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"collection": { "type": "string" },
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"update": { "type": "object" },
|
||||||
|
"upsert": { "type": "boolean", "default": false }
|
||||||
|
},
|
||||||
|
"required": ["collection", "id", "update"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DocQueryRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"collection": { "type": "string" },
|
||||||
|
"query": { "$ref": "#/$defs/DocumentQuery" }
|
||||||
|
},
|
||||||
|
"required": ["collection", "query"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VectorEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"vector": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "number" }
|
||||||
|
},
|
||||||
|
"metadata": { "type": "object" }
|
||||||
|
},
|
||||||
|
"required": ["id", "vector"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VectorUpsertRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"collection": { "type": "string" },
|
||||||
|
"vectors": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/VectorEntry" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["collection", "vectors"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VectorSearchRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"collection": { "type": "string" },
|
||||||
|
"vector": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "number" }
|
||||||
|
},
|
||||||
|
"k": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10 },
|
||||||
|
"filter": { "type": "object" },
|
||||||
|
"includeMetadata": { "type": "boolean", "default": true },
|
||||||
|
"includeVectors": { "type": "boolean", "default": false }
|
||||||
|
},
|
||||||
|
"required": ["collection", "vector"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VectorSearchResult": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"score": { "type": "number" },
|
||||||
|
"vector": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "number" }
|
||||||
|
},
|
||||||
|
"metadata": { "type": "object" }
|
||||||
|
},
|
||||||
|
"required": ["id", "score"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DataPoint": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"timestamp": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"value": { "type": "number" },
|
||||||
|
"tags": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["timestamp", "value"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TimeRange": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"start": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"end": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["start", "end"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Aggregation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"function": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["avg", "sum", "min", "max", "count", "first", "last", "stddev"]
|
||||||
|
},
|
||||||
|
"interval": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"groupBy": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["function"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TsWriteRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"series": { "type": "string" },
|
||||||
|
"points": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/DataPoint" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["series", "points"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TsQueryRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"series": { "type": "string" },
|
||||||
|
"range": { "$ref": "#/$defs/TimeRange" },
|
||||||
|
"aggregation": { "$ref": "#/$defs/Aggregation" },
|
||||||
|
"filter": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["series", "range"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TsQueryResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"series": { "type": "string" },
|
||||||
|
"points": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/DataPoint" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["series", "points"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
245
sdk/shared/schemas/economics.json
Normal file
245
sdk/shared/schemas/economics.json
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/economics.json",
|
||||||
|
"title": "Synor Economics SDK",
|
||||||
|
"description": "Pricing, billing, staking, and reward management",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"EconomicsConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ServiceType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["compute", "storage", "hosting", "database", "rpc", "bridge", "mining"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"UsageMetrics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"service": { "$ref": "#/$defs/ServiceType" },
|
||||||
|
"computeSeconds": { "type": "number" },
|
||||||
|
"storageBytes": { "type": "integer" },
|
||||||
|
"bandwidthBytes": { "type": "integer" },
|
||||||
|
"requests": { "type": "integer" },
|
||||||
|
"gpuSeconds": { "type": "number" }
|
||||||
|
},
|
||||||
|
"required": ["service"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Price": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"currency": { "type": "string", "default": "SYNOR" },
|
||||||
|
"unit": { "type": "string" },
|
||||||
|
"breakdown": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/PriceComponent" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["amount", "currency"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"PriceComponent": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"quantity": { "type": "number" },
|
||||||
|
"unitPrice": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["name", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"UsagePlan": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"services": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/UsageMetrics" }
|
||||||
|
},
|
||||||
|
"period": { "$ref": "#/$defs/BillingPeriod" }
|
||||||
|
},
|
||||||
|
"required": ["services"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CostEstimate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"total": { "$ref": "#/$defs/Price" },
|
||||||
|
"services": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"service": { "$ref": "#/$defs/ServiceType" },
|
||||||
|
"cost": { "$ref": "#/$defs/Price" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"discounts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/AppliedDiscount" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["total"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"BillingPeriod": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["hourly", "daily", "weekly", "monthly", "yearly"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Usage": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"period": { "$ref": "#/$defs/BillingPeriod" },
|
||||||
|
"startDate": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"endDate": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"services": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"service": { "$ref": "#/$defs/ServiceType" },
|
||||||
|
"metrics": { "$ref": "#/$defs/UsageMetrics" },
|
||||||
|
"cost": { "$ref": "#/$defs/Price" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"total": { "$ref": "#/$defs/Price" }
|
||||||
|
},
|
||||||
|
"required": ["period", "startDate", "endDate", "total"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Invoice": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"period": { "$ref": "#/$defs/BillingPeriod" },
|
||||||
|
"startDate": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"endDate": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"amount": { "$ref": "#/$defs/Price" },
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "paid", "overdue", "cancelled"]
|
||||||
|
},
|
||||||
|
"paidAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"txHash": { "$ref": "common.json#/$defs/TxHash" }
|
||||||
|
},
|
||||||
|
"required": ["id", "period", "amount", "status"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"AccountBalance": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"available": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"pending": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"staked": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"rewards": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["available"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"StakeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"duration": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"validator": { "$ref": "common.json#/$defs/Address" }
|
||||||
|
},
|
||||||
|
"required": ["amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"StakeReceipt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stakeId": { "type": "string" },
|
||||||
|
"txHash": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"startDate": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"endDate": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"estimatedApy": { "type": "number" }
|
||||||
|
},
|
||||||
|
"required": ["stakeId", "txHash", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"UnstakeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stakeId": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["stakeId"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"UnstakeReceipt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txHash": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"rewards": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"availableAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["txHash", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Rewards": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"total": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"claimed": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"pending": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"apy": { "type": "number" },
|
||||||
|
"breakdown": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stakeId": { "type": "string" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"period": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["total", "claimed", "pending"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Discount": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": { "type": "string" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["percentage", "fixed", "volume", "loyalty", "referral"]
|
||||||
|
},
|
||||||
|
"value": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"minSpend": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"maxDiscount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"validUntil": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"usesRemaining": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["code", "name", "type", "value"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"AppliedDiscount": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"discount": { "$ref": "#/$defs/Discount" },
|
||||||
|
"savedAmount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["discount", "savedAmount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ApplyDiscountRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["code"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
212
sdk/shared/schemas/governance.json
Normal file
212
sdk/shared/schemas/governance.json
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/governance.json",
|
||||||
|
"title": "Synor Governance SDK",
|
||||||
|
"description": "DAO, proposals, voting, and vesting management",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"GovernanceConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ProposalStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["draft", "pending", "active", "passed", "rejected", "executed", "cancelled", "expired"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ProposalType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["parameter_change", "treasury_spend", "contract_upgrade", "text", "emergency"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Proposal": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"title": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"type": { "$ref": "#/$defs/ProposalType" },
|
||||||
|
"status": { "$ref": "#/$defs/ProposalStatus" },
|
||||||
|
"proposer": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"votingStartsAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"votingEndsAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"executionDelay": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"quorum": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"currentQuorum": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"votesFor": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"votesAgainst": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"votesAbstain": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"actions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/ProposalAction" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "title", "type", "status", "proposer"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ProposalAction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"target": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"method": { "type": "string" },
|
||||||
|
"args": { "type": "array" },
|
||||||
|
"value": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["target", "method"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ProposalDraft": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": { "type": "string", "minLength": 10, "maxLength": 200 },
|
||||||
|
"description": { "type": "string", "minLength": 50 },
|
||||||
|
"type": { "$ref": "#/$defs/ProposalType" },
|
||||||
|
"actions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/ProposalAction" }
|
||||||
|
},
|
||||||
|
"votingPeriod": { "$ref": "common.json#/$defs/Duration" }
|
||||||
|
},
|
||||||
|
"required": ["title", "description", "type"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ProposalFilter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": { "$ref": "#/$defs/ProposalStatus" },
|
||||||
|
"type": { "$ref": "#/$defs/ProposalType" },
|
||||||
|
"proposer": { "$ref": "common.json#/$defs/Address" }
|
||||||
|
},
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/PaginationParams" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VoteChoice": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["for", "against", "abstain"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Vote": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"proposalId": { "type": "string" },
|
||||||
|
"choice": { "$ref": "#/$defs/VoteChoice" },
|
||||||
|
"weight": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"reason": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["proposalId", "choice"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VoteReceipt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txHash": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"proposalId": { "type": "string" },
|
||||||
|
"voter": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"choice": { "$ref": "#/$defs/VoteChoice" },
|
||||||
|
"weight": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"timestamp": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["txHash", "proposalId", "voter", "choice", "weight"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DelegationReceipt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txHash": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"delegator": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"delegatee": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["txHash", "delegator", "delegatee", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VotingPower": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"ownPower": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"delegatedPower": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"totalPower": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"delegatedTo": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"delegators": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["address", "totalPower"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DaoConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"token": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"votingDelay": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"votingPeriod": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"proposalThreshold": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"quorumThreshold": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["name", "token"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Dao": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"token": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"treasury": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"proposalCount": { "type": "integer" },
|
||||||
|
"memberCount": { "type": "integer" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["id", "address", "name", "token"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VestingSchedule": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"beneficiary": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"totalAmount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"startTime": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"cliffDuration": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"vestingDuration": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"slicePeriod": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"revocable": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["beneficiary", "totalAmount", "startTime", "vestingDuration"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VestingContract": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"schedule": { "$ref": "#/$defs/VestingSchedule" },
|
||||||
|
"released": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"releasable": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"revoked": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["id", "address", "schedule", "released"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ClaimVestedRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contractId": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["contractId"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
190
sdk/shared/schemas/hosting.json
Normal file
190
sdk/shared/schemas/hosting.json
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/hosting.json",
|
||||||
|
"title": "Synor Hosting SDK",
|
||||||
|
"description": "Domain management, DNS, deployments, and SSL certificates",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"HostingConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Domain": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"owner": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"resolver": { "$ref": "common.json#/$defs/ContentId" },
|
||||||
|
"registeredAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"expiresAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"verified": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["name", "owner"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DomainRecord": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"cid": { "$ref": "common.json#/$defs/ContentId" },
|
||||||
|
"dnslink": { "type": "string" },
|
||||||
|
"ipv4": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string", "format": "ipv4" }
|
||||||
|
},
|
||||||
|
"ipv6": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string", "format": "ipv6" }
|
||||||
|
},
|
||||||
|
"updatedAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"RegisterDomainRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string", "pattern": "^[a-z0-9-]+\\.synor$" },
|
||||||
|
"duration": { "$ref": "common.json#/$defs/Duration" }
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ResolveDomainRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"UpdateDomainRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"record": { "$ref": "#/$defs/DomainRecord" }
|
||||||
|
},
|
||||||
|
"required": ["name", "record"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DnsRecordType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "CAA", "DNSLINK"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DnsRecord": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "$ref": "#/$defs/DnsRecordType" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"value": { "type": "string" },
|
||||||
|
"ttl": { "type": "integer", "minimum": 60, "default": 3600 },
|
||||||
|
"priority": { "type": "integer", "minimum": 0 }
|
||||||
|
},
|
||||||
|
"required": ["type", "name", "value"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SetDnsRecordsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"records": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/DnsRecord" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["domain", "records"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetDnsRecordsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"type": { "$ref": "#/$defs/DnsRecordType" }
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DeploymentStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["queued", "building", "deploying", "ready", "failed", "cancelled"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Deployment": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"cid": { "$ref": "common.json#/$defs/ContentId" },
|
||||||
|
"status": { "$ref": "#/$defs/DeploymentStatus" },
|
||||||
|
"url": { "type": "string", "format": "uri" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"readyAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"error": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["id", "domain", "cid", "status"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DeployRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cid": { "$ref": "common.json#/$defs/ContentId" },
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"buildCommand": { "type": "string" },
|
||||||
|
"outputDirectory": { "type": "string" },
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["cid", "domain"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ListDeploymentsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"status": { "$ref": "#/$defs/DeploymentStatus" }
|
||||||
|
},
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/PaginationParams" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Certificate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"issuer": { "type": "string" },
|
||||||
|
"validFrom": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"validTo": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"fingerprint": { "type": "string" },
|
||||||
|
"autoRenew": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["domain", "validFrom", "validTo"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ProvisionSslRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"wildcard": { "type": "boolean", "default": false }
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DomainVerification": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": { "type": "string" },
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["dns", "http", "email"]
|
||||||
|
},
|
||||||
|
"token": { "type": "string" },
|
||||||
|
"verified": { "type": "boolean" },
|
||||||
|
"expiresAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["domain", "method", "token"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
195
sdk/shared/schemas/mining.json
Normal file
195
sdk/shared/schemas/mining.json
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/mining.json",
|
||||||
|
"title": "Synor Mining SDK",
|
||||||
|
"description": "Mining pool connection, block templates, and hashrate management",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"MiningConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }],
|
||||||
|
"properties": {
|
||||||
|
"stratumUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Stratum protocol endpoint"
|
||||||
|
},
|
||||||
|
"workerName": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PoolConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": { "type": "string", "format": "uri" },
|
||||||
|
"username": { "type": "string" },
|
||||||
|
"password": { "type": "string" },
|
||||||
|
"workerName": { "type": "string" },
|
||||||
|
"algorithm": { "$ref": "#/$defs/MiningAlgorithm" }
|
||||||
|
},
|
||||||
|
"required": ["url", "username"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"MiningAlgorithm": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["sha256d", "scrypt", "ethash", "randomx", "kawpow", "synor-pow"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"StratumConnection": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"poolUrl": { "type": "string" },
|
||||||
|
"connected": { "type": "boolean" },
|
||||||
|
"difficulty": { "type": "number" },
|
||||||
|
"extranonce1": { "type": "string" },
|
||||||
|
"extranonce2Size": { "type": "integer" },
|
||||||
|
"connectedAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["id", "connected"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"BlockTemplate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"previousBlockHash": { "$ref": "common.json#/$defs/BlockHash" },
|
||||||
|
"height": { "type": "integer" },
|
||||||
|
"version": { "type": "integer" },
|
||||||
|
"bits": { "type": "string" },
|
||||||
|
"target": { "type": "string" },
|
||||||
|
"curTime": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"coinbaseValue": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"transactions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/TemplateTransaction" }
|
||||||
|
},
|
||||||
|
"merkleRoot": { "type": "string" },
|
||||||
|
"nonceRange": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["previousBlockHash", "height", "target", "coinbaseValue"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TemplateTransaction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": { "type": "string" },
|
||||||
|
"txid": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"fee": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"weight": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["data", "txid", "fee"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"MinedWork": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"jobId": { "type": "string" },
|
||||||
|
"extranonce2": { "type": "string" },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"nTime": { "type": "string" },
|
||||||
|
"hash": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["jobId", "extranonce2", "nonce", "nTime"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SubmitResult": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"accepted": { "type": "boolean" },
|
||||||
|
"blockHash": { "$ref": "common.json#/$defs/BlockHash" },
|
||||||
|
"difficulty": { "type": "number" },
|
||||||
|
"rejectReason": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["accepted"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Hashrate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"current": { "type": "number", "description": "Hashes per second" },
|
||||||
|
"average1h": { "type": "number" },
|
||||||
|
"average24h": { "type": "number" },
|
||||||
|
"unit": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["H/s", "KH/s", "MH/s", "GH/s", "TH/s", "PH/s"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["current", "unit"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"MiningStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hashrate": { "$ref": "#/$defs/Hashrate" },
|
||||||
|
"sharesAccepted": { "type": "integer" },
|
||||||
|
"sharesRejected": { "type": "integer" },
|
||||||
|
"blocksFound": { "type": "integer" },
|
||||||
|
"uptime": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"efficiency": { "type": "number", "minimum": 0, "maximum": 100 },
|
||||||
|
"lastShareAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["hashrate", "sharesAccepted", "sharesRejected"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TimePeriod": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["hour", "day", "week", "month", "all"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Earnings": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"period": { "$ref": "#/$defs/TimePeriod" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"blocks": { "type": "integer" },
|
||||||
|
"shares": { "type": "integer" },
|
||||||
|
"estimatedDaily": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["period", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"MiningDevice": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["cpu", "gpu", "asic", "fpga"]
|
||||||
|
},
|
||||||
|
"vendor": { "type": "string" },
|
||||||
|
"model": { "type": "string" },
|
||||||
|
"hashrate": { "$ref": "#/$defs/Hashrate" },
|
||||||
|
"temperature": { "type": "number" },
|
||||||
|
"fanSpeed": { "type": "integer", "minimum": 0, "maximum": 100 },
|
||||||
|
"power": { "type": "number", "description": "Watts" },
|
||||||
|
"enabled": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["id", "name", "type"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DeviceConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"intensity": { "type": "integer", "minimum": 0, "maximum": 100 },
|
||||||
|
"powerLimit": { "type": "integer" },
|
||||||
|
"coreClockOffset": { "type": "integer" },
|
||||||
|
"memoryClockOffset": { "type": "integer" },
|
||||||
|
"fanSpeed": { "type": "integer", "minimum": 0, "maximum": 100 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"ListDevicesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"devices": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/MiningDevice" }
|
||||||
|
},
|
||||||
|
"totalHashrate": { "$ref": "#/$defs/Hashrate" }
|
||||||
|
},
|
||||||
|
"required": ["devices"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
243
sdk/shared/schemas/privacy.json
Normal file
243
sdk/shared/schemas/privacy.json
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/privacy.json",
|
||||||
|
"title": "Synor Privacy SDK",
|
||||||
|
"description": "Confidential transactions, ring signatures, stealth addresses, and commitments",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"PrivacyConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Commitment": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": { "type": "string", "description": "Commitment value (hex)" },
|
||||||
|
"blindingFactor": { "type": "string", "description": "Blinding factor (hex)" }
|
||||||
|
},
|
||||||
|
"required": ["value"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"RangeProof": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"proof": { "type": "string", "description": "Bulletproof range proof (hex)" },
|
||||||
|
"commitment": { "$ref": "#/$defs/Commitment" }
|
||||||
|
},
|
||||||
|
"required": ["proof", "commitment"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ConfidentialOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"commitment": { "$ref": "#/$defs/Commitment" },
|
||||||
|
"rangeProof": { "$ref": "#/$defs/RangeProof" },
|
||||||
|
"encryptedAmount": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["address", "commitment", "rangeProof"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ConfidentialTransaction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txid": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"inputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "common.json#/$defs/UTXO" }
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/ConfidentialOutput" }
|
||||||
|
},
|
||||||
|
"fee": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"proof": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["inputs", "outputs"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CreateConfidentialTxRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"inputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "common.json#/$defs/UTXO" }
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["address", "amount"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["inputs", "outputs"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"RingSignature": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"keyImage": { "type": "string" },
|
||||||
|
"c": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"r": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"ringSize": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["keyImage", "c", "r", "ringSize"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CreateRingSignatureRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"messageFormat": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["text", "hex", "base64"],
|
||||||
|
"default": "hex"
|
||||||
|
},
|
||||||
|
"ring": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "common.json#/$defs/PublicKey" },
|
||||||
|
"minItems": 2
|
||||||
|
},
|
||||||
|
"secretIndex": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["message", "ring"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VerifyRingSignatureRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"signature": { "$ref": "#/$defs/RingSignature" },
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"ring": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "common.json#/$defs/PublicKey" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["signature", "message", "ring"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"StealthAddress": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"viewKey": { "$ref": "common.json#/$defs/PublicKey" },
|
||||||
|
"spendKey": { "$ref": "common.json#/$defs/PublicKey" },
|
||||||
|
"ephemeralKey": { "$ref": "common.json#/$defs/PublicKey" }
|
||||||
|
},
|
||||||
|
"required": ["address", "viewKey", "spendKey"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GenerateStealthAddressResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stealthAddress": { "$ref": "#/$defs/StealthAddress" },
|
||||||
|
"sharedSecret": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["stealthAddress"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DeriveSharedSecretRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stealthAddress": { "$ref": "#/$defs/StealthAddress" },
|
||||||
|
"privateKey": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["stealthAddress", "privateKey"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SharedSecret": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secret": { "type": "string" },
|
||||||
|
"derivedKey": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["secret"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CreateCommitmentRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"blinding": { "type": "string", "description": "Optional blinding factor (hex)" }
|
||||||
|
},
|
||||||
|
"required": ["value"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CreateCommitmentResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"commitment": { "$ref": "#/$defs/Commitment" },
|
||||||
|
"blindingFactor": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["commitment", "blindingFactor"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"OpenCommitmentRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"commitment": { "$ref": "#/$defs/Commitment" },
|
||||||
|
"value": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"blinding": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["commitment", "value", "blinding"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"OpenCommitmentResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["valid"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ZeroKnowledgeProof": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"proofType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["groth16", "plonk", "bulletproofs", "stark"]
|
||||||
|
},
|
||||||
|
"proof": { "type": "string" },
|
||||||
|
"publicInputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"verificationKey": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["proofType", "proof"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VerifyZkProofRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"proof": { "$ref": "#/$defs/ZeroKnowledgeProof" },
|
||||||
|
"publicInputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["proof"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"VerifyZkProofResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": { "type": "boolean" },
|
||||||
|
"error": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["valid"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
sdk/shared/schemas/rpc.json
Normal file
205
sdk/shared/schemas/rpc.json
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/rpc.json",
|
||||||
|
"title": "Synor RPC SDK",
|
||||||
|
"description": "Blockchain RPC client for querying blocks, transactions, and chain state",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"RpcConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }],
|
||||||
|
"properties": {
|
||||||
|
"wsEndpoint": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "WebSocket endpoint for subscriptions"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["mainnet", "testnet"],
|
||||||
|
"default": "mainnet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"BlockHeader": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hash": { "$ref": "common.json#/$defs/BlockHash" },
|
||||||
|
"height": { "type": "integer", "minimum": 0 },
|
||||||
|
"version": { "type": "integer" },
|
||||||
|
"previousHash": { "$ref": "common.json#/$defs/BlockHash" },
|
||||||
|
"merkleRoot": { "type": "string" },
|
||||||
|
"timestamp": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"difficulty": { "type": "string" },
|
||||||
|
"nonce": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["hash", "height", "previousHash", "merkleRoot", "timestamp"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Block": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "#/$defs/BlockHeader" }],
|
||||||
|
"properties": {
|
||||||
|
"transactions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "common.json#/$defs/TxHash" }
|
||||||
|
},
|
||||||
|
"size": { "type": "integer" },
|
||||||
|
"weight": { "type": "integer" },
|
||||||
|
"txCount": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["transactions", "txCount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TransactionStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "confirmed", "failed", "replaced"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Transaction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txid": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"blockHash": { "$ref": "common.json#/$defs/BlockHash" },
|
||||||
|
"blockHeight": { "type": "integer" },
|
||||||
|
"confirmations": { "type": "integer" },
|
||||||
|
"timestamp": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"status": { "$ref": "#/$defs/TransactionStatus" },
|
||||||
|
"raw": { "type": "string" },
|
||||||
|
"size": { "type": "integer" },
|
||||||
|
"fee": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"inputs": { "type": "array" },
|
||||||
|
"outputs": { "type": "array" }
|
||||||
|
},
|
||||||
|
"required": ["txid", "status"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetBlockRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hash": { "$ref": "common.json#/$defs/BlockHash" },
|
||||||
|
"height": { "type": "integer" },
|
||||||
|
"includeTransactions": { "type": "boolean", "default": true }
|
||||||
|
},
|
||||||
|
"oneOf": [
|
||||||
|
{ "required": ["hash"] },
|
||||||
|
{ "required": ["height"] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetTransactionRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txid": { "$ref": "common.json#/$defs/TxHash" }
|
||||||
|
},
|
||||||
|
"required": ["txid"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SendTransactionRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"raw": { "type": "string", "description": "Hex-encoded signed transaction" }
|
||||||
|
},
|
||||||
|
"required": ["raw"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SendTransactionResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txid": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"accepted": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["txid", "accepted"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"FeeEstimate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"priority": { "$ref": "common.json#/$defs/Priority" },
|
||||||
|
"feeRate": { "$ref": "common.json#/$defs/Amount" },
|
||||||
|
"estimatedBlocks": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["priority", "feeRate", "estimatedBlocks"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"EstimateFeeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"priority": { "$ref": "common.json#/$defs/Priority" },
|
||||||
|
"targetBlocks": { "type": "integer", "minimum": 1, "maximum": 100 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"ChainInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"chain": { "type": "string" },
|
||||||
|
"network": { "type": "string" },
|
||||||
|
"height": { "type": "integer" },
|
||||||
|
"bestBlockHash": { "$ref": "common.json#/$defs/BlockHash" },
|
||||||
|
"difficulty": { "type": "string" },
|
||||||
|
"medianTime": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"chainWork": { "type": "string" },
|
||||||
|
"syncing": { "type": "boolean" },
|
||||||
|
"syncProgress": { "type": "number", "minimum": 0, "maximum": 1 }
|
||||||
|
},
|
||||||
|
"required": ["chain", "network", "height", "bestBlockHash"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"MempoolInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"size": { "type": "integer" },
|
||||||
|
"bytes": { "type": "integer" },
|
||||||
|
"usage": { "type": "integer" },
|
||||||
|
"maxMempool": { "type": "integer" },
|
||||||
|
"minFee": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["size", "bytes"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SubscriptionType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["blocks", "transactions", "address", "mempool"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SubscribeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "$ref": "#/$defs/SubscriptionType" },
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" }
|
||||||
|
},
|
||||||
|
"required": ["type"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Subscription": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"type": { "$ref": "#/$defs/SubscriptionType" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" }
|
||||||
|
},
|
||||||
|
"required": ["id", "type"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"BlockNotification": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "block" },
|
||||||
|
"block": { "$ref": "#/$defs/Block" }
|
||||||
|
},
|
||||||
|
"required": ["type", "block"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TransactionNotification": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "transaction" },
|
||||||
|
"transaction": { "$ref": "#/$defs/Transaction" },
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" }
|
||||||
|
},
|
||||||
|
"required": ["type", "transaction"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
259
sdk/shared/schemas/storage.json
Normal file
259
sdk/shared/schemas/storage.json
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/storage.json",
|
||||||
|
"title": "Synor Storage SDK",
|
||||||
|
"description": "Decentralized storage, pinning, and content retrieval",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"StorageConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }],
|
||||||
|
"properties": {
|
||||||
|
"gateway": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "IPFS gateway URL"
|
||||||
|
},
|
||||||
|
"pinningService": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Remote pinning service URL"
|
||||||
|
},
|
||||||
|
"chunkSize": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 262144,
|
||||||
|
"maximum": 1048576,
|
||||||
|
"default": 262144,
|
||||||
|
"description": "Chunk size in bytes for large files"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"ContentId": {
|
||||||
|
"$ref": "common.json#/$defs/ContentId"
|
||||||
|
},
|
||||||
|
|
||||||
|
"UploadOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pin": { "type": "boolean", "default": true },
|
||||||
|
"wrapWithDirectory": { "type": "boolean", "default": false },
|
||||||
|
"cidVersion": { "type": "integer", "enum": [0, 1], "default": 1 },
|
||||||
|
"hashAlgorithm": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["sha2-256", "blake3"],
|
||||||
|
"default": "sha2-256"
|
||||||
|
},
|
||||||
|
"onProgress": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Callback function name for progress updates"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"UploadResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" },
|
||||||
|
"size": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"hash": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["cid", "size"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DownloadOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"offset": { "type": "integer", "minimum": 0 },
|
||||||
|
"length": { "type": "integer", "minimum": 1 },
|
||||||
|
"onProgress": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Callback function name for progress updates"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PinStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["queued", "pinning", "pinned", "failed", "unpinned"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Pin": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" },
|
||||||
|
"status": { "$ref": "#/$defs/PinStatus" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"size": { "type": "integer" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"expiresAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"delegates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["cid", "status"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"PinRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"duration": { "$ref": "common.json#/$defs/Duration" },
|
||||||
|
"origins": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["cid"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ListPinsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/PinStatus" }
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["exact", "iexact", "partial", "ipartial"]
|
||||||
|
},
|
||||||
|
"name": { "type": "string" }
|
||||||
|
},
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/PaginationParams" }]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GatewayUrl": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": { "type": "string", "format": "uri" },
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" },
|
||||||
|
"path": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["url", "cid"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CarFile": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "integer", "enum": [1, 2] },
|
||||||
|
"roots": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/ContentId" }
|
||||||
|
},
|
||||||
|
"blocks": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/CarBlock" }
|
||||||
|
},
|
||||||
|
"size": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["version", "roots"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CarBlock": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" },
|
||||||
|
"data": { "type": "string", "description": "Base64-encoded block data" },
|
||||||
|
"size": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["cid", "data"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"FileEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"content": { "type": "string", "description": "Base64-encoded file content" },
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" }
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"DirectoryEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" },
|
||||||
|
"size": { "type": "integer" },
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["file", "directory"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "cid", "type"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CreateDirectoryRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"files": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/FileEntry" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["files"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ListDirectoryRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" },
|
||||||
|
"path": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["cid"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ListDirectoryResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entries": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/DirectoryEntry" }
|
||||||
|
},
|
||||||
|
"cid": { "$ref": "#/$defs/ContentId" }
|
||||||
|
},
|
||||||
|
"required": ["entries", "cid"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ImportCarRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"car": { "type": "string", "description": "Base64-encoded CAR file" },
|
||||||
|
"pin": { "type": "boolean", "default": true }
|
||||||
|
},
|
||||||
|
"required": ["car"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ImportCarResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roots": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/ContentId" }
|
||||||
|
},
|
||||||
|
"blocksImported": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["roots", "blocksImported"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"StorageStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"totalSize": { "type": "integer" },
|
||||||
|
"pinCount": { "type": "integer" },
|
||||||
|
"bandwidth": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"upload": { "type": "integer" },
|
||||||
|
"download": { "type": "integer" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["totalSize", "pinCount"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
208
sdk/shared/schemas/wallet.json
Normal file
208
sdk/shared/schemas/wallet.json
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://synor.io/schemas/wallet.json",
|
||||||
|
"title": "Synor Wallet SDK",
|
||||||
|
"description": "Wallet management, key operations, and transaction signing",
|
||||||
|
|
||||||
|
"$defs": {
|
||||||
|
"WalletConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [{ "$ref": "common.json#/$defs/SynorConfig" }],
|
||||||
|
"properties": {
|
||||||
|
"network": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["mainnet", "testnet"],
|
||||||
|
"default": "mainnet"
|
||||||
|
},
|
||||||
|
"derivationPath": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "m/44'/60'/0'/0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Wallet": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "format": "uuid" },
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"publicKey": { "$ref": "common.json#/$defs/PublicKey" },
|
||||||
|
"createdAt": { "$ref": "common.json#/$defs/Timestamp" },
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["standard", "multisig", "stealth", "hardware"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "address", "publicKey", "type"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"CreateWalletRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"passphrase": { "type": "string", "minLength": 8 },
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["standard", "stealth"],
|
||||||
|
"default": "standard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"CreateWalletResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"wallet": { "$ref": "#/$defs/Wallet" },
|
||||||
|
"mnemonic": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "BIP39 mnemonic phrase (24 words)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["wallet", "mnemonic"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ImportWalletRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mnemonic": { "type": "string" },
|
||||||
|
"passphrase": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["mnemonic"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"StealthAddress": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"viewKey": { "$ref": "common.json#/$defs/PublicKey" },
|
||||||
|
"spendKey": { "$ref": "common.json#/$defs/PublicKey" },
|
||||||
|
"ephemeralKey": { "$ref": "common.json#/$defs/PublicKey" }
|
||||||
|
},
|
||||||
|
"required": ["address", "viewKey", "spendKey"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TransactionInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txid": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"vout": { "type": "integer", "minimum": 0 },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["txid", "vout", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TransactionOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"amount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["address", "amount"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Transaction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "integer" },
|
||||||
|
"inputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/TransactionInput" }
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/TransactionOutput" }
|
||||||
|
},
|
||||||
|
"lockTime": { "type": "integer" },
|
||||||
|
"fee": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["version", "inputs", "outputs"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignedTransaction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"raw": { "type": "string", "description": "Hex-encoded signed transaction" },
|
||||||
|
"txid": { "$ref": "common.json#/$defs/TxHash" },
|
||||||
|
"size": { "type": "integer" },
|
||||||
|
"weight": { "type": "integer" }
|
||||||
|
},
|
||||||
|
"required": ["raw", "txid"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignMessageRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["text", "hex", "base64"],
|
||||||
|
"default": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["message"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignMessageResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"signature": { "$ref": "common.json#/$defs/Signature" },
|
||||||
|
"publicKey": { "$ref": "common.json#/$defs/PublicKey" },
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" }
|
||||||
|
},
|
||||||
|
"required": ["signature", "publicKey", "address"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetBalanceRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"includeTokens": { "type": "boolean", "default": false }
|
||||||
|
},
|
||||||
|
"required": ["address"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"TokenBalance": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"token": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"symbol": { "type": "string" },
|
||||||
|
"decimals": { "type": "integer" },
|
||||||
|
"balance": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["token", "balance"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetBalanceResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"native": { "$ref": "common.json#/$defs/Balance" },
|
||||||
|
"tokens": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/TokenBalance" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["native"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetUtxosRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": { "$ref": "common.json#/$defs/Address" },
|
||||||
|
"minConfirmations": { "type": "integer", "default": 1 },
|
||||||
|
"minAmount": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["address"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"GetUtxosResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"utxos": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "common.json#/$defs/UTXO" }
|
||||||
|
},
|
||||||
|
"total": { "$ref": "common.json#/$defs/Amount" }
|
||||||
|
},
|
||||||
|
"required": ["utxos", "total"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue