synor/sdk/ruby/lib/synor_contract/client.rb
Gulshan Yadav e65ea40af2 feat: implement Privacy and Contract SDKs for all 12 languages (Phase 5)
Privacy SDK features:
- Confidential transactions with Pedersen commitments
- Bulletproof range proofs for value validation
- Ring signatures for anonymous signing with key images
- Stealth addresses for unlinkable payments
- Blinding factor generation and value operations

Contract SDK features:
- Smart contract deployment (standard and CREATE2)
- Call (view/pure) and Send (state-changing) operations
- Event log filtering, subscription, and decoding
- ABI encoding/decoding utilities
- Gas estimation and contract verification
- Multicall for batched operations
- Storage slot reading

Languages implemented:
- JavaScript/TypeScript
- Python (async with httpx)
- Go
- Rust (async with reqwest/tokio)
- Java (async with OkHttp)
- Kotlin (coroutines with Ktor)
- Swift (async/await with URLSession)
- Flutter/Dart
- C (header-only interface)
- C++ (header-only with std::future)
- C#/.NET (async with HttpClient)
- Ruby (Faraday HTTP client)

All SDKs follow consistent patterns:
- Configuration with API key, endpoint, timeout, retries
- Custom exception types with error codes
- Retry logic with exponential backoff
- Health check endpoints
- Closed state management
2026-01-28 09:03:34 +05:30

370 lines
11 KiB
Ruby

# frozen_string_literal: true
require "faraday"
require "json"
require "uri"
module SynorContract
# Synor Contract SDK client for Ruby.
# Smart contract deployment, interaction, and event handling.
class Client
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
# ==================== Contract Deployment ====================
def deploy(options)
body = {
bytecode: options.bytecode
}
body[:abi] = options.abi.map(&:to_h) if options.abi
body[:constructor_args] = options.constructor_args if options.constructor_args
body[:value] = options.value if options.value
body[:gas_limit] = options.gas_limit if options.gas_limit
body[:gas_price] = options.gas_price if options.gas_price
body[:nonce] = options.nonce if options.nonce
response = post("/contract/deploy", body)
parse_deployment_result(response)
end
def deploy_create2(options, salt:)
body = {
bytecode: options.bytecode,
salt: salt
}
body[:abi] = options.abi.map(&:to_h) if options.abi
body[:constructor_args] = options.constructor_args if options.constructor_args
body[:value] = options.value if options.value
body[:gas_limit] = options.gas_limit if options.gas_limit
body[:gas_price] = options.gas_price if options.gas_price
response = post("/contract/deploy/create2", body)
parse_deployment_result(response)
end
def predict_address(bytecode:, salt:, deployer: nil)
body = { bytecode: bytecode, salt: salt }
body[:deployer] = deployer if deployer
response = post("/contract/predict-address", body)
response["address"]
end
# ==================== Contract Interaction ====================
def call(options)
body = {
contract: options.contract,
method: options.method,
args: options.args,
abi: options.abi.map(&:to_h)
}
post("/contract/call", body)
end
def send(options)
body = {
contract: options.contract,
method: options.method,
args: options.args,
abi: options.abi.map(&:to_h)
}
body[:value] = options.value if options.value
body[:gas_limit] = options.gas_limit if options.gas_limit
body[:gas_price] = options.gas_price if options.gas_price
body[:nonce] = options.nonce if options.nonce
response = post("/contract/send", body)
parse_transaction_result(response)
end
# ==================== Events ====================
def get_events(filter)
body = { contract: filter.contract }
body[:event] = filter.event if filter.event
body[:from_block] = filter.from_block if filter.from_block
body[:to_block] = filter.to_block if filter.to_block
body[:topics] = filter.topics if filter.topics
body[:abi] = filter.abi.map(&:to_h) if filter.abi
response = post("/contract/events", body)
response.map { |e| parse_decoded_event(e) }
end
def get_logs(contract, from_block: nil, to_block: nil)
path = "/contract/logs?contract=#{encode(contract)}"
path += "&from_block=#{from_block}" if from_block
path += "&to_block=#{to_block}" if to_block
response = get(path)
response.map { |l| parse_event_log(l) }
end
def decode_logs(logs:, abi:)
body = {
logs: logs.map(&:to_h),
abi: abi.map(&:to_h)
}
response = post("/contract/decode-logs", body)
response.map { |e| parse_decoded_event(e) }
end
# ==================== ABI Utilities ====================
def encode_call(options)
body = {
method: options.method,
args: options.args,
abi: options.abi.map(&:to_h)
}
response = post("/contract/encode", body)
response["data"]
end
def decode_result(options)
body = {
data: options.data,
method: options.method,
abi: options.abi.map(&:to_h)
}
response = post("/contract/decode", body)
response["result"]
end
def get_selector(signature)
response = get("/contract/selector?signature=#{encode(signature)}")
response["selector"]
end
# ==================== Gas Estimation ====================
def estimate_gas(options)
body = {
contract: options.contract,
method: options.method,
args: options.args,
abi: options.abi.map(&:to_h)
}
body[:value] = options.value if options.value
response = post("/contract/estimate-gas", body)
parse_gas_estimation(response)
end
# ==================== Contract Information ====================
def get_bytecode(address)
response = get("/contract/#{encode(address)}/bytecode")
parse_bytecode_info(response)
end
def verify(options)
body = {
address: options.address,
source_code: options.source_code,
compiler_version: options.compiler_version
}
body[:constructor_args] = options.constructor_args if options.constructor_args
body[:optimization] = options.optimization if options.optimization
body[:optimization_runs] = options.optimization_runs if options.optimization_runs
body[:license] = options.license if options.license
response = post("/contract/verify", body)
parse_verification_result(response)
end
def get_verification_status(address)
response = get("/contract/#{encode(address)}/verification")
parse_verification_result(response)
end
# ==================== Multicall ====================
def multicall(requests)
body = { calls: requests.map(&:to_h) }
response = post("/contract/multicall", body)
response.map { |r| parse_multicall_result(r) }
end
# ==================== Storage ====================
def read_storage(options)
path = "/contract/storage?contract=#{encode(options.contract)}&slot=#{encode(options.slot)}"
path += "&block=#{options.block_number}" if options.block_number
response = get(path)
response["value"]
end
# ==================== Lifecycle ====================
def health_check
response = get("/health")
response["status"] == "healthy"
rescue StandardError
false
end
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.to_s)
end
def parse_deployment_result(data)
DeploymentResult.new(
contract_address: data["contract_address"],
transaction_hash: data["transaction_hash"],
deployer: data["deployer"],
gas_used: data["gas_used"],
block_number: data["block_number"],
block_hash: data["block_hash"]
)
end
def parse_transaction_result(data)
TransactionResult.new(
transaction_hash: data["transaction_hash"],
block_number: data["block_number"],
block_hash: data["block_hash"],
gas_used: data["gas_used"],
effective_gas_price: data["effective_gas_price"],
status: data["status"],
logs: (data["logs"] || []).map { |l| parse_event_log(l) }
)
end
def parse_event_log(data)
EventLog.new(
address: data["address"],
topics: data["topics"],
data: data["data"],
block_number: data["block_number"],
transaction_hash: data["transaction_hash"],
log_index: data["log_index"],
block_hash: data["block_hash"],
removed: data["removed"]
)
end
def parse_decoded_event(data)
DecodedEvent.new(
name: data["name"],
signature: data["signature"],
args: data["args"],
log: data["log"] ? parse_event_log(data["log"]) : nil
)
end
def parse_gas_estimation(data)
GasEstimation.new(
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"],
estimated_cost: data["estimated_cost"]
)
end
def parse_bytecode_info(data)
BytecodeInfo.new(
bytecode: data["bytecode"],
deployed_bytecode: data["deployed_bytecode"],
size: data["size"],
is_contract: data["is_contract"]
)
end
def parse_verification_result(data)
VerificationResult.new(
verified: data["verified"],
address: data["address"],
compiler_version: data["compiler_version"],
optimization: data["optimization"],
optimization_runs: data["optimization_runs"],
license: data["license"],
abi: data["abi"]&.map { |e| parse_abi_entry(e) },
source_code: data["source_code"]
)
end
def parse_abi_entry(data)
AbiEntry.new(
type: data["type"],
name: data["name"],
inputs: data["inputs"]&.map { |p| parse_abi_parameter(p) },
outputs: data["outputs"]&.map { |p| parse_abi_parameter(p) },
state_mutability: data["stateMutability"],
anonymous: data["anonymous"]
)
end
def parse_abi_parameter(data)
AbiParameter.new(
name: data["name"],
type: data["type"],
indexed: data["indexed"],
components: data["components"]&.map { |c| parse_abi_parameter(c) }
)
end
def parse_multicall_result(data)
MulticallResult.new(
success: data["success"],
result: data["result"],
error: data["error"]
)
end
end
end