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
269 lines
8 KiB
Ruby
269 lines
8 KiB
Ruby
# 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
|