// Package ibc provides the Synor IBC SDK for Go. package ibc import ( "bytes" "context" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "time" ) // SynorIbc is the main IBC client type SynorIbc struct { config Config client *http.Client closed bool // Sub-clients Clients *LightClientClient Connections *ConnectionsClient Channels *ChannelsClient Packets *PacketsClient Transfer *TransferClient Swaps *SwapsClient } // NewSynorIbc creates a new IBC client func NewSynorIbc(config Config) *SynorIbc { ibc := &SynorIbc{ config: config, client: &http.Client{Timeout: config.Timeout}, } ibc.Clients = &LightClientClient{ibc: ibc} ibc.Connections = &ConnectionsClient{ibc: ibc} ibc.Channels = &ChannelsClient{ibc: ibc} ibc.Packets = &PacketsClient{ibc: ibc} ibc.Transfer = &TransferClient{ibc: ibc} ibc.Swaps = &SwapsClient{ibc: ibc} return ibc } // ChainID returns the chain ID func (c *SynorIbc) ChainID() string { return c.config.ChainID } // GetChainInfo returns chain information func (c *SynorIbc) GetChainInfo(ctx context.Context) (map[string]interface{}, error) { var result map[string]interface{} err := c.get(ctx, "/chain", &result) return result, err } // GetHeight returns current height func (c *SynorIbc) GetHeight(ctx context.Context) (Height, error) { var result Height err := c.get(ctx, "/chain/height", &result) return result, err } // HealthCheck performs a health check func (c *SynorIbc) HealthCheck(ctx context.Context) bool { var result map[string]string if err := c.get(ctx, "/health", &result); err != nil { return false } return result["status"] == "healthy" } // Close closes the client func (c *SynorIbc) Close() { c.closed = true } // IsClosed returns whether client is closed func (c *SynorIbc) IsClosed() bool { return c.closed } func (c *SynorIbc) get(ctx context.Context, path string, result interface{}) error { return c.request(ctx, "GET", path, nil, result) } func (c *SynorIbc) post(ctx context.Context, path string, body, result interface{}) error { return c.request(ctx, "POST", path, body, result) } func (c *SynorIbc) delete(ctx context.Context, path string, result interface{}) error { return c.request(ctx, "DELETE", path, nil, result) } func (c *SynorIbc) request(ctx context.Context, method, path string, body, result interface{}) error { if c.closed { return &IbcError{Message: "Client has been closed", Code: "CLIENT_CLOSED"} } var bodyReader io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { return err } bodyReader = bytes.NewReader(data) } url := c.config.Endpoint + path var lastErr error for attempt := 0; attempt <= c.config.Retries; attempt++ { req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.config.APIKey) req.Header.Set("X-SDK-Version", "go/0.1.0") req.Header.Set("X-Chain-Id", c.config.ChainID) resp, err := c.client.Do(req) if err != nil { lastErr = err time.Sleep(time.Duration(1<= 400 { var errResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&errResp) msg := fmt.Sprintf("HTTP %d", resp.StatusCode) if m, ok := errResp["message"].(string); ok { msg = m } code := "" if c, ok := errResp["code"].(string); ok { code = c } return &IbcError{Message: msg, Code: code, Status: resp.StatusCode} } if result != nil { return json.NewDecoder(resp.Body).Decode(result) } return nil } return lastErr } // LightClientClient handles light client operations type LightClientClient struct { ibc *SynorIbc } // Create creates a new light client func (c *LightClientClient) Create(ctx context.Context, clientType ClientType, clientState ClientState, consensusState ConsensusState) (ClientId, error) { var result struct { ClientID string `json:"client_id"` } err := c.ibc.post(ctx, "/clients", map[string]interface{}{ "client_type": clientType, "client_state": clientState, "consensus_state": consensusState, }, &result) return ClientId{ID: result.ClientID}, err } // Update updates a light client func (c *LightClientClient) Update(ctx context.Context, clientId ClientId, header Header) (Height, error) { var result Height err := c.ibc.post(ctx, "/clients/"+clientId.ID+"/update", map[string]interface{}{ "header": header, }, &result) return result, err } // GetState returns client state func (c *LightClientClient) GetState(ctx context.Context, clientId ClientId) (ClientState, error) { var result ClientState err := c.ibc.get(ctx, "/clients/"+clientId.ID+"/state", &result) return result, err } // List returns all clients func (c *LightClientClient) List(ctx context.Context) ([]map[string]interface{}, error) { var result []map[string]interface{} err := c.ibc.get(ctx, "/clients", &result) return result, err } // ConnectionsClient handles connection operations type ConnectionsClient struct { ibc *SynorIbc } // OpenInit initializes a connection func (c *ConnectionsClient) OpenInit(ctx context.Context, clientId, counterpartyClientId ClientId) (ConnectionId, error) { var result struct { ConnectionID string `json:"connection_id"` } err := c.ibc.post(ctx, "/connections/init", map[string]interface{}{ "client_id": clientId.ID, "counterparty_client_id": counterpartyClientId.ID, }, &result) return ConnectionId{ID: result.ConnectionID}, err } // Get returns a connection func (c *ConnectionsClient) Get(ctx context.Context, connectionId ConnectionId) (ConnectionEnd, error) { var result ConnectionEnd err := c.ibc.get(ctx, "/connections/"+connectionId.ID, &result) return result, err } // List returns all connections func (c *ConnectionsClient) List(ctx context.Context) ([]map[string]interface{}, error) { var result []map[string]interface{} err := c.ibc.get(ctx, "/connections", &result) return result, err } // ChannelsClient handles channel operations type ChannelsClient struct { ibc *SynorIbc } // BindPort binds a port func (c *ChannelsClient) BindPort(ctx context.Context, portId PortId, module string) error { return c.ibc.post(ctx, "/ports/bind", map[string]interface{}{ "port_id": portId.ID, "module": module, }, nil) } // OpenInit initializes a channel func (c *ChannelsClient) OpenInit(ctx context.Context, portId PortId, ordering ChannelOrder, connectionId ConnectionId, counterpartyPort PortId, version string) (ChannelId, error) { var result struct { ChannelID string `json:"channel_id"` } err := c.ibc.post(ctx, "/channels/init", map[string]interface{}{ "port_id": portId.ID, "ordering": ordering, "connection_id": connectionId.ID, "counterparty_port": counterpartyPort.ID, "version": version, }, &result) return ChannelId{ID: result.ChannelID}, err } // Get returns a channel func (c *ChannelsClient) Get(ctx context.Context, portId PortId, channelId ChannelId) (Channel, error) { var result Channel err := c.ibc.get(ctx, "/channels/"+portId.ID+"/"+channelId.ID, &result) return result, err } // List returns all channels func (c *ChannelsClient) List(ctx context.Context) ([]map[string]interface{}, error) { var result []map[string]interface{} err := c.ibc.get(ctx, "/channels", &result) return result, err } // PacketsClient handles packet operations type PacketsClient struct { ibc *SynorIbc } // Send sends a packet func (c *PacketsClient) Send(ctx context.Context, sourcePort PortId, sourceChannel ChannelId, data []byte, timeout Timeout) (map[string]interface{}, error) { var result map[string]interface{} err := c.ibc.post(ctx, "/packets/send", map[string]interface{}{ "source_port": sourcePort.ID, "source_channel": sourceChannel.ID, "data": base64.StdEncoding.EncodeToString(data), "timeout_height": timeout.Height, "timeout_timestamp": timeout.Timestamp, }, &result) return result, err } // TransferClient handles ICS-20 transfers type TransferClient struct { ibc *SynorIbc } // Transfer sends tokens to another chain func (c *TransferClient) Transfer(ctx context.Context, sourcePort, sourceChannel, denom, amount, sender, receiver string, timeout *Timeout, memo string) (map[string]interface{}, error) { body := map[string]interface{}{ "source_port": sourcePort, "source_channel": sourceChannel, "token": map[string]string{ "denom": denom, "amount": amount, }, "sender": sender, "receiver": receiver, "memo": memo, } if timeout != nil { body["timeout_height"] = timeout.Height body["timeout_timestamp"] = timeout.Timestamp } var result map[string]interface{} err := c.ibc.post(ctx, "/transfer", body, &result) return result, err } // GetDenomTrace returns denom trace func (c *TransferClient) GetDenomTrace(ctx context.Context, ibcDenom string) (map[string]string, error) { var result map[string]string err := c.ibc.get(ctx, "/transfer/denom_trace/"+ibcDenom, &result) return result, err } // SwapsClient handles atomic swap operations type SwapsClient struct { ibc *SynorIbc } // Initiate starts an atomic swap func (c *SwapsClient) Initiate(ctx context.Context, responder string, initiatorAsset, responderAsset SwapAsset) (map[string]interface{}, error) { var result map[string]interface{} err := c.ibc.post(ctx, "/swaps/initiate", map[string]interface{}{ "responder": responder, "initiator_asset": initiatorAsset, "responder_asset": responderAsset, }, &result) return result, err } // Lock locks the initiator's tokens func (c *SwapsClient) Lock(ctx context.Context, swapId SwapId) error { return c.ibc.post(ctx, "/swaps/"+swapId.ID+"/lock", nil, nil) } // Respond responds to a swap func (c *SwapsClient) Respond(ctx context.Context, swapId SwapId, asset SwapAsset) (Htlc, error) { var result Htlc err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/respond", map[string]interface{}{ "asset": asset, }, &result) return result, err } // Claim claims tokens with secret func (c *SwapsClient) Claim(ctx context.Context, swapId SwapId, secret []byte) (map[string]interface{}, error) { var result map[string]interface{} err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/claim", map[string]interface{}{ "secret": base64.StdEncoding.EncodeToString(secret), }, &result) return result, err } // Refund refunds an expired swap func (c *SwapsClient) Refund(ctx context.Context, swapId SwapId) (map[string]interface{}, error) { var result map[string]interface{} err := c.ibc.post(ctx, "/swaps/"+swapId.ID+"/refund", nil, &result) return result, err } // Get returns a swap func (c *SwapsClient) Get(ctx context.Context, swapId SwapId) (AtomicSwap, error) { var result AtomicSwap err := c.ibc.get(ctx, "/swaps/"+swapId.ID, &result) return result, err } // ListActive returns active swaps func (c *SwapsClient) ListActive(ctx context.Context) ([]AtomicSwap, error) { var result []AtomicSwap err := c.ibc.get(ctx, "/swaps/active", &result) return result, err } // VerifySecret verifies a hashlock with secret func (c *SwapsClient) VerifySecret(hashlock Hashlock, secret []byte) bool { hash := sha256.Sum256(secret) return bytes.Equal(hashlock.Hash, hash[:]) }