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 (
|
||||
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",
|
||||
"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",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
@ -10,28 +10,52 @@
|
|||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"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": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
||||
"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 src/compute/index.ts src/wallet/index.ts src/rpc/index.ts src/storage/index.ts --format cjs,esm --dts --watch",
|
||||
"test": "vitest",
|
||||
"lint": "biome check src/",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"synor",
|
||||
"blockchain",
|
||||
"compute",
|
||||
"wallet",
|
||||
"rpc",
|
||||
"storage",
|
||||
"ipfs",
|
||||
"gpu",
|
||||
"tpu",
|
||||
"npu",
|
||||
"ai",
|
||||
"ml",
|
||||
"distributed-computing",
|
||||
"heterogeneous-compute"
|
||||
"distributed-computing"
|
||||
],
|
||||
"author": "Synor Team",
|
||||
"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]
|
||||
name = "synor-compute"
|
||||
name = "synor-sdk"
|
||||
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"
|
||||
license = { text = "MIT" }
|
||||
authors = [
|
||||
{ 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 = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
|
|
@ -17,12 +17,14 @@ classifiers = [
|
|||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"httpx>=0.25.0",
|
||||
"numpy>=1.24.0",
|
||||
"pydantic>=2.0.0",
|
||||
"websockets>=12.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
@ -35,8 +37,9 @@ dev = [
|
|||
|
||||
[project.urls]
|
||||
Homepage = "https://synor.cc"
|
||||
Documentation = "https://docs.synor.cc/compute"
|
||||
Documentation = "https://docs.synor.cc/sdk"
|
||||
Repository = "https://github.com/synor/synor"
|
||||
Changelog = "https://github.com/synor/synor/blob/main/CHANGELOG.md"
|
||||
|
||||
[build-system]
|
||||
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]
|
||||
name = "synor-compute"
|
||||
version = "0.1.0"
|
||||
|
|
@ -10,7 +12,7 @@ keywords = ["compute", "gpu", "ai", "ml", "distributed"]
|
|||
categories = ["api-bindings", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.11", features = ["json", "stream", "multipart"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
@ -19,6 +21,8 @@ thiserror = "1"
|
|||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
rand = "0.8"
|
||||
base64 = "0.21"
|
||||
urlencoding = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ mod tensor;
|
|||
mod client;
|
||||
mod error;
|
||||
|
||||
pub mod wallet;
|
||||
pub mod rpc;
|
||||
pub mod storage;
|
||||
|
||||
#[cfg(test)]
|
||||
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 transposed = matrix.transpose();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct Tensor {
|
||||
shape: Vec<usize>,
|
||||
data: Vec<f64>,
|
||||
#[serde(default)]
|
||||
dtype: Precision,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ impl Default for AttentionOptions {
|
|||
num_heads: 8,
|
||||
flash: true,
|
||||
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