synor/sdk/ruby/lib/synor_bridge/client.rb
Gulshan Yadav a874faef13 feat: complete Phase 3 SDKs for Swift, C, C++, C#, and Ruby
Implements Database, Hosting, and Bridge SDKs for remaining languages:

Swift SDKs:
- SynorDatabase with KV, Document, Vector, TimeSeries stores
- SynorHosting with domain, DNS, deployment, SSL operations
- SynorBridge with lock-mint and burn-unlock cross-chain flows

C SDKs:
- database.h/c - multi-model database client
- hosting.h/c - hosting and domain management
- bridge.h/c - cross-chain asset transfers

C++ SDKs:
- database.hpp - modern C++17 with std::future async
- hosting.hpp - domain and deployment operations
- bridge.hpp - cross-chain bridge with wait operations

C# SDKs:
- SynorDatabase.cs - async/await with inner store classes
- SynorHosting.cs - domain management and analytics
- SynorBridge.cs - cross-chain with BridgeException handling

Ruby SDKs:
- synor_database - Struct-based types with Faraday HTTP
- synor_hosting - domain, DNS, SSL, analytics
- synor_bridge - lock-mint/burn-unlock with retry logic

Phase 3 complete: Database/Hosting/Bridge now available in all 12 languages.
2026-01-27 02:23:07 +05:30

579 lines
17 KiB
Ruby

