# frozen_string_literal: true require "faraday" require "json" require "uri" module SynorPrivacy # Synor Privacy SDK client for Ruby. # Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. 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 # ==================== Confidential Transactions ==================== def create_confidential_tx(inputs:, outputs:) body = { inputs: inputs.map(&:to_h), outputs: outputs.map(&:to_h) } response = post("/privacy/confidential/create", body) parse_confidential_transaction(response) end def verify_confidential_tx(tx:) body = { transaction: tx.to_h } response = post("/privacy/confidential/verify", body) response["valid"] == true end def create_commitment(value:, blinding_factor:) body = { value: value, blinding_factor: blinding_factor } response = post("/privacy/commitment/create", body) Commitment.new( commitment: response["commitment"], blinding_factor: response["blinding_factor"] ) end def verify_commitment(commitment:, value:, blinding_factor:) body = { commitment: commitment, value: value, blinding_factor: blinding_factor } response = post("/privacy/commitment/verify", body) response["valid"] == true end def create_range_proof(value:, blinding_factor:, min_value:, max_value:) body = { value: value, blinding_factor: blinding_factor, min_value: min_value, max_value: max_value } response = post("/privacy/range-proof/create", body) RangeProof.new( proof: response["proof"], commitment: response["commitment"], min_value: response["min_value"], max_value: response["max_value"] ) end def verify_range_proof(proof:) body = { proof: proof.to_h } response = post("/privacy/range-proof/verify", body) response["valid"] == true end # ==================== Ring Signatures ==================== def create_ring_signature(message:, ring:, signer_index:, private_key:) body = { message: message, ring: ring, signer_index: signer_index, private_key: private_key } response = post("/privacy/ring/sign", body) RingSignature.new( c0: response["c0"], s: response["s"], key_image: response["key_image"], ring: response["ring"] ) end def verify_ring_signature(signature:, message:) body = { signature: signature.to_h, message: message } response = post("/privacy/ring/verify", body) response["valid"] == true end def generate_decoys(count, exclude_key: nil) path = "/privacy/ring/decoys?count=#{count}" path += "&exclude=#{encode(exclude_key)}" if exclude_key get(path) end def check_key_image(key_image) response = get("/privacy/ring/key-image/#{key_image}") response["spent"] == true end # ==================== Stealth Addresses ==================== def generate_stealth_keypair response = post("/privacy/stealth/generate", {}) StealthKeyPair.new( spend_public_key: response["spend_public_key"], spend_private_key: response["spend_private_key"], view_public_key: response["view_public_key"], view_private_key: response["view_private_key"] ) end def derive_stealth_address(spend_public_key:, view_public_key:) body = { spend_public_key: spend_public_key, view_public_key: view_public_key } response = post("/privacy/stealth/derive", body) StealthAddress.new( address: response["address"], ephemeral_public_key: response["ephemeral_public_key"], tx_public_key: response["tx_public_key"] ) end def recover_stealth_private_key(stealth_address:, view_private_key:, spend_private_key:) body = { stealth_address: stealth_address, view_private_key: view_private_key, spend_private_key: spend_private_key } response = post("/privacy/stealth/recover", body) response["private_key"] end def scan_outputs(view_private_key:, spend_public_key:, from_block:, to_block: nil) body = { view_private_key: view_private_key, spend_public_key: spend_public_key, from_block: from_block } body[:to_block] = to_block if to_block response = post("/privacy/stealth/scan", body) response.map { |o| parse_stealth_output(o) } end # ==================== Blinding ==================== def generate_blinding_factor response = post("/privacy/blinding/generate", {}) response["blinding_factor"] end def blind_value(value:, blinding_factor:) body = { value: value, blinding_factor: blinding_factor } response = post("/privacy/blinding/blind", body) response["blinded_value"] end def unblind_value(blinded_value:, blinding_factor:) body = { blinded_value: blinded_value, blinding_factor: blinding_factor } response = post("/privacy/blinding/unblind", body) 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) end def parse_confidential_transaction(data) ConfidentialTransaction.new( id: data["id"], inputs: (data["inputs"] || []).map { |i| parse_confidential_input(i) }, outputs: (data["outputs"] || []).map { |o| parse_confidential_output(o) }, fee: data["fee"], excess: data["excess"], excess_sig: data["excess_sig"], kernel_offset: data["kernel_offset"] ) end def parse_confidential_input(data) ConfidentialTxInput.new( commitment: data["commitment"], blinding_factor: data["blinding_factor"], value: data["value"], key_image: data["key_image"] ) end def parse_confidential_output(data) ConfidentialTxOutput.new( commitment: data["commitment"], blinding_factor: data["blinding_factor"], value: data["value"], recipient_public_key: data["recipient_public_key"], range_proof: data["range_proof"] ) end def parse_stealth_output(data) StealthOutput.new( tx_hash: data["tx_hash"], output_index: data["output_index"], stealth_address: data["stealth_address"], amount: data["amount"], block_height: data["block_height"] ) end end end