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.
579 lines
17 KiB
Ruby
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
|