# frozen_string_literal: true
require "faraday"
require "json"
require "uri"
module SynorBridge
# Synor Bridge SDK client for Ruby.
# Cross-chain asset transfers with lock-mint and burn-unlock patterns.
class Client
FINAL_STATUSES = [
TransferStatus::COMPLETED,
TransferStatus::FAILED,
TransferStatus::REFUNDED
].freeze
attr_reader :closed
def initialize(config)
@config = config
@closed = false
@conn = Faraday.new(url: config.endpoint) do |f|
f.request :json
f.response :json
f.options.timeout = config.timeout
f.headers["Authorization"] = "Bearer #{config.api_key}"
f.headers["Content-Type"] = "application/json"
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
end
end
# ==================== Chain Operations ====================
# Get supported chains
def get_supported_chains
response = get("/chains")
(response["chains"] || []).map { |c| parse_chain(c) }
end
# Get chain details
def get_chain(chain_id)
response = get("/chains/#{chain_id.downcase}")
parse_chain(response)
end
# Check if chain is supported
def chain_supported?(chain_id)
chain = get_chain(chain_id)
chain.supported
rescue StandardError
false
end
# ==================== Asset Operations ====================
# Get supported assets for a chain
def get_supported_assets(chain_id)
response = get("/chains/#{chain_id.downcase}/assets")
(response["assets"] || []).map { |a| parse_asset(a) }
end
# Get asset details
def get_asset(asset_id)
response = get("/assets/#{encode(asset_id)}")
parse_asset(response)
end
# Get wrapped asset info
def get_wrapped_asset(original_asset_id, target_chain)
response = get("/assets/#{encode(original_asset_id)}/wrapped/#{target_chain.downcase}")
parse_wrapped_asset(response)
end
# ==================== Fee Operations ====================
# Estimate transfer fee
def estimate_fee(asset:, amount:, source_chain:, target_chain:)
body = {
asset: asset,
amount: amount,
source_chain: source_chain.downcase,
target_chain: target_chain.downcase
}
response = post("/fees/estimate", body)
parse_fee_estimate(response)
end
# Get exchange rate
def get_exchange_rate(from_asset, to_asset)
response = get("/rates/#{encode(from_asset)}/#{encode(to_asset)}")
parse_exchange_rate(response)
end
# ==================== Lock-Mint Flow ====================
# Lock assets for cross-chain transfer
def lock(asset:, amount:, target_chain:, options: nil)
body = {
asset: asset,
amount: amount,
target_chain: target_chain.downcase
}
if options
body[:recipient] = options.recipient if options.recipient
body[:deadline] = options.deadline if options.deadline
body[:slippage] = options.slippage if options.slippage
end
response = post("/transfers/lock", body)
parse_lock_receipt(response)
end
# Get lock proof
def get_lock_proof(lock_receipt_id)
response = get("/transfers/lock/#{encode(lock_receipt_id)}/proof")
parse_lock_proof(response)
end
# Wait for lock proof
def wait_for_lock_proof(lock_receipt_id, poll_interval: 5, max_wait: 600)
deadline = Time.now + max_wait
while Time.now < deadline
begin
return get_lock_proof(lock_receipt_id)
rescue HttpError => e
raise e unless e.confirmations_pending?
sleep(poll_interval)
end
end
raise HttpError.new("Timeout waiting for lock proof", code: "CONFIRMATIONS_PENDING")
end
# Mint wrapped assets
def mint(proof:, target_address:, options: nil)
body = { proof: proof_to_hash(proof), target_address: target_address }
if options
body[:gas_limit] = options.gas_limit if options.gas_limit
body[:max_fee_per_gas] = options.max_fee_per_gas if options.max_fee_per_gas
body[:max_priority_fee_per_gas] = options.max_priority_fee_per_gas if options.max_priority_fee_per_gas
end
response = post("/transfers/mint", body)
parse_signed_transaction(response)
end
# ==================== Burn-Unlock Flow ====================
# Burn wrapped assets
def burn(wrapped_asset:, amount:, options: nil)
body = { wrapped_asset: wrapped_asset, amount: amount }
if options
body[:recipient] = options.recipient if options.recipient
body[:deadline] = options.deadline if options.deadline
end
response = post("/transfers/burn", body)
parse_burn_receipt(response)
end
# Get burn proof
def get_burn_proof(burn_receipt_id)
response = get("/transfers/burn/#{encode(burn_receipt_id)}/proof")
parse_burn_proof(response)
end
# Wait for burn proof
def wait_for_burn_proof(burn_receipt_id, poll_interval: 5, max_wait: 600)
deadline = Time.now + max_wait
while Time.now < deadline
begin
return get_burn_proof(burn_receipt_id)
rescue HttpError => e
raise e unless e.confirmations_pending?
sleep(poll_interval)
end
end
raise HttpError.new("Timeout waiting for burn proof", code: "CONFIRMATIONS_PENDING")
end
# Unlock original assets
def unlock(proof:, options: nil)
body = { proof: burn_proof_to_hash(proof) }
if options
body[:gas_limit] = options.gas_limit if options.gas_limit
body[:gas_price] = options.gas_price if options.gas_price
end
response = post("/transfers/unlock", body)
parse_signed_transaction(response)
end
# ==================== Transfer Management ====================
# Get transfer details
def get_transfer(transfer_id)
response = get("/transfers/#{encode(transfer_id)}")
parse_transfer(response)
end
# Get transfer status
def get_transfer_status(transfer_id)
transfer = get_transfer(transfer_id)
transfer.status
end
# List transfers
def list_transfers(filter: nil)
params = {}
if filter
params[:status] = filter.status.downcase if filter.status
params[:source_chain] = filter.source_chain.downcase if filter.source_chain
params[:target_chain] = filter.target_chain.downcase if filter.target_chain
params[:limit] = filter.limit if filter.limit
params[:offset] = filter.offset if filter.offset
end
response = get("/transfers", params)
(response["transfers"] || []).map { |t| parse_transfer(t) }
end
# Wait for transfer completion
def wait_for_transfer(transfer_id, poll_interval: 10, max_wait: 1800)
deadline = Time.now + max_wait
while Time.now < deadline
transfer = get_transfer(transfer_id)
return transfer if FINAL_STATUSES.include?(transfer.status)
sleep(poll_interval)
end
raise Error, "Timeout waiting for transfer completion"
end
# ==================== Convenience Methods ====================
# Bridge assets to another chain (lock-mint flow)
def bridge_to(asset:, amount:, target_chain:, target_address:, lock_options: nil, mint_options: nil)
receipt = lock(asset: asset, amount: amount, target_chain: target_chain, options: lock_options)
puts "Locked: #{receipt.id}, waiting for confirmations..." if @config.debug
proof = wait_for_lock_proof(receipt.id)
puts "Proof ready, minting on #{target_chain}..." if @config.debug
mint(proof: proof, target_address: target_address, options: mint_options)
wait_for_transfer(receipt.id)
end
# Bridge assets back to original chain (burn-unlock flow)
def bridge_back(wrapped_asset:, amount:, burn_options: nil, unlock_options: nil)
receipt = burn(wrapped_asset: wrapped_asset, amount: amount, options: burn_options)
puts "Burned: #{receipt.id}, waiting for confirmations..." if @config.debug
proof = wait_for_burn_proof(receipt.id)
puts "Proof ready, unlocking on #{receipt.target_chain}..." if @config.debug
unlock(proof: proof, options: unlock_options)
wait_for_transfer(receipt.id)
end
# ==================== Lifecycle ====================
# Health check
def health_check
response = get("/health")
response["status"] == "healthy"
rescue StandardError
false
end
# Close the client
def close
@closed = true
@conn.close if @conn.respond_to?(:close)
end
private
def get(path, params = {})
execute { @conn.get(path, params).body }
end
def post(path, body)
execute { @conn.post(path, body).body }
end
def execute
raise ClientClosedError, "Client has been closed" if @closed
last_error = nil
@config.retries.times do |attempt|
begin
response = yield
check_error(response) if response.is_a?(Hash)
return response
rescue StandardError => e
last_error = e
sleep(2**attempt) if attempt < @config.retries - 1
end
end
raise last_error
end
def check_error(response)
return unless response["error"] || (response["code"] && response["message"])
message = response["message"] || response["error"] || "Unknown error"
code = response["code"]
status = response["status_code"] || 0
raise HttpError.new(message, status_code: status, code: code)
end
def encode(str)
URI.encode_www_form_component(str)
end
# Parsing methods
def parse_chain(data)
return nil unless data
Chain.new(
id: data["id"],
name: data["name"],
chain_id: data["chain_id"],
rpc_url: data["rpc_url"],
explorer_url: data["explorer_url"],
native_currency: data["native_currency"] ? NativeCurrency.new(
name: data["native_currency"]["name"],
symbol: data["native_currency"]["symbol"],
decimals: data["native_currency"]["decimals"]
) : nil,
confirmations: data["confirmations"],
estimated_block_time: data["estimated_block_time"],
supported: data["supported"]
)
end
def parse_asset(data)
return nil unless data
Asset.new(
id: data["id"],
symbol: data["symbol"],
name: data["name"],
type: data["type"],
chain: data["chain"],
contract_address: data["contract_address"],
decimals: data["decimals"],
logo_url: data["logo_url"],
verified: data["verified"]
)
end
def parse_wrapped_asset(data)
return nil unless data
WrappedAsset.new(
original_asset: parse_asset(data["original_asset"]),
wrapped_asset: parse_asset(data["wrapped_asset"]),
chain: data["chain"],
bridge_contract: data["bridge_contract"]
)
end
def parse_lock_receipt(data)
return nil unless data
LockReceipt.new(
id: data["id"],
tx_hash: data["tx_hash"],
source_chain: data["source_chain"],
target_chain: data["target_chain"],
asset: parse_asset(data["asset"]),
amount: data["amount"],
sender: data["sender"],
recipient: data["recipient"],
lock_timestamp: data["lock_timestamp"],
confirmations: data["confirmations"],
required_confirmations: data["required_confirmations"]
)
end
def parse_lock_proof(data)
return nil unless data
LockProof.new(
lock_receipt: parse_lock_receipt(data["lock_receipt"]),
merkle_proof: data["merkle_proof"],
block_header: data["block_header"],
signatures: (data["signatures"] || []).map do |s|
ValidatorSignature.new(
validator: s["validator"],
signature: s["signature"],
timestamp: s["timestamp"]
)
end
)
end
def parse_burn_receipt(data)
return nil unless data
BurnReceipt.new(
id: data["id"],
tx_hash: data["tx_hash"],
source_chain: data["source_chain"],
target_chain: data["target_chain"],
wrapped_asset: parse_asset(data["wrapped_asset"]),
original_asset: parse_asset(data["original_asset"]),
amount: data["amount"],
sender: data["sender"],
recipient: data["recipient"],
burn_timestamp: data["burn_timestamp"],
confirmations: data["confirmations"],
required_confirmations: data["required_confirmations"]
)
end
def parse_burn_proof(data)
return nil unless data
BurnProof.new(
burn_receipt: parse_burn_receipt(data["burn_receipt"]),
merkle_proof: data["merkle_proof"],
block_header: data["block_header"],
signatures: (data["signatures"] || []).map do |s|
ValidatorSignature.new(
validator: s["validator"],
signature: s["signature"],
timestamp: s["timestamp"]
)
end
)
end
def parse_transfer(data)
return nil unless data
Transfer.new(
id: data["id"],
direction: data["direction"],
status: data["status"],
source_chain: data["source_chain"],
target_chain: data["target_chain"],
asset: parse_asset(data["asset"]),
amount: data["amount"],
sender: data["sender"],
recipient: data["recipient"],
source_tx_hash: data["source_tx_hash"],
target_tx_hash: data["target_tx_hash"],
fee: data["fee"],
fee_asset: parse_asset(data["fee_asset"]),
created_at: data["created_at"],
updated_at: data["updated_at"],
completed_at: data["completed_at"],
error_message: data["error_message"]
)
end
def parse_fee_estimate(data)
return nil unless data
FeeEstimate.new(
bridge_fee: data["bridge_fee"],
gas_fee_source: data["gas_fee_source"],
gas_fee_target: data["gas_fee_target"],
total_fee: data["total_fee"],
fee_asset: parse_asset(data["fee_asset"]),
estimated_time: data["estimated_time"],
exchange_rate: data["exchange_rate"]
)
end
def parse_exchange_rate(data)
return nil unless data
ExchangeRate.new(
from_asset: parse_asset(data["from_asset"]),
to_asset: parse_asset(data["to_asset"]),
rate: data["rate"],
inverse_rate: data["inverse_rate"],
last_updated: data["last_updated"],
source: data["source"]
)
end
def parse_signed_transaction(data)
return nil unless data
SignedTransaction.new(
tx_hash: data["tx_hash"],
chain: data["chain"],
from: data["from"],
to: data["to"],
value: data["value"],
data: data["data"],
gas_limit: data["gas_limit"],
gas_price: data["gas_price"],
max_fee_per_gas: data["max_fee_per_gas"],
max_priority_fee_per_gas: data["max_priority_fee_per_gas"],
nonce: data["nonce"],
signature: data["signature"]
)
end
# Conversion methods
def proof_to_hash(proof)
{
lock_receipt: lock_receipt_to_hash(proof.lock_receipt),
merkle_proof: proof.merkle_proof,
block_header: proof.block_header,
signatures: proof.signatures.map { |s| signature_to_hash(s) }
}
end
def lock_receipt_to_hash(receipt)
{
id: receipt.id,
tx_hash: receipt.tx_hash,
source_chain: receipt.source_chain,
target_chain: receipt.target_chain,
asset: asset_to_hash(receipt.asset),
amount: receipt.amount,
sender: receipt.sender,
recipient: receipt.recipient,
lock_timestamp: receipt.lock_timestamp,
confirmations: receipt.confirmations,
required_confirmations: receipt.required_confirmations
}
end
def burn_proof_to_hash(proof)
{
burn_receipt: burn_receipt_to_hash(proof.burn_receipt),
merkle_proof: proof.merkle_proof,
block_header: proof.block_header,
signatures: proof.signatures.map { |s| signature_to_hash(s) }
}
end
def burn_receipt_to_hash(receipt)
{
id: receipt.id,
tx_hash: receipt.tx_hash,
source_chain: receipt.source_chain,
target_chain: receipt.target_chain,
wrapped_asset: asset_to_hash(receipt.wrapped_asset),
original_asset: asset_to_hash(receipt.original_asset),
amount: receipt.amount,
sender: receipt.sender,
recipient: receipt.recipient,
burn_timestamp: receipt.burn_timestamp,
confirmations: receipt.confirmations,
required_confirmations: receipt.required_confirmations
}
end
def asset_to_hash(asset)
return nil unless asset
{
id: asset.id,
symbol: asset.symbol,
name: asset.name,
type: asset.type,
chain: asset.chain,
contract_address: asset.contract_address,
decimals: asset.decimals,
logo_url: asset.logo_url,
verified: asset.verified
}
end
def signature_to_hash(sig)
{
validator: sig.validator,
signature: sig.signature,
timestamp: sig.timestamp
}
end
end
end