# frozen_string_literal: true require "faraday" require "json" module SynorWallet # Synor Wallet SDK Client # # @example # client = SynorWallet::Client.new(api_key: 'your-api-key') # # # Create wallet # result = client.create_wallet # puts result.mnemonic # # # Get balance # balance = client.get_balance(address) # class Client attr_reader :config def initialize(api_key: nil, **options) @config = Config.new(api_key: api_key, **options) raise ArgumentError, "API key is required" unless @config.api_key @conn = Faraday.new(url: @config.base_url) do |f| f.request :json f.response :json f.options.timeout = @config.timeout f.headers["Authorization"] = "Bearer #{@config.api_key}" f.headers["X-SDK-Version"] = "ruby/#{VERSION}" end @closed = false end # ==================== Wallet Operations ==================== # Create a new wallet # # @param type [String] Wallet type (standard, multisig, hardware) # @return [CreateWalletResult] def create_wallet(type: WalletType::STANDARD) check_closed! body = { type: type, network: @config.network } response = with_retry { @conn.post("/wallets", body) } CreateWalletResult.from_hash(response.body) end # Import wallet from mnemonic # # @param mnemonic [String] 12 or 24 word mnemonic phrase # @param passphrase [String, nil] Optional passphrase # @return [Wallet] def import_wallet(mnemonic, passphrase: nil) check_closed! body = { mnemonic: mnemonic, passphrase: passphrase, network: @config.network } response = with_retry { @conn.post("/wallets/import", body) } Wallet.from_hash(response.body) end # Get wallet by ID # # @param wallet_id [String] # @return [Wallet] def get_wallet(wallet_id) check_closed! response = with_retry { @conn.get("/wallets/#{wallet_id}") } Wallet.from_hash(response.body) end # Generate a new address # # @param wallet_id [String] # @param is_change [Boolean] # @return [Address] def generate_address(wallet_id, is_change: false) check_closed! body = { is_change: is_change } response = with_retry { @conn.post("/wallets/#{wallet_id}/addresses", body) } Address.from_hash(response.body) end # Get stealth address # # @param wallet_id [String] # @return [StealthAddress] def get_stealth_address(wallet_id) check_closed! response = with_retry { @conn.get("/wallets/#{wallet_id}/stealth-address") } StealthAddress.from_hash(response.body) end # ==================== Signing Operations ==================== # Sign a transaction # # @param wallet_id [String] # @param transaction [Transaction] # @return [SignedTransaction] def sign_transaction(wallet_id, transaction) check_closed! body = { wallet_id: wallet_id, transaction: transaction.to_h } response = with_retry { @conn.post("/transactions/sign", body) } SignedTransaction.from_hash(response.body) end # Sign a message # # @param wallet_id [String] # @param message [String] # @param address_index [Integer] # @return [Signature] def sign_message(wallet_id, message, address_index: 0) check_closed! body = { wallet_id: wallet_id, message: message, address_index: address_index } response = with_retry { @conn.post("/messages/sign", body) } Signature.from_hash(response.body) end # Verify a message signature # # @param message [String] # @param signature [String] # @param address [String] # @return [Boolean] def verify_message(message, signature, address) check_closed! body = { message: message, signature: signature, address: address } response = with_retry { @conn.post("/messages/verify", body) } response.body["valid"] end # ==================== Balance & UTXOs ==================== # Get balance for an address # # @param address [String] # @return [Balance] def get_balance(address) check_closed! response = with_retry { @conn.get("/addresses/#{address}/balance") } Balance.from_hash(response.body) end # Get UTXOs for an address # # @param address [String] # @param min_confirmations [Integer] # @return [Array] def get_utxos(address, min_confirmations: 1) check_closed! response = with_retry do @conn.get("/addresses/#{address}/utxos", min_confirmations: min_confirmations) end response.body.map { |u| UTXO.from_hash(u) } end # ==================== Fee Estimation ==================== # Estimate fee # # @param priority [String] # @return [Integer] Fee per byte in satoshis def estimate_fee(priority: Priority::MEDIUM) check_closed! response = with_retry { @conn.get("/fees/estimate", priority: priority) } response.body["fee_per_byte"] end # ==================== Lifecycle ==================== def close @closed = true @conn.close if @conn.respond_to?(:close) end def closed? @closed end private def check_closed! raise ClientClosedError, "Client has been closed" if @closed end def with_retry attempts = 0 begin attempts += 1 response = yield handle_response(response) response rescue Faraday::Error, ApiError => e if attempts < @config.retries sleep(attempts) retry end raise end end def handle_response(response) return if response.success? error_message = response.body["error"] || response.body["message"] || "Unknown error" raise ApiError.new(error_message, status_code: response.status) end end end