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
370 lines
11 KiB
Ruby
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
|