* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+351
View File
@@ -0,0 +1,351 @@
package client
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)
// StarshipClient provides HTTP client for Starship REST API
type StarshipClient struct {
baseURL string
httpClient *http.Client
}
// NewStarshipClient creates a new Starship HTTP client
func NewStarshipClient(baseURL string) *StarshipClient {
return &StarshipClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// ChainQueryResponse represents common chain query response structure
type ChainQueryResponse struct {
Height string `json:"height"`
Result json.RawMessage `json:"result"`
}
// BalanceResponse represents balance query response
type BalanceResponse struct {
Balance sdk.Coin `json:"balance"`
}
// GetBalance queries the balance of an account
func (c *StarshipClient) GetBalance(ctx context.Context, address, denom string) (math.Int, error) {
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s/by_denom?denom=%s", c.baseURL, address, denom)
var balanceResp BalanceResponse
if err := c.doRequest(ctx, url, &balanceResp); err != nil {
return math.ZeroInt(), fmt.Errorf("failed to query balance: %w", err)
}
return balanceResp.Balance.Amount, nil
}
// AllBalancesResponse represents all balances query response
type AllBalancesResponse struct {
Balances []sdk.Coin `json:"balances"`
Pagination struct {
NextKey string `json:"next_key"`
Total string `json:"total"`
} `json:"pagination"`
}
// GetAllBalances queries all balances of an account
func (c *StarshipClient) GetAllBalances(ctx context.Context, address string) ([]sdk.Coin, error) {
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s", c.baseURL, address)
var balancesResp AllBalancesResponse
if err := c.doRequest(ctx, url, &balancesResp); err != nil {
return nil, fmt.Errorf("failed to query all balances: %w", err)
}
return balancesResp.Balances, nil
}
// SupplyResponse represents supply query response
type SupplyResponse struct {
Amount sdk.Coin `json:"amount"`
}
// GetSupply queries the total supply of a denomination
func (c *StarshipClient) GetSupply(ctx context.Context, denom string) (math.Int, error) {
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/supply/by_denom?denom=%s", c.baseURL, denom)
var supplyResp SupplyResponse
if err := c.doRequest(ctx, url, &supplyResp); err != nil {
return math.ZeroInt(), fmt.Errorf("failed to query supply: %w", err)
}
return supplyResp.Amount.Amount, nil
}
// BankParamsResponse represents bank params query response
type BankParamsResponse struct {
Params banktypes.Params `json:"params"`
}
// GetBankParams queries bank module parameters
func (c *StarshipClient) GetBankParams(ctx context.Context) (*banktypes.Params, error) {
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/params", c.baseURL)
var paramsResp BankParamsResponse
if err := c.doRequest(ctx, url, &paramsResp); err != nil {
return nil, fmt.Errorf("failed to query bank params: %w", err)
}
return &paramsResp.Params, nil
}
// NodeInfoResponse represents node info query response
type NodeInfoResponse struct {
DefaultNodeInfo struct {
Network string `json:"network"`
Version string `json:"version"`
Moniker string `json:"moniker"`
} `json:"default_node_info"`
ApplicationVersion struct {
Name string `json:"name"`
AppName string `json:"app_name"`
Version string `json:"version"`
GitCommit string `json:"git_commit"`
} `json:"application_version"`
}
// GetNodeInfo queries node information
func (c *StarshipClient) GetNodeInfo(ctx context.Context) (*NodeInfoResponse, error) {
url := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/node_info", c.baseURL)
var nodeInfo NodeInfoResponse
if err := c.doRequest(ctx, url, &nodeInfo); err != nil {
return nil, fmt.Errorf("failed to query node info: %w", err)
}
return &nodeInfo, nil
}
// doRequest performs HTTP GET request with retry logic
func (c *StarshipClient) doRequest(ctx context.Context, url string, target any) error {
const maxRetries = 3
const retryDelay = 2 * time.Second
for attempt := 0; attempt < maxRetries; attempt++ {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
if attempt == maxRetries-1 {
return fmt.Errorf("request failed after %d attempts: %w", maxRetries, err)
}
time.Sleep(retryDelay)
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if attempt == maxRetries-1 {
return fmt.Errorf("request failed with status %d", resp.StatusCode)
}
time.Sleep(retryDelay)
continue
}
if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
return nil
}
return fmt.Errorf("unreachable code")
}
// EventSearchResponse represents event search response
type EventSearchResponse struct {
Events []EventResult `json:"events"`
Pagination struct {
NextKey string `json:"next_key"`
Total string `json:"total"`
} `json:"pagination"`
}
// EventResult represents a single event result
type EventResult struct {
Type string `json:"type"`
Attributes []sdk.Attribute `json:"attributes"`
Height string `json:"height"`
TxHash string `json:"tx_hash"`
}
// BlockEventsResponse represents block events response
type BlockEventsResponse struct {
Height string `json:"height"`
BeginBlockEvents []sdk.Event `json:"begin_block_events"`
EndBlockEvents []sdk.Event `json:"end_block_events"`
TxEvents []TxEvents `json:"tx_events"`
}
// TxEvents represents transaction events
type TxEvents struct {
TxHash string `json:"tx_hash"`
Events []sdk.Event `json:"events"`
}
// QueryEventsByHeight queries events by block height
func (c *StarshipClient) QueryEventsByHeight(ctx context.Context, height int64) (*BlockEventsResponse, error) {
url := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/blocks/%d/events", c.baseURL, height)
var eventsResp BlockEventsResponse
if err := c.doRequest(ctx, url, &eventsResp); err != nil {
return nil, fmt.Errorf("failed to query events by height: %w", err)
}
return &eventsResp, nil
}
// QueryEventsByType queries events by event type
func (c *StarshipClient) QueryEventsByType(ctx context.Context, eventType string, minHeight, maxHeight int64) (*EventSearchResponse, error) {
query := fmt.Sprintf("message.action='%s'", eventType)
return c.SearchEvents(ctx, query, minHeight, maxHeight)
}
// QueryEventsByAttribute queries events by attribute key-value pair
func (c *StarshipClient) QueryEventsByAttribute(ctx context.Context, key, value string, minHeight, maxHeight int64) (*EventSearchResponse, error) {
query := fmt.Sprintf("%s='%s'", key, value)
return c.SearchEvents(ctx, query, minHeight, maxHeight)
}
// SearchEvents performs a general event search with CometBFT query syntax
func (c *StarshipClient) SearchEvents(ctx context.Context, query string, minHeight, maxHeight int64) (*EventSearchResponse, error) {
queryParams := url.Values{}
queryParams.Add("query", query)
if minHeight > 0 {
queryParams.Add("min_height", strconv.FormatInt(minHeight, 10))
}
if maxHeight > 0 {
queryParams.Add("max_height", strconv.FormatInt(maxHeight, 10))
}
searchURL := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/events?%s", c.baseURL, queryParams.Encode())
var eventsResp EventSearchResponse
if err := c.doRequest(ctx, searchURL, &eventsResp); err != nil {
return nil, fmt.Errorf("failed to search events: %w", err)
}
return &eventsResp, nil
}
// GetLatestBlockHeight gets the latest block height
func (c *StarshipClient) GetLatestBlockHeight(ctx context.Context) (int64, error) {
url := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/blocks/latest", c.baseURL)
var blockResp struct {
Block struct {
Header struct {
Height string `json:"height"`
} `json:"header"`
} `json:"block"`
}
if err := c.doRequest(ctx, url, &blockResp); err != nil {
return 0, fmt.Errorf("failed to get latest block height: %w", err)
}
height, err := strconv.ParseInt(blockResp.Block.Header.Height, 10, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse block height: %w", err)
}
return height, nil
}
// WaitForNextBlock waits for the next block to be produced
func (c *StarshipClient) WaitForNextBlock(ctx context.Context) (int64, error) {
currentHeight, err := c.GetLatestBlockHeight(ctx)
if err != nil {
return 0, err
}
targetHeight := currentHeight + 1
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return 0, ctx.Err()
case <-ticker.C:
height, err := c.GetLatestBlockHeight(ctx)
if err != nil {
continue
}
if height >= targetHeight {
return height, nil
}
}
}
}
// FilterEventsByType filters events by type from a transaction response
func FilterEventsByType(events []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
}, eventType string) []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
} {
var filtered []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
}
for _, event := range events {
if strings.Contains(event.Type, eventType) {
filtered = append(filtered, event)
}
}
return filtered
}
// GetEventAttribute gets a specific attribute value from an event
func GetEventAttribute(event struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
}, key string,
) (string, bool) {
for _, attr := range event.Attributes {
if attr.Key == key {
return attr.Value, true
}
}
return "", false
}
+162
View File
@@ -0,0 +1,162 @@
package client
import (
"context"
"fmt"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported"
)
// ChannelResponse represents IBC channel query response
type ChannelResponse struct {
Channel channeltypes.Channel `json:"channel"`
Proof []byte `json:"proof"`
ProofHeight struct {
RevisionNumber string `json:"revision_number"`
RevisionHeight string `json:"revision_height"`
} `json:"proof_height"`
}
// ChannelsResponse represents IBC channels query response
type ChannelsResponse struct {
Channels []struct {
State string `json:"state"`
Ordering string `json:"ordering"`
Counterparty struct {
PortID string `json:"port_id"`
ChannelID string `json:"channel_id"`
} `json:"counterparty"`
ConnectionHops []string `json:"connection_hops"`
Version string `json:"version"`
PortID string `json:"port_id"`
ChannelID string `json:"channel_id"`
} `json:"channels"`
Pagination struct {
NextKey string `json:"next_key"`
Total string `json:"total"`
} `json:"pagination"`
Height struct {
RevisionNumber string `json:"revision_number"`
RevisionHeight string `json:"revision_height"`
} `json:"height"`
}
// GetChannel queries an IBC channel
func (c *StarshipClient) GetChannel(ctx context.Context, portID, channelID string) (*ChannelResponse, error) {
url := fmt.Sprintf("%s/ibc/core/channel/v1/channels/%s/ports/%s", c.baseURL, channelID, portID)
var channelResp ChannelResponse
if err := c.doRequest(ctx, url, &channelResp); err != nil {
return nil, fmt.Errorf("failed to query channel: %w", err)
}
return &channelResp, nil
}
// GetChannels queries all IBC channels
func (c *StarshipClient) GetChannels(ctx context.Context) (*ChannelsResponse, error) {
url := fmt.Sprintf("%s/ibc/core/channel/v1/channels", c.baseURL)
var channelsResp ChannelsResponse
if err := c.doRequest(ctx, url, &channelsResp); err != nil {
return nil, fmt.Errorf("failed to query channels: %w", err)
}
return &channelsResp, nil
}
// GetTransferChannel finds the first open transfer channel
func (c *StarshipClient) GetTransferChannel(ctx context.Context) (string, error) {
channels, err := c.GetChannels(ctx)
if err != nil {
return "", fmt.Errorf("failed to get channels: %w", err)
}
for _, channel := range channels.Channels {
if channel.PortID == "transfer" && channel.State == "STATE_OPEN" {
return channel.ChannelID, nil
}
}
return "", fmt.Errorf("no open transfer channel found")
}
// ConnectionResponse represents IBC connection query response
type ConnectionResponse struct {
Connection struct {
ClientID string `json:"client_id"`
Versions []struct {
Identifier string `json:"identifier"`
Features []string `json:"features"`
} `json:"versions"`
State string `json:"state"`
Counterparty struct {
ClientID string `json:"client_id"`
ConnectionID string `json:"connection_id"`
Prefix struct {
KeyPrefix []byte `json:"key_prefix"`
} `json:"prefix"`
} `json:"counterparty"`
DelayPeriod string `json:"delay_period"`
} `json:"connection"`
Proof []byte `json:"proof"`
ProofHeight struct {
RevisionNumber string `json:"revision_number"`
RevisionHeight string `json:"revision_height"`
} `json:"proof_height"`
}
// GetConnection queries an IBC connection
func (c *StarshipClient) GetConnection(ctx context.Context, connectionID string) (*ConnectionResponse, error) {
url := fmt.Sprintf("%s/ibc/core/connection/v1/connections/%s", c.baseURL, connectionID)
var connResp ConnectionResponse
if err := c.doRequest(ctx, url, &connResp); err != nil {
return nil, fmt.Errorf("failed to query connection: %w", err)
}
return &connResp, nil
}
// ClientStateResponse represents IBC client state query response
type ClientStateResponse struct {
ClientState ibcexported.ClientState `json:"client_state"`
Proof []byte `json:"proof"`
ProofHeight struct {
RevisionNumber string `json:"revision_number"`
RevisionHeight string `json:"revision_height"`
} `json:"proof_height"`
}
// GetClientState queries an IBC client state
func (c *StarshipClient) GetClientState(ctx context.Context, clientID string) (*ClientStateResponse, error) {
url := fmt.Sprintf("%s/ibc/core/client/v1/client_states/%s", c.baseURL, clientID)
var clientResp ClientStateResponse
if err := c.doRequest(ctx, url, &clientResp); err != nil {
return nil, fmt.Errorf("failed to query client state: %w", err)
}
return &clientResp, nil
}
// DenomTraceResponse represents IBC denom trace query response
type DenomTraceResponse struct {
DenomTrace struct {
Path string `json:"path"`
BaseDenom string `json:"base_denom"`
} `json:"denom_trace"`
}
// GetDenomTrace queries an IBC denom trace
func (c *StarshipClient) GetDenomTrace(ctx context.Context, hash string) (*DenomTraceResponse, error) {
url := fmt.Sprintf("%s/ibc/apps/transfer/v1/denom_traces/%s", c.baseURL, hash)
var traceResp DenomTraceResponse
if err := c.doRequest(ctx, url, &traceResp); err != nil {
return nil, fmt.Errorf("failed to query denom trace: %w", err)
}
return &traceResp, nil
}
+181
View File
@@ -0,0 +1,181 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
)
// TxResponse represents transaction broadcast response
type TxResponse struct {
TxHash string `json:"txhash"`
Code uint32 `json:"code"`
RawLog string `json:"raw_log"`
GasUsed string `json:"gas_used"`
GasWanted string `json:"gas_wanted"`
Height string `json:"height"`
Events []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
} `json:"events"`
}
// BroadcastTxRequest represents transaction broadcast request
type BroadcastTxRequest struct {
TxBytes []byte `json:"tx_bytes"`
Mode BroadcastMode `json:"mode"`
}
// BroadcastMode represents different broadcast modes
type BroadcastMode string
const (
BroadcastModeSync BroadcastMode = "BROADCAST_MODE_SYNC"
BroadcastModeAsync BroadcastMode = "BROADCAST_MODE_ASYNC"
BroadcastModeBlock BroadcastMode = "BROADCAST_MODE_BLOCK"
)
// BroadcastTx broadcasts a transaction to the network
func (c *StarshipClient) BroadcastTx(ctx context.Context, txBytes []byte, mode BroadcastMode) (*TxResponse, error) {
url := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs", c.baseURL)
reqBody := BroadcastTxRequest{
TxBytes: txBytes,
Mode: mode,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to broadcast transaction: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("broadcast failed with status %d", resp.StatusCode)
}
var broadcastResp struct {
TxResponse TxResponse `json:"tx_response"`
}
if err := json.NewDecoder(resp.Body).Decode(&broadcastResp); err != nil {
return nil, fmt.Errorf("failed to decode broadcast response: %w", err)
}
return &broadcastResp.TxResponse, nil
}
// GetTxResponse represents get transaction response
type GetTxResponse struct {
Tx tx.Tx `json:"tx"`
TxResponse TxResponse `json:"tx_response"`
}
// GetTx queries a transaction by hash
func (c *StarshipClient) GetTx(ctx context.Context, txHash string) (*GetTxResponse, error) {
url := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", c.baseURL, txHash)
var txResp GetTxResponse
if err := c.doRequest(ctx, url, &txResp); err != nil {
return nil, fmt.Errorf("failed to query transaction: %w", err)
}
return &txResp, nil
}
// SimulateRequest represents transaction simulation request
type SimulateRequest struct {
TxBytes []byte `json:"tx_bytes"`
}
// SimulateResponse represents transaction simulation response
type SimulateResponse struct {
GasInfo struct {
GasWanted string `json:"gas_wanted"`
GasUsed string `json:"gas_used"`
} `json:"gas_info"`
Result struct {
Data string `json:"data"`
Log string `json:"log"`
Events []sdk.StringEvent `json:"events"`
} `json:"result"`
}
// SimulateTx simulates a transaction to estimate gas
func (c *StarshipClient) SimulateTx(ctx context.Context, txBytes []byte) (*SimulateResponse, error) {
url := fmt.Sprintf("%s/cosmos/tx/v1beta1/simulate", c.baseURL)
reqBody := SimulateRequest{
TxBytes: txBytes,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal simulate request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create simulate request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to simulate transaction: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("simulation failed with status %d", resp.StatusCode)
}
var simResp SimulateResponse
if err := json.NewDecoder(resp.Body).Decode(&simResp); err != nil {
return nil, fmt.Errorf("failed to decode simulation response: %w", err)
}
return &simResp, nil
}
// WaitForTx waits for a transaction to be included in a block
func (c *StarshipClient) WaitForTx(ctx context.Context, txHash string, timeout time.Duration) (*GetTxResponse, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for transaction %s", txHash)
case <-ticker.C:
tx, err := c.GetTx(ctx, txHash)
if err == nil && tx.TxResponse.Code == 0 {
return tx, nil
}
// Continue waiting if transaction not found or failed
}
}
}
+320
View File
@@ -0,0 +1,320 @@
package client
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
)
// WebSocketClient provides WebSocket client for CometBFT event subscription
type WebSocketClient struct {
baseURL string
conn *websocket.Conn
}
// EventSubscription represents an event subscription
type EventSubscription struct {
Query string
Events chan *SubscriptionEvent
Errors chan error
done chan struct{}
}
// SubscriptionEvent represents an event received via subscription
type SubscriptionEvent struct {
Query string `json:"query"`
Data EventResultData `json:"data"`
Events []any `json:"events,omitempty"`
}
// EventResultData represents the data part of a subscription event
type EventResultData struct {
Type string `json:"type"`
Value any `json:"value"`
}
// JSONRPCRequest represents a JSON-RPC request
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params any `json:"params"`
ID int `json:"id"`
}
// JSONRPCResponse represents a JSON-RPC response
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result any `json:"result,omitempty"`
Error *JSONRPCError `json:"error,omitempty"`
ID int `json:"id"`
}
// JSONRPCError represents a JSON-RPC error
type JSONRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data,omitempty"`
}
// SubscribeParams represents subscription parameters
type SubscribeParams struct {
Query string `json:"query"`
}
// NewWebSocketClient creates a new WebSocket client
func NewWebSocketClient(baseURL string) *WebSocketClient {
return &WebSocketClient{
baseURL: baseURL,
}
}
// Connect establishes a WebSocket connection to CometBFT
func (ws *WebSocketClient) Connect(ctx context.Context) error {
// Convert HTTP URL to WebSocket URL
wsURL := strings.Replace(ws.baseURL, "http://", "ws://", 1)
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
wsURL += "/websocket"
u, err := url.Parse(wsURL)
if err != nil {
return fmt.Errorf("invalid WebSocket URL: %w", err)
}
dialer := websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
}
conn, _, err := dialer.DialContext(ctx, u.String(), nil)
if err != nil {
return fmt.Errorf("failed to connect to WebSocket: %w", err)
}
ws.conn = conn
return nil
}
// Close closes the WebSocket connection
func (ws *WebSocketClient) Close() error {
if ws.conn != nil {
return ws.conn.Close()
}
return nil
}
// Subscribe subscribes to events matching the given query
func (ws *WebSocketClient) Subscribe(ctx context.Context, query string) (*EventSubscription, error) {
if ws.conn == nil {
return nil, fmt.Errorf("WebSocket connection not established")
}
// Send subscription request
req := JSONRPCRequest{
JSONRPC: "2.0",
Method: "subscribe",
Params: SubscribeParams{
Query: query,
},
ID: 1,
}
if err := ws.conn.WriteJSON(req); err != nil {
return nil, fmt.Errorf("failed to send subscription request: %w", err)
}
// Read subscription response
var resp JSONRPCResponse
if err := ws.conn.ReadJSON(&resp); err != nil {
return nil, fmt.Errorf("failed to read subscription response: %w", err)
}
if resp.Error != nil {
return nil, fmt.Errorf("subscription error: %s", resp.Error.Message)
}
// Create subscription
subscription := &EventSubscription{
Query: query,
Events: make(chan *SubscriptionEvent, 100),
Errors: make(chan error, 10),
done: make(chan struct{}),
}
// Start listening for events
go ws.listenForEvents(ctx, subscription)
return subscription, nil
}
// SubscribeToNewBlocks subscribes to new block events
func (ws *WebSocketClient) SubscribeToNewBlocks(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "tm.event = 'NewBlock'")
}
// SubscribeToNewBlockHeaders subscribes to new block header events
func (ws *WebSocketClient) SubscribeToNewBlockHeaders(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "tm.event = 'NewBlockHeader'")
}
// SubscribeToTxEvents subscribes to transaction events
func (ws *WebSocketClient) SubscribeToTxEvents(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "tm.event = 'Tx'")
}
// SubscribeToDIDEvents subscribes to DID module events
func (ws *WebSocketClient) SubscribeToDIDEvents(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "did.v1.EventDIDCreated EXISTS OR did.v1.EventDIDUpdated EXISTS OR did.v1.EventDIDDeactivated EXISTS")
}
// SubscribeToDWNEvents subscribes to DWN module events
func (ws *WebSocketClient) SubscribeToDWNEvents(ctx context.Context) (*EventSubscription, error) {
return ws.Subscribe(ctx, "dwn.v1.EventRecordWritten EXISTS OR dwn.v1.EventRecordDeleted EXISTS")
}
// SubscribeToCustomEvents subscribes to custom events with specific attributes
func (ws *WebSocketClient) SubscribeToCustomEvents(ctx context.Context, eventType, attributeKey, attributeValue string) (*EventSubscription, error) {
query := fmt.Sprintf("%s EXISTS", eventType)
if attributeKey != "" && attributeValue != "" {
query += fmt.Sprintf(" AND %s.%s = '%s'", eventType, attributeKey, attributeValue)
}
return ws.Subscribe(ctx, query)
}
// listenForEvents listens for incoming events on the WebSocket connection
func (ws *WebSocketClient) listenForEvents(ctx context.Context, subscription *EventSubscription) {
defer close(subscription.Events)
defer close(subscription.Errors)
for {
select {
case <-ctx.Done():
return
case <-subscription.done:
return
default:
// Set read deadline
if err := ws.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
subscription.Errors <- fmt.Errorf("failed to set read deadline: %w", err)
return
}
var message json.RawMessage
if err := ws.conn.ReadJSON(&message); err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
return
}
subscription.Errors <- fmt.Errorf("failed to read WebSocket message: %w", err)
continue
}
// Try to parse as JSON-RPC response first
var resp JSONRPCResponse
if err := json.Unmarshal(message, &resp); err == nil && resp.Result != nil {
// This is likely an event notification
var event SubscriptionEvent
if eventBytes, err := json.Marshal(resp.Result); err == nil {
if err := json.Unmarshal(eventBytes, &event); err == nil {
event.Query = subscription.Query
select {
case subscription.Events <- &event:
case <-ctx.Done():
return
case <-subscription.done:
return
}
}
}
}
}
}
}
// Unsubscribe unsubscribes from the event subscription
func (ws *WebSocketClient) Unsubscribe(ctx context.Context, subscription *EventSubscription) error {
if ws.conn == nil {
return fmt.Errorf("WebSocket connection not established")
}
// Send unsubscribe request
req := JSONRPCRequest{
JSONRPC: "2.0",
Method: "unsubscribe",
Params: SubscribeParams{
Query: subscription.Query,
},
ID: 2,
}
if err := ws.conn.WriteJSON(req); err != nil {
return fmt.Errorf("failed to send unsubscribe request: %w", err)
}
// Signal the listening goroutine to stop
close(subscription.done)
return nil
}
// Close closes the event subscription
func (sub *EventSubscription) Close() {
if sub.done != nil {
select {
case <-sub.done:
// Already closed
default:
close(sub.done)
}
}
}
// WaitForEvent waits for a specific event with timeout
func (sub *EventSubscription) WaitForEvent(ctx context.Context, timeout time.Duration, eventFilter func(*SubscriptionEvent) bool) (*SubscriptionEvent, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for event")
case err := <-sub.Errors:
return nil, fmt.Errorf("subscription error: %w", err)
case event := <-sub.Events:
if event == nil {
return nil, fmt.Errorf("event channel closed")
}
if eventFilter == nil || eventFilter(event) {
return event, nil
}
}
}
}
// WaitForEventByType waits for an event of a specific type
func (sub *EventSubscription) WaitForEventByType(ctx context.Context, timeout time.Duration, eventType string) (*SubscriptionEvent, error) {
return sub.WaitForEvent(ctx, timeout, func(event *SubscriptionEvent) bool {
// This is a simplified check - in practice, you'd parse the event data more carefully
eventStr := fmt.Sprintf("%v", event.Data.Value)
return strings.Contains(eventStr, eventType)
})
}
// GetAllEvents returns all events received so far (non-blocking)
func (sub *EventSubscription) GetAllEvents() []*SubscriptionEvent {
var events []*SubscriptionEvent
for {
select {
case event := <-sub.Events:
if event == nil {
return events
}
events = append(events, event)
default:
return events
}
}
}