synor/sdk/ruby/lib/synor/ibc.rb
Gulshan Yadav 97add23062 feat(sdk): implement IBC SDK for all 12 languages
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
2026-01-28 12:53:46 +05:30

470 lines
12 KiB
Ruby

# frozen_string_literal: true
require 'net/http'
require 'uri'
require 'json'
require 'base64'
module Synor
# IBC SDK for Ruby
#
# Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability.
module Ibc
VERSION = '0.1.0'
# IBC Height representation
class Height
attr_reader :revision_number, :revision_height
def initialize(revision_number: 0, revision_height: 1)
@revision_number = revision_number
@revision_height = revision_height
end
def zero?
@revision_number.zero? && @revision_height.zero?
end
def increment
Height.new(revision_number: @revision_number, revision_height: @revision_height + 1)
end
def self.from_json(json)
Height.new(
revision_number: json['revision_number'] || 0,
revision_height: json['revision_height'] || 1
)
end
def to_json(*_args)
{ 'revision_number' => @revision_number, 'revision_height' => @revision_height }
end
end
# Light client types
module ClientType
TENDERMINT = 'tendermint'
SOLO_MACHINE = 'solo_machine'
LOCALHOST = 'localhost'
WASM = 'wasm'
end
# Trust level configuration
class TrustLevel
attr_reader :numerator, :denominator
def initialize(numerator: 1, denominator: 3)
@numerator = numerator
@denominator = denominator
end
def self.from_json(json)
TrustLevel.new(
numerator: json['numerator'] || 1,
denominator: json['denominator'] || 3
)
end
def to_json(*_args)
{ 'numerator' => @numerator, 'denominator' => @denominator }
end
end
# Light client state
class ClientState
attr_reader :chain_id, :trust_level, :trusting_period, :unbonding_period,
:max_clock_drift, :latest_height, :frozen_height
def initialize(chain_id:, trust_level:, trusting_period:, unbonding_period:,
max_clock_drift:, latest_height:, frozen_height: nil)
@chain_id = chain_id
@trust_level = trust_level
@trusting_period = trusting_period
@unbonding_period = unbonding_period
@max_clock_drift = max_clock_drift
@latest_height = latest_height
@frozen_height = frozen_height
end
def self.from_json(json)
ClientState.new(
chain_id: json['chain_id'],
trust_level: TrustLevel.from_json(json['trust_level']),
trusting_period: json['trusting_period'],
unbonding_period: json['unbonding_period'],
max_clock_drift: json['max_clock_drift'],
latest_height: Height.from_json(json['latest_height']),
frozen_height: json['frozen_height'] ? Height.from_json(json['frozen_height']) : nil
)
end
def to_json(*_args)
result = {
'chain_id' => @chain_id,
'trust_level' => @trust_level.to_json,
'trusting_period' => @trusting_period,
'unbonding_period' => @unbonding_period,
'max_clock_drift' => @max_clock_drift,
'latest_height' => @latest_height.to_json
}
result['frozen_height'] = @frozen_height.to_json if @frozen_height
result
end
end
# Connection state
module ConnectionState
UNINITIALIZED = 'uninitialized'
INIT = 'init'
TRYOPEN = 'tryopen'
OPEN = 'open'
end
# Channel ordering
module ChannelOrder
UNORDERED = 'unordered'
ORDERED = 'ordered'
end
# Channel state
module ChannelState
UNINITIALIZED = 'uninitialized'
INIT = 'init'
TRYOPEN = 'tryopen'
OPEN = 'open'
CLOSED = 'closed'
end
# Timeout information
class Timeout
attr_reader :height, :timestamp
def initialize(height:, timestamp: 0)
@height = height
@timestamp = timestamp
end
def self.from_height(h)
Timeout.new(height: Height.new(revision_height: h))
end
def self.from_timestamp(ts)
Timeout.new(height: Height.new, timestamp: ts)
end
def to_json(*_args)
{ 'height' => @height.to_json, 'timestamp' => @timestamp.to_s }
end
end
# Swap state
module SwapState
PENDING = 'pending'
LOCKED = 'locked'
COMPLETED = 'completed'
REFUNDED = 'refunded'
EXPIRED = 'expired'
CANCELLED = 'cancelled'
end
# Atomic swap
class AtomicSwap
attr_reader :swap_id, :state, :initiator_htlc, :responder_htlc
def initialize(swap_id:, state:, initiator_htlc:, responder_htlc: nil)
@swap_id = swap_id
@state = state
@initiator_htlc = initiator_htlc
@responder_htlc = responder_htlc
end
def self.from_json(json)
AtomicSwap.new(
swap_id: json['swap_id']['id'],
state: json['state'],
initiator_htlc: json['initiator_htlc'],
responder_htlc: json['responder_htlc']
)
end
end
# IBC SDK configuration
class Config
attr_accessor :api_key, :endpoint, :ws_endpoint, :timeout, :retries, :chain_id, :debug
def initialize(
api_key:,
endpoint: 'https://ibc.synor.io/v1',
ws_endpoint: 'wss://ibc.synor.io/v1/ws',
timeout: 30,
retries: 3,
chain_id: 'synor-1',
debug: false
)
@api_key = api_key
@endpoint = endpoint
@ws_endpoint = ws_endpoint
@timeout = timeout
@retries = retries
@chain_id = chain_id
@debug = debug
end
end
# IBC exception
class IbcError < StandardError
attr_reader :code, :status
def initialize(message, code: nil, status: nil)
super(message)
@code = code
@status = status
end
end
# Main IBC client
class Client
attr_reader :clients, :connections, :channels, :transfer, :swaps
def initialize(config)
@config = config
@closed = false
@clients = LightClientClient.new(self)
@connections = ConnectionsClient.new(self)
@channels = ChannelsClient.new(self)
@transfer = TransferClient.new(self)
@swaps = SwapsClient.new(self)
end
def chain_id
@config.chain_id
end
def get_chain_info
get('/chain')
end
def get_height
result = get('/chain/height')
Height.from_json(result)
end
def health_check
result = get('/health')
result['status'] == 'healthy'
rescue StandardError
false
end
def close
@closed = true
end
def closed?
@closed
end
# Internal HTTP methods
def get(path)
request(:get, path)
end
def post(path, body)
request(:post, path, body)
end
private
def request(method, path, body = nil)
raise IbcError.new('Client has been closed', code: 'CLIENT_CLOSED') if @closed
uri = URI.parse("#{@config.endpoint}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.read_timeout = @config.timeout
request = case method
when :get
Net::HTTP::Get.new(uri.request_uri)
when :post
req = Net::HTTP::Post.new(uri.request_uri)
req.body = body.to_json if body
req
end
request['Content-Type'] = 'application/json'
request['Authorization'] = "Bearer #{@config.api_key}"
request['X-SDK-Version'] = 'ruby/0.1.0'
request['X-Chain-Id'] = @config.chain_id
response = http.request(request)
unless response.is_a?(Net::HTTPSuccess)
error = JSON.parse(response.body) rescue {}
raise IbcError.new(
error['message'] || "HTTP #{response.code}",
code: error['code'],
status: response.code.to_i
)
end
JSON.parse(response.body)
end
end
# Light client sub-client
class LightClientClient
def initialize(ibc)
@ibc = ibc
end
def create(client_type:, client_state:, consensus_state:)
result = @ibc.post('/clients', {
'client_type' => client_type,
'client_state' => client_state.to_json,
'consensus_state' => consensus_state
})
result['client_id']
end
def get_state(client_id)
result = @ibc.get("/clients/#{client_id}/state")
ClientState.from_json(result)
end
def list
result = @ibc.get('/clients')
result['clients'] || []
end
end
# Connections sub-client
class ConnectionsClient
def initialize(ibc)
@ibc = ibc
end
def open_init(client_id:, counterparty_client_id:)
result = @ibc.post('/connections/init', {
'client_id' => client_id,
'counterparty_client_id' => counterparty_client_id
})
result['connection_id']
end
def get(connection_id)
@ibc.get("/connections/#{connection_id}")
end
def list
result = @ibc.get('/connections')
result['connections'] || []
end
end
# Channels sub-client
class ChannelsClient
def initialize(ibc)
@ibc = ibc
end
def bind_port(port_id:, module_name:)
@ibc.post('/ports/bind', { 'port_id' => port_id, 'module' => module_name })
end
def open_init(port_id:, ordering:, connection_id:, counterparty_port:, version:)
result = @ibc.post('/channels/init', {
'port_id' => port_id,
'ordering' => ordering,
'connection_id' => connection_id,
'counterparty_port' => counterparty_port,
'version' => version
})
result['channel_id']
end
def get(port_id:, channel_id:)
@ibc.get("/channels/#{port_id}/#{channel_id}")
end
def list
result = @ibc.get('/channels')
result['channels'] || []
end
end
# Transfer sub-client (ICS-20)
class TransferClient
def initialize(ibc)
@ibc = ibc
end
def transfer(source_port:, source_channel:, denom:, amount:, sender:, receiver:,
timeout: nil, memo: nil)
body = {
'source_port' => source_port,
'source_channel' => source_channel,
'token' => { 'denom' => denom, 'amount' => amount },
'sender' => sender,
'receiver' => receiver
}
if timeout
body['timeout_height'] = timeout.height.to_json
body['timeout_timestamp'] = timeout.timestamp.to_s
end
body['memo'] = memo if memo
@ibc.post('/transfer', body)
end
def get_denom_trace(ibc_denom)
@ibc.get("/transfer/denom_trace/#{ibc_denom}")
end
end
# Swaps sub-client (HTLC)
class SwapsClient
def initialize(ibc)
@ibc = ibc
end
def initiate(responder:, initiator_asset:, responder_asset:)
@ibc.post('/swaps/initiate', {
'responder' => responder,
'initiator_asset' => initiator_asset,
'responder_asset' => responder_asset
})
end
def lock(swap_id)
@ibc.post("/swaps/#{swap_id}/lock", {})
end
def respond(swap_id, asset)
@ibc.post("/swaps/#{swap_id}/respond", { 'asset' => asset })
end
def claim(swap_id, secret)
@ibc.post("/swaps/#{swap_id}/claim", {
'secret' => Base64.strict_encode64(secret.pack('C*'))
})
end
def refund(swap_id)
@ibc.post("/swaps/#{swap_id}/refund", {})
end
def get(swap_id)
result = @ibc.get("/swaps/#{swap_id}")
AtomicSwap.from_json(result)
end
def list_active
result = @ibc.get('/swaps/active')
(result['swaps'] || []).map { |s| AtomicSwap.from_json(s) }
end
end
end
end