class BlockIo::Client
Constants
- ACCEPT_TYPE
- CONTENT_TYPE
- KEEP_ALIVE
- TIMEOUT
- USER_AGENT
Attributes
api_key[R]
api_request_headers[R]
network[R]
version[R]
Public Class Methods
new(args = {})
click to toggle source
# File lib/block_io/client.rb, line 13 def initialize(args = {}) # api_key # pin # version # hostname # proxy # pool_size # keys raise 'Must provide an API Key.' unless args.key?(:api_key) and args[:api_key].to_s.size > 0 @api_key = args[:api_key] @pin = args[:pin] @version = args[:version] || 2 @hostname = args[:hostname] || 'block.io' @keys = {} # prepare proxy settings if provided proxy = args[:proxy] || {} if proxy.keys.size > 0 then raise Exception.new('Must specify hostname, port, username, password if using a proxy.') if [:url, :username, :password].any?{|x| !proxy.key?(x)} @proxy = {:proxy => proxy[:url], :proxyuserpwd => "#{proxy[:username]}:#{proxy[:password]}"}.freeze else @proxy = {} end @api_request_headers = {'User-Agent' => USER_AGENT, 'Content-Type' => CONTENT_TYPE, 'Accept' => ACCEPT_TYPE, 'Expect' => '', 'Connection' => KEEP_ALIVE, 'Host' => @hostname}.freeze # this will get populated after a successful API call @network = nil end
Public Instance Methods
create_and_sign_transaction(data, keys = [])
click to toggle source
# File lib/block_io/client.rb, line 115 def create_and_sign_transaction(data, keys = []) # takes data from prepare_transaction, prepare_dtrust_transaction, prepare_sweep_transaction # creates the transaction given the inputs and outputs from data # signs the transaction using keys (if not provided, decrypts the key using the PIN) set_network(data['data']['network']) if data['data'].key?('network') raise 'Data must be contain one or more inputs' unless data['data']['inputs'].size > 0 raise 'Data must contain one or more outputs' unless data['data']['outputs'].size > 0 raise 'Data must contain information about addresses' unless data['data']['input_address_data'].size > 0 # TODO make stricter private_keys = keys.map{|x| Key.from_private_key_hex(x)} inputs = data['data']['inputs'] outputs = data['data']['outputs'] tx = Bitcoin::Tx.new # populate the inputs tx.in << inputs.map do |input| Bitcoin::TxIn.new(:out_point => Bitcoin::OutPoint.from_txid(input['previous_txid'], input['previous_output_index'])) end tx.in.flatten! # populate the outputs tx.out << outputs.map do |output| Bitcoin::TxOut.new(:value => (BigDecimal(output['output_value']) * BigDecimal(100000000)).to_i, :script_pubkey => Bitcoin::Script.parse_from_addr(output['receiving_address'])) end tx.out.flatten! # some protection against misbehaving machines and/or code raise Exception.new('Expected unsigned transaction ID mismatch. Please report this error to support@block.io.') unless (data['data']['expected_unsigned_txid'].nil? or data['data']['expected_unsigned_txid'].eql?(tx.txid)) # extract key encrypted_key = data['data']['user_key'] if !encrypted_key.nil? and !@keys.key?(encrypted_key['public_key']) then # decrypt the key with PIN raise Exception.new('PIN not set and no keys provided. Cannot sign transaction.') unless !@pin.nil? or @keys.size > 0 key = Helper.dynamicExtractKey(encrypted_key, @pin) raise Exception.new('Public key mismatch for requested signer and ourselves. Invalid Secret PIN detected.') unless key.public_key_hex.eql?(encrypted_key['public_key']) # store this key for later use @keys[key.public_key_hex] = key end # store the provided keys, if any, for later use @keys.merge!(private_keys.map{|key| [key.public_key_hex, key]}.to_h) signatures = [] if @keys.size > 0 then # try to sign whatever we can here and give the user the data back # Block.io will check to see if all signatures are present, or return an error otherwise saying insufficient signatures provided i = 0 loop do input = inputs[i] break if input.nil? input_address_data = data['data']['input_address_data'].detect{|d| d['address'].eql?(input['spending_address'])} sighash_for_input = Helper.getSigHashForInput(tx, i, input, input_address_data) # in bytes signatures << input_address_data['public_keys'].map do |signer_public_key| # sign what we can and append signatures to the signatures object next unless @keys.key?(signer_public_key) { 'input_index' => i, 'public_key' => signer_public_key, 'signature' => @keys[signer_public_key].sign(sighash_for_input).unpack1('H*') # in hex } end i += 1 # go to next input end end signatures.flatten! signatures.compact! # if we have everything we need for this transaction, just finalize the transaction if Helper.allSignaturesPresent?(tx, inputs, signatures, data['data']['input_address_data']) then Helper.finalizeTransaction(tx, inputs, signatures, data['data']['input_address_data']) signatures = [] # no signatures left to append end # reset keys @keys = {} # the response for submitting the transaction {'tx_type' => data['data']['tx_type'], 'tx_hex' => tx.to_hex, 'signatures' => (signatures.size.eql?(0) ? nil : signatures)} end
method_missing(m, *args)
click to toggle source
# File lib/block_io/client.rb, line 47 def method_missing(m, *args) method_name = m.to_s raise Exception.new('Must provide arguments as a Hash.') unless args.size <= 1 and args.all?{|x| x.is_a?(Hash)} raise Exception.new('Parameter keys must be symbols. For instance: :label => "default" instead of "label" => "default"') unless args[0].nil? or args[0].keys.all?{|x| x.is_a?(Symbol)} raise Exception.new('Cannot pass PINs to any calls. PINs can only be set when initiating this library.') if !args[0].nil? and args[0].key?(:pin) raise Exception.new('Do not specify API Keys here. Initiate a new BlockIo object instead if you need to use another API Key.') if !args[0].nil? and args[0].key?(:api_key) if method_name.eql?('prepare_sweep_transaction') then # we need to ensure @network is set before we allow this # we need to send only the public key, not the given private key # we're sweeping from an address internal_prepare_sweep_transaction(args[0], method_name) else api_call({:method_name => method_name, :params => args[0] || {}}) end end
padded_f(d)
click to toggle source
# File lib/block_io/client.rb, line 67 def padded_f(d) # returns a padded decimal to 8 decimal places b = BigDecimal(d).to_s('F') b << '0' * (8 - (b.size - b.index('.') - 1)) b end
summarize_prepared_transaction(data)
click to toggle source
# File lib/block_io/client.rb, line 74 def summarize_prepared_transaction(data) # takes the response from prepare_transaction/prepare_dtrust_transaction/prepare_sweep_transaction # returns the network fee being paid, the blockio fee being paid, amounts being sent input_sum = data['data']['inputs'].map{|input| BigDecimal(input['input_value'])}.inject(:+) output_values = [BigDecimal(0)] blockio_fees = [BigDecimal(0)] change_amounts = [BigDecimal(0)] i = 0 loop do output = data['data']['outputs'][i] break if output.nil? i += 1 if output['output_category'].eql?('blockio-fee') then blockio_fees << BigDecimal(output['output_value']) elsif output['output_category'].eql?('change') then change_amounts << BigDecimal(output['output_value']) else # user-specified output_values << BigDecimal(output['output_value']) end end output_sum = output_values.inject(:+) blockio_fee = blockio_fees.inject(:+) change_amount = change_amounts.inject(:+) network_fee = input_sum - output_sum - blockio_fee - change_amount { 'network' => data['data']['network'], 'network_fee' => padded_f(network_fee), 'blockio_fee' => padded_f(blockio_fee), 'total_amount_to_send' => padded_f(output_sum) } end
Private Instance Methods
api_call(args)
click to toggle source
# File lib/block_io/client.rb, line 242 def api_call(args) response = Typhoeus.post("https://#{@hostname}/api/v#{@version}/#{args[:method_name]}", :body => Oj.dump(args[:params].merge({:api_key => @api_key}), :mode => :compat), :headers => @api_request_headers, :timeout => TIMEOUT, **@proxy) body = nil if response.timed_out? then body = {'status' => 'fail', 'data' => {'error_message' => 'Request timed out.'}} elsif response.code.eql?(0) then body = {'status' => 'fail', 'data' => {'error_message' => 'No response received.'}} else begin body = Oj.safe_load(response.body.to_s) rescue Exception => e body = {'status' => 'fail', 'data' => {'error_message' => "Unknown error occurred. Please report this to support@block.io. Status #{response.code}."}} end end if !body['status'].eql?('success') then # raise an exception on error for easy handling # user can extract raw response using e.raw_data e = APIException.new(body['data']['error_message'].to_s) e.set_raw_data(body) raise e end # success set_network(body['data']['network']) if body['data'].key?('network') body end
internal_prepare_sweep_transaction(args = {}, method_name = 'prepare_sweep_transaction')
click to toggle source
# File lib/block_io/client.rb, line 218 def internal_prepare_sweep_transaction(args = {}, method_name = 'prepare_sweep_transaction') # set the network first if not already known api_call({:method_name => 'get_balance', :params => {}}) if @network.nil? raise Exception.new('No private_key provided.') unless args.key?(:private_key) and (args[:private_key] || '').size > 0 # ensure the private key never goes to Block.io key = Key.from_wif(args[:private_key]) sanitized_args = args.merge({:public_key => key.public_key_hex}) sanitized_args.delete(:private_key) @keys[key.public_key_hex] = key # store this in our set of keys for later use api_call({:method_name => method_name, :params => sanitized_args}) end
set_network(network)
click to toggle source
# File lib/block_io/client.rb, line 236 def set_network(network) # load the chain_params for this network @network ||= network Bitcoin.chain_params = @network unless @network.to_s.size == 0 end