diff --git a/sdk/go/go.mod b/sdk/go/go.mod index 1b08db0..b93808d 100644 --- a/sdk/go/go.mod +++ b/sdk/go/go.mod @@ -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 diff --git a/sdk/go/rpc/rpc.go b/sdk/go/rpc/rpc.go new file mode 100644 index 0000000..d099617 --- /dev/null +++ b/sdk/go/rpc/rpc.go @@ -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) +} diff --git a/sdk/go/storage/storage.go b/sdk/go/storage/storage.go new file mode 100644 index 0000000..ce99cfb --- /dev/null +++ b/sdk/go/storage/storage.go @@ -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) +} diff --git a/sdk/go/wallet/wallet.go b/sdk/go/wallet/wallet.go new file mode 100644 index 0000000..4bf121d --- /dev/null +++ b/sdk/go/wallet/wallet.go @@ -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) +} diff --git a/sdk/js/package.json b/sdk/js/package.json index e7522cf..a646db0 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -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", diff --git a/sdk/js/src/rpc/client.ts b/sdk/js/src/rpc/client.ts new file mode 100644 index 0000000..4f69d1f --- /dev/null +++ b/sdk/js/src/rpc/client.ts @@ -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; + private ws: WebSocket | null = null; + private subscriptions: Map 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 { + const path = typeof hashOrHeight === 'number' + ? `/blocks/height/${hashOrHeight}` + : `/blocks/${hashOrHeight}`; + return this.request('GET', path); + } + + /** + * Get latest block. + */ + async getLatestBlock(): Promise { + return this.request('GET', '/blocks/latest'); + } + + /** + * Get block header only. + */ + async getBlockHeader(hashOrHeight: string | number): Promise { + 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 { + 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 { + return this.request('GET', `/transactions/${txid}`); + } + + /** + * Get raw transaction hex. + */ + async getRawTransaction(txid: string): Promise { + const response = await this.request('GET', `/transactions/${txid}/raw`); + return response.raw; + } + + /** + * Send a raw transaction. + */ + async sendRawTransaction(rawTx: string): Promise { + 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 { + const params: Record = {}; + 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 { + return this.request('GET', '/fees/estimate', undefined, { priority }); + } + + /** + * Get all fee estimates. + */ + async getAllFeeEstimates(): Promise { + const response = await this.request('GET', '/fees/estimate/all'); + return response.estimates; + } + + // ==================== Chain Information ==================== + + /** + * Get chain information. + */ + async getChainInfo(): Promise { + return this.request('GET', '/chain'); + } + + /** + * Get mempool information. + */ + async getMempoolInfo(): Promise { + return this.request('GET', '/mempool'); + } + + /** + * Get mempool transactions. + */ + async getMempoolTransactions(limit: number = 100): Promise { + 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 { + 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 { + 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 { + return this.subscribe('address', (notification) => { + if (notification.type === 'transaction') { + callback(notification.transaction); + } + }, { address }); + } + + /** + * Subscribe to mempool transactions. + */ + async subscribeMempool(callback: (tx: Transaction) => void): Promise { + return this.subscribe('mempool', (notification) => { + if (notification.type === 'transaction') { + callback(notification.transaction); + } + }); + } + + private async subscribe( + type: SubscriptionType, + callback: (notification: Notification) => void, + params?: Record + ): Promise { + 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 { + 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 + ): Promise { + 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(); + } +} diff --git a/sdk/js/src/rpc/index.ts b/sdk/js/src/rpc/index.ts new file mode 100644 index 0000000..4124218 --- /dev/null +++ b/sdk/js/src/rpc/index.ts @@ -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'; diff --git a/sdk/js/src/rpc/types.ts b/sdk/js/src/rpc/types.ts new file mode 100644 index 0000000..20ef3ae --- /dev/null +++ b/sdk/js/src/rpc/types.ts @@ -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; diff --git a/sdk/js/src/storage/client.ts b/sdk/js/src/storage/client.ts new file mode 100644 index 0000000..302dc2f --- /dev/null +++ b/sdk/js/src/storage/client.ts @@ -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> & Pick; + + 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 { + 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 { + // 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 { + 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> { + 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 { + const response = await this.request('POST', '/pins', request); + return response as Pin; + } + + /** + * Unpin content by CID. + */ + async unpin(cid: string): Promise { + await this.request('DELETE', `/pins/${cid}`); + } + + /** + * Get pin status by CID. + */ + async getPinStatus(cid: string): Promise { + const response = await this.request('GET', `/pins/${cid}`); + return response as Pin; + } + + /** + * List pins. + */ + async listPins(options: ListPinsOptions = {}): Promise { + 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 { + 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 { + 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 { + 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 { + const response = await this.request('POST', '/directory', { files }); + return response as UploadResponse; + } + + /** + * List directory contents. + */ + async listDirectory(cid: string, path?: string): Promise { + 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 { + const response = await this.request('GET', '/stats'); + return response as StorageStats; + } + + /** + * Check if content exists. + */ + async exists(cid: string): Promise { + 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 { + 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 { + const headers: Record = { + 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), + }, + 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 { + 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'; + } +} diff --git a/sdk/js/src/storage/index.ts b/sdk/js/src/storage/index.ts new file mode 100644 index 0000000..4e143bb --- /dev/null +++ b/sdk/js/src/storage/index.ts @@ -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'; diff --git a/sdk/js/src/storage/types.ts b/sdk/js/src/storage/types.ts new file mode 100644 index 0000000..a1217eb --- /dev/null +++ b/sdk/js/src/storage/types.ts @@ -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; +} diff --git a/sdk/js/src/wallet/client.ts b/sdk/js/src/wallet/client.ts new file mode 100644 index 0000000..107225c --- /dev/null +++ b/sdk/js/src/wallet/client.ts @@ -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> & { 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 { + 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 { + 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 { + const response = await this.request('GET', `/wallets/${walletId}`); + return response.wallet; + } + + /** + * List all wallets for this account. + * + * @returns List of wallets + */ + async listWallets(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const params: Record = {}; + 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 { + 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 { + 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 { + 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 { + 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 + ): Promise { + 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(); + } +} diff --git a/sdk/js/src/wallet/index.ts b/sdk/js/src/wallet/index.ts new file mode 100644 index 0000000..dba5612 --- /dev/null +++ b/sdk/js/src/wallet/index.ts @@ -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'; diff --git a/sdk/js/src/wallet/types.ts b/sdk/js/src/wallet/types.ts new file mode 100644 index 0000000..faab62f --- /dev/null +++ b/sdk/js/src/wallet/types.ts @@ -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; +} diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index f0bf2ec..ca0b4f3 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -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"] diff --git a/sdk/python/synor_rpc/__init__.py b/sdk/python/synor_rpc/__init__.py new file mode 100644 index 0000000..e3e0a97 --- /dev/null +++ b/sdk/python/synor_rpc/__init__.py @@ -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" diff --git a/sdk/python/synor_rpc/client.py b/sdk/python/synor_rpc/client.py new file mode 100644 index 0000000..a04fe82 --- /dev/null +++ b/sdk/python/synor_rpc/client.py @@ -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"), + ) diff --git a/sdk/python/synor_rpc/types.py b/sdk/python/synor_rpc/types.py new file mode 100644 index 0000000..80a8afa --- /dev/null +++ b/sdk/python/synor_rpc/types.py @@ -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] diff --git a/sdk/python/synor_storage/__init__.py b/sdk/python/synor_storage/__init__.py new file mode 100644 index 0000000..85335ae --- /dev/null +++ b/sdk/python/synor_storage/__init__.py @@ -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" diff --git a/sdk/python/synor_storage/client.py b/sdk/python/synor_storage/client.py new file mode 100644 index 0000000..a8a881e --- /dev/null +++ b/sdk/python/synor_storage/client.py @@ -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", []), + ) diff --git a/sdk/python/synor_storage/types.py b/sdk/python/synor_storage/types.py new file mode 100644 index 0000000..36b56af --- /dev/null +++ b/sdk/python/synor_storage/types.py @@ -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 diff --git a/sdk/python/synor_wallet/__init__.py b/sdk/python/synor_wallet/__init__.py new file mode 100644 index 0000000..3b2c601 --- /dev/null +++ b/sdk/python/synor_wallet/__init__.py @@ -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" diff --git a/sdk/python/synor_wallet/client.py b/sdk/python/synor_wallet/client.py new file mode 100644 index 0000000..36b7638 --- /dev/null +++ b/sdk/python/synor_wallet/client.py @@ -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, + } diff --git a/sdk/python/synor_wallet/types.py b/sdk/python/synor_wallet/types.py new file mode 100644 index 0000000..06632f7 --- /dev/null +++ b/sdk/python/synor_wallet/types.py @@ -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 diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml index 4a99f89..1d22bd9 100644 --- a/sdk/rust/Cargo.toml +++ b/sdk/rust/Cargo.toml @@ -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" diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs index b4aad91..171e6a0 100644 --- a/sdk/rust/src/lib.rs +++ b/sdk/rust/src/lib.rs @@ -48,6 +48,10 @@ mod tensor; mod client; mod error; +pub mod wallet; +pub mod rpc; +pub mod storage; + #[cfg(test)] mod tests; diff --git a/sdk/rust/src/rpc/client.rs b/sdk/rust/src/rpc/client.rs new file mode 100644 index 0000000..9d1917d --- /dev/null +++ b/sdk/rust/src/rpc/client.rs @@ -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, +} + +#[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, +} + +impl SynorRpc { + /// Create a new RPC client with the given API key. + pub fn new(api_key: impl Into) -> 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + #[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 { + 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> { + #[derive(Deserialize)] + struct EstimatesResponse { + estimates: Vec, + } + + 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 { + self.request("GET", "/chain", Option::<&()>::None).await + } + + /// Get mempool information. + pub async fn get_mempool_info(&self) -> Result { + self.request("GET", "/mempool", Option::<&()>::None).await + } + + /// Get UTXOs for an address. + pub async fn get_utxos(&self, address: &str) -> Result> { + 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 { + 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> { + #[derive(Deserialize)] + struct BlocksResponse { + blocks: Vec, + } + + 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, + offset: Option, + ) -> Result> { + #[derive(Deserialize)] + struct TxsResponse { + transactions: Vec, + } + + 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) -> Result> { + #[derive(Deserialize)] + struct MempoolTxsResponse { + transactions: Vec, + } + + 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(&self, method: &str, path: &str, body: Option<&B>) -> Result + 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"); + } +} diff --git a/sdk/rust/src/rpc/error.rs b/sdk/rust/src/rpc/error.rs new file mode 100644 index 0000000..a7988d4 --- /dev/null +++ b/sdk/rust/src/rpc/error.rs @@ -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, + }, + /// 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 for RpcError { + fn from(err: reqwest::Error) -> Self { + if err.is_timeout() { + RpcError::Timeout + } else { + RpcError::Request(err.to_string()) + } + } +} + +impl From for RpcError { + fn from(err: serde_json::Error) -> Self { + RpcError::Serialization(err.to_string()) + } +} + +/// Result type alias for RPC operations. +pub type Result = std::result::Result; diff --git a/sdk/rust/src/rpc/mod.rs b/sdk/rust/src/rpc/mod.rs new file mode 100644 index 0000000..b55c4f3 --- /dev/null +++ b/sdk/rust/src/rpc/mod.rs @@ -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> { +//! 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; diff --git a/sdk/rust/src/rpc/types.rs b/sdk/rust/src/rpc/types.rs new file mode 100644 index 0000000..ef360bb --- /dev/null +++ b/sdk/rust/src/rpc/types.rs @@ -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) -> 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) -> Self { + self.endpoint = endpoint.into(); + self + } + + /// Set the WebSocket endpoint. + pub fn ws_endpoint(mut self, ws_endpoint: impl Into) -> 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, + 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, +} + +/// 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, + #[serde(default)] + pub outputs: Vec, + pub block_hash: Option, + pub block_height: Option, + pub timestamp: Option, + pub raw: Option, +} + +/// 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, +} + +/// 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, +} diff --git a/sdk/rust/src/storage/client.rs b/sdk/rust/src/storage/client.rs new file mode 100644 index 0000000..b9fccd0 --- /dev/null +++ b/sdk/rust/src/storage/client.rs @@ -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, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct FileData { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + cid: Option, +} + +#[derive(Debug, Deserialize)] +struct DirectoryResponse { + entries: Vec, +} + +impl SynorStorage { + /// Create a new Storage client with the given API key. + pub fn new(api_key: impl Into) -> 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) -> Result { + 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) -> Result> { + 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 { + 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 { + let path = format!("/pins/{}", cid); + self.request("GET", &path, Option::<&()>::None).await + } + + /// List pins. + pub async fn list_pins(&self, options: Option) -> Result { + 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) -> Result { + let file_data: Vec = 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, + } + + 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 { + #[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> { + 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) -> Result { + let file_data: Vec = 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, + } + + 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> { + 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 { + self.request("GET", "/stats", Option::<&()>::None).await + } + + /// Check if content exists. + pub async fn exists(&self, cid: &str) -> Result { + 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 { + let path = format!("/content/{}/metadata", cid); + self.request("GET", &path, Option::<&()>::None).await + } + + /// Internal HTTP request method. + async fn request(&self, method: &str, path: &str, body: Option<&B>) -> Result + 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())); + } +} diff --git a/sdk/rust/src/storage/error.rs b/sdk/rust/src/storage/error.rs new file mode 100644 index 0000000..bb10f99 --- /dev/null +++ b/sdk/rust/src/storage/error.rs @@ -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, + }, + /// 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 for StorageError { + fn from(err: reqwest::Error) -> Self { + if err.is_timeout() { + StorageError::Timeout + } else { + StorageError::Request(err.to_string()) + } + } +} + +impl From for StorageError { + fn from(err: serde_json::Error) -> Self { + StorageError::Serialization(err.to_string()) + } +} + +impl From for StorageError { + fn from(err: std::io::Error) -> Self { + StorageError::Io(err.to_string()) + } +} + +/// Result type alias for Storage operations. +pub type Result = std::result::Result; diff --git a/sdk/rust/src/storage/mod.rs b/sdk/rust/src/storage/mod.rs new file mode 100644 index 0000000..2154336 --- /dev/null +++ b/sdk/rust/src/storage/mod.rs @@ -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> { +//! 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; diff --git a/sdk/rust/src/storage/types.rs b/sdk/rust/src/storage/types.rs new file mode 100644 index 0000000..09a1197 --- /dev/null +++ b/sdk/rust/src/storage/types.rs @@ -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, + 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) -> 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) -> Self { + self.endpoint = endpoint.into(); + self + } + + /// Set the gateway. + pub fn gateway(mut self, gateway: impl Into) -> Self { + self.gateway = gateway.into(); + self + } + + /// Set the pinning service. + pub fn pinning_service(mut self, service: impl Into) -> 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, + pub hash: Option, +} + +/// Download options. +#[derive(Debug, Clone, Default)] +pub struct DownloadOptions { + pub offset: Option, + pub length: Option, +} + +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, + pub size: Option, + pub created_at: Option, + pub expires_at: Option, + #[serde(default)] + pub delegates: Vec, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub origins: Vec, +} + +impl PinRequest { + /// Create a new pin request. + pub fn new(cid: impl Into) -> Self { + Self { + cid: cid.into(), + name: None, + duration: None, + origins: Vec::new(), + } + } + + /// Set the name. + pub fn name(mut self, name: impl Into) -> 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) -> Self { + self.origins = origins; + self + } +} + +/// List pins options. +#[derive(Debug, Clone, Default)] +pub struct ListPinsOptions { + pub status: Vec, + pub match_type: Option, + pub name: Option, + pub limit: Option, + pub offset: Option, +} + +/// List pins response. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListPinsResponse { + pub pins: Vec, + pub total: i32, + pub has_more: bool, +} + +/// Gateway URL. +#[derive(Debug, Clone)] +pub struct GatewayUrl { + pub url: String, + pub cid: String, + pub path: Option, +} + +/// 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, +} + +/// CAR file. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CarFile { + pub version: i32, + pub roots: Vec, + #[serde(default)] + pub blocks: Vec, + pub size: Option, +} + +/// File entry for directory creation. +#[derive(Debug, Clone)] +pub struct FileEntry { + pub name: String, + pub content: Option>, + pub cid: Option, +} + +impl FileEntry { + /// Create a new file entry. + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + content: None, + cid: None, + } + } + + /// Set the content. + pub fn content(mut self, content: Vec) -> Self { + self.content = Some(content); + self + } + + /// Set the CID. + pub fn cid(mut self, cid: impl Into) -> 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, + #[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, + 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, +} + +/// Bandwidth statistics. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BandwidthStats { + pub upload: i64, + pub download: i64, +} diff --git a/sdk/rust/src/tensor.rs b/sdk/rust/src/tensor.rs index 53ebdc4..17db84a 100644 --- a/sdk/rust/src/tensor.rs +++ b/sdk/rust/src/tensor.rs @@ -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, data: Vec, + #[serde(default)] dtype: Precision, } diff --git a/sdk/rust/src/types.rs b/sdk/rust/src/types.rs index 79d191d..b7a3bb9 100644 --- a/sdk/rust/src/types.rs +++ b/sdk/rust/src/types.rs @@ -196,7 +196,7 @@ impl Default for AttentionOptions { num_heads: 8, flash: true, precision: Precision::FP16, - processor: ProcessorType::GPU, + processor: ProcessorType::Gpu, } } } diff --git a/sdk/rust/src/wallet/client.rs b/sdk/rust/src/wallet/client.rs new file mode 100644 index 0000000..6811d98 --- /dev/null +++ b/sdk/rust/src/wallet/client.rs @@ -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, +} + +impl SynorWallet { + /// Create a new client with an API key. + pub fn new(api_key: impl Into) -> 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 { + 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 { + 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 { + 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> { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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) -> Result> { + 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 { + 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 { + 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 { + 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> { + 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(&self, path: &str) -> Result { + 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(&self, path: &str, body: Value) -> Result { + 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?) + } +} diff --git a/sdk/rust/src/wallet/error.rs b/sdk/rust/src/wallet/error.rs new file mode 100644 index 0000000..c0faf5d --- /dev/null +++ b/sdk/rust/src/wallet/error.rs @@ -0,0 +1,72 @@ +//! Synor Wallet SDK error types. + +use std::fmt; + +/// Result type for wallet operations. +pub type Result = std::result::Result; + +/// 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, + }, + /// 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 for WalletError { + fn from(error: reqwest::Error) -> Self { + WalletError::Http(error) + } +} + +impl From for WalletError { + fn from(error: serde_json::Error) -> Self { + WalletError::Serialization(error) + } +} diff --git a/sdk/rust/src/wallet/mod.rs b/sdk/rust/src/wallet/mod.rs new file mode 100644 index 0000000..1069885 --- /dev/null +++ b/sdk/rust/src/wallet/mod.rs @@ -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> { +//! 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::*; diff --git a/sdk/rust/src/wallet/types.rs b/sdk/rust/src/wallet/types.rs new file mode 100644 index 0000000..97f1fe6 --- /dev/null +++ b/sdk/rust/src/wallet/types.rs @@ -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, +} + +impl WalletConfig { + /// Create a new configuration with an API key. + pub fn new(api_key: impl Into) -> 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) -> 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) -> 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, +} + +/// 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, +} + +/// 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, +} + +/// Unsigned transaction. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Transaction { + /// Version. + pub version: u32, + /// Inputs. + pub inputs: Vec, + /// Outputs. + pub outputs: Vec, + /// Lock time. + #[serde(default)] + pub lock_time: u32, + /// Fee. + pub fee: Option, +} + +/// 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, +} + +/// 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, +} + +/// 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, +} + +/// 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, + /// Wallet type. + pub wallet_type: WalletType, +} + +impl ImportWalletOptions { + /// Create new import options with a mnemonic. + pub fn new(mnemonic: impl Into) -> Self { + Self { + mnemonic: mnemonic.into(), + passphrase: None, + wallet_type: WalletType::default(), + } + } + + /// Set the passphrase. + pub fn passphrase(mut self, passphrase: impl Into) -> 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, + /// Specific UTXOs to use. + pub utxos: Option>, + /// Change address. + pub change_address: Option, +} + +impl BuildTransactionOptions { + /// Create new build options. + pub fn new(to: impl Into, amount: impl Into) -> 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) -> Self { + self.utxos = Some(utxos); + self + } + + /// Set the change address. + pub fn change_address(mut self, address: impl Into) -> 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, + /// Minimum amount. + pub min_amount: Option, +} + +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) -> Self { + self.min_amount = Some(amount.into()); + self + } +} diff --git a/sdk/shared/schemas/bridge.json b/sdk/shared/schemas/bridge.json new file mode 100644 index 0000000..a2e6b6a --- /dev/null +++ b/sdk/shared/schemas/bridge.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/common.json b/sdk/shared/schemas/common.json new file mode 100644 index 0000000..b5b6962 --- /dev/null +++ b/sdk/shared/schemas/common.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/contract.json b/sdk/shared/schemas/contract.json new file mode 100644 index 0000000..f37020b --- /dev/null +++ b/sdk/shared/schemas/contract.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/database.json b/sdk/shared/schemas/database.json new file mode 100644 index 0000000..19c89f4 --- /dev/null +++ b/sdk/shared/schemas/database.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/economics.json b/sdk/shared/schemas/economics.json new file mode 100644 index 0000000..ca24db5 --- /dev/null +++ b/sdk/shared/schemas/economics.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/governance.json b/sdk/shared/schemas/governance.json new file mode 100644 index 0000000..947955e --- /dev/null +++ b/sdk/shared/schemas/governance.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/hosting.json b/sdk/shared/schemas/hosting.json new file mode 100644 index 0000000..31d0a20 --- /dev/null +++ b/sdk/shared/schemas/hosting.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/mining.json b/sdk/shared/schemas/mining.json new file mode 100644 index 0000000..4f02671 --- /dev/null +++ b/sdk/shared/schemas/mining.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/privacy.json b/sdk/shared/schemas/privacy.json new file mode 100644 index 0000000..c193dee --- /dev/null +++ b/sdk/shared/schemas/privacy.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/rpc.json b/sdk/shared/schemas/rpc.json new file mode 100644 index 0000000..62295f0 --- /dev/null +++ b/sdk/shared/schemas/rpc.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/storage.json b/sdk/shared/schemas/storage.json new file mode 100644 index 0000000..b4beded --- /dev/null +++ b/sdk/shared/schemas/storage.json @@ -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"] + } + } +} diff --git a/sdk/shared/schemas/wallet.json b/sdk/shared/schemas/wallet.json new file mode 100644 index 0000000..27cb739 --- /dev/null +++ b/sdk/shared/schemas/wallet.json @@ -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"] + } + } +}