Implement Inter-Blockchain Communication (IBC) SDK with full ICS protocol support across all 12 programming languages: - JavaScript/TypeScript, Python, Go, Rust - Java, Kotlin, Swift, Flutter/Dart - C, C++, C#/.NET, Ruby Features: - Light client management (Tendermint, Solo Machine, WASM) - Connection handshake (4-way: Init, Try, Ack, Confirm) - Channel management with ordered/unordered support - ICS-20 fungible token transfers - HTLC atomic swaps with hashlock (SHA256) and timelock - Packet relay with timeout handling
385 lines
11 KiB
Go
385 lines
11 KiB
Go
// 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<<attempt) * 100 * time.Millisecond)
|
|
continue
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 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[:])
|
|
}
|