package chain import ( "bytes" "encoding/json" "fmt" "io" "net/http" "sync/atomic" "time" ) type rpcRequest struct { JSONRPC string `json:"jsonrpc"` ID int64 `json:"id"` Method string `json:"method"` Params any `json:"params"` } type rpcResponse struct { JSONRPC string `json:"jsonrpc"` ID any `json:"id"` Result json.RawMessage `json:"result,omitempty"` Error *rpcError `json:"error,omitempty"` } type rpcError struct { Code int `json:"code"` Message string `json:"message"` } func (e *rpcError) Error() string { return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message) } // Client is a JSON-RPC 2.0 client for the TOL Chain node. type Client struct { nodeURL string http *http.Client idSeq atomic.Int64 } func NewClient(nodeURL string) *Client { return &Client{ nodeURL: nodeURL, http: &http.Client{Timeout: 10 * time.Second}, } } // Call invokes a JSON-RPC method and unmarshals the result into out. func (c *Client) Call(method string, params any, out any) error { reqBody := rpcRequest{ JSONRPC: "2.0", ID: c.idSeq.Add(1), Method: method, Params: params, } data, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("marshal RPC request: %w", err) } resp, err := c.http.Post(c.nodeURL, "application/json", bytes.NewReader(data)) if err != nil { return fmt.Errorf("RPC network error: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("RPC HTTP error: status %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) if err != nil { return fmt.Errorf("read RPC response: %w", err) } var rpcResp rpcResponse if err := json.Unmarshal(body, &rpcResp); err != nil { return fmt.Errorf("unmarshal RPC response: %w", err) } if rpcResp.Error != nil { return rpcResp.Error } if out != nil { if err := json.Unmarshal(rpcResp.Result, out); err != nil { return fmt.Errorf("unmarshal RPC result: %w", err) } } return nil } // --- Typed convenience methods --- type BalanceResult struct { Address string `json:"address"` Balance uint64 `json:"balance"` Nonce uint64 `json:"nonce"` } func (c *Client) GetBalance(address string) (*BalanceResult, error) { var result BalanceResult err := c.Call("getBalance", map[string]string{"address": address}, &result) return &result, err } func (c *Client) GetAsset(id string) (json.RawMessage, error) { var result json.RawMessage err := c.Call("getAsset", map[string]string{"id": id}, &result) return result, err } func (c *Client) GetAssetsByOwner(owner string, offset, limit int) (json.RawMessage, error) { var result json.RawMessage err := c.Call("getAssetsByOwner", map[string]any{ "owner": owner, "offset": offset, "limit": limit, }, &result) return result, err } func (c *Client) GetInventory(owner string) (json.RawMessage, error) { var result json.RawMessage err := c.Call("getInventory", map[string]string{"owner": owner}, &result) return result, err } func (c *Client) GetActiveListings(offset, limit int) (json.RawMessage, error) { var result json.RawMessage err := c.Call("getActiveListings", map[string]any{ "offset": offset, "limit": limit, }, &result) return result, err } func (c *Client) GetListing(id string) (json.RawMessage, error) { var result json.RawMessage err := c.Call("getListing", map[string]string{"id": id}, &result) return result, err } type SendTxResult struct { TxID string `json:"tx_id"` } func (c *Client) SendTx(tx any) (*SendTxResult, error) { var result SendTxResult err := c.Call("sendTx", tx, &result) return &result, err } // TxStatusResult mirrors the indexer.TxResult from the TOL Chain node. type TxStatusResult struct { TxID string `json:"tx_id"` BlockHeight int64 `json:"block_height"` Success bool `json:"success"` Error string `json:"error"` } // GetTxStatus queries the execution result of a transaction. // Returns nil result (no error) if the transaction has not been included in a block yet. func (c *Client) GetTxStatus(txID string) (*TxStatusResult, error) { var result *TxStatusResult err := c.Call("getTxStatus", map[string]string{"tx_id": txID}, &result) if err != nil { return nil, err } return result, nil } // TxError is returned when a transaction was included in a block but execution failed. type TxError struct { TxID string Message string } func (e *TxError) Error() string { return fmt.Sprintf("transaction %s failed: %s", e.TxID, e.Message) } // DefaultTxTimeout is the default timeout for WaitForTx. PoA block intervals // are typically a few seconds, so 15s provides ample margin. const DefaultTxTimeout = 15 * time.Second // SendTxAndWait sends a transaction and waits for block confirmation. // It combines SendTx + WaitForTx for the common fire-and-confirm pattern. func (c *Client) SendTxAndWait(tx any, timeout time.Duration) (*TxStatusResult, error) { sendResult, err := c.SendTx(tx) if err != nil { return nil, fmt.Errorf("send tx: %w", err) } return c.WaitForTx(sendResult.TxID, timeout) } // WaitForTx polls getTxStatus until the transaction is included in a block or // the timeout is reached. It returns the confirmed TxStatusResult on success, // a TxError if the transaction executed but failed, or a timeout error. func (c *Client) WaitForTx(txID string, timeout time.Duration) (*TxStatusResult, error) { deadline := time.Now().Add(timeout) interval := 200 * time.Millisecond for { result, err := c.GetTxStatus(txID) if err != nil { return nil, fmt.Errorf("getTxStatus: %w", err) } if result != nil { if !result.Success { return result, &TxError{TxID: txID, Message: result.Error} } return result, nil } if time.Now().After(deadline) { return nil, fmt.Errorf("transaction %s not confirmed within %s", txID, timeout) } time.Sleep(interval) // Increase interval up to 1s to reduce polling pressure. if interval < time.Second { interval = interval * 3 / 2 if interval > time.Second { interval = time.Second } } } }