Class: BlockIo::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/block_io/client.rb

Constant Summary collapse

USER_AGENT =
'gem:block_io:' << VERSION.to_s
CONTENT_TYPE =
'application/json; charset=UTF-8'
ACCEPT_TYPE =
'application/json'
KEEP_ALIVE =
'Keep-Alive'
TIMEOUT =
60

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ Client

Returns a new instance of Client.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(m, *args) ⇒ Object

Raises:

  • (Exception)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 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

Instance Attribute Details

#api_keyObject (readonly)

Returns the value of attribute api_key.



5
6
7
# File 'lib/block_io/client.rb', line 5

def api_key
  @api_key
end

#api_request_headersObject (readonly)

Returns the value of attribute api_request_headers.



5
6
7
# File 'lib/block_io/client.rb', line 5

def api_request_headers
  @api_request_headers
end

#networkObject (readonly)

Returns the value of attribute network.



5
6
7
# File 'lib/block_io/client.rb', line 5

def network
  @network
end

#versionObject (readonly)

Returns the value of attribute version.



5
6
7
# File 'lib/block_io/client.rb', line 5

def version
  @version
end

Instance Method Details

#create_and_sign_transaction(data, keys = []) ⇒ Object

Raises:

  • (Exception)


115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# 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 [email protected].') 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

#padded_f(d) ⇒ Object



67
68
69
70
71
72
# 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) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# 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