Class: BlockIo::Client

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ Client

Returns a new instance of Client.

Raises:

  • (Exception)


7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/block_io/client.rb', line 7

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"
  @proxy = args[:proxy] || {}
  @keys = {}

  raise Exception.new("Must specify hostname, port, username, password if using a proxy.") if @proxy.keys.size > 0 and [:hostname, :port, :username, :password].any?{|x| !@proxy.key?(x)}

  @conn = ConnectionPool.new(:size => args[:pool_size] || 5) { http = HTTP.headers(:accept => "application/json", :user_agent => "gem:block_io:#{VERSION}");
    http = http.via(args.dig(:proxy, :hostname), args.dig(:proxy, :port), args.dig(:proxy, :username), args.dig(:proxy, :password)) if @proxy.key?(:hostname);
    http = http.persistent("https://#{@hostname}");
    http }
  
  # 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)


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/block_io/client.rb', line 37

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

#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)


93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
# File 'lib/block_io/client.rb', line 93

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)}

  # TODO debug all of this
  
  inputs = data['data']['inputs']
  outputs = data['data']['outputs']

  tx = Bitcoin::Tx.new

  # populate the inputs
  inputs.each do |input|
    tx.in << Bitcoin::TxIn.new(:out_point => Bitcoin::OutPoint.from_txid(input['previous_txid'], input['previous_output_index']))
  end

  # populate the outputs
  outputs.each do |output|
    tx.out << Bitcoin::TxOut.new(:value => (BigDecimal(output['output_value']) * BigDecimal(100000000)).to_i, :script_pubkey => Bitcoin::Script.parse_from_addr(output['receiving_address']))
  end


  # 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'] == 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
  private_keys.each{|key| @keys[key.public_key_hex] = key}
  
  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
    while i < inputs.size do
      input = inputs[i]

      input_address_data = data['data']['input_address_data'].detect{|d| d['address'] == input['spending_address']}
      sighash_for_input = Helper.getSigHashForInput(tx, i, input, input_address_data) # in bytes

      input_address_data['public_keys'].each do |signer_public_key|
        # sign what we can and append signatures to the signatures object
        
        next unless @keys.key?(signer_public_key)
        
        signature = @keys[signer_public_key].sign(sighash_for_input).unpack("H*")[0] # in hex
        signatures << {"input_index" => i, "public_key" => signer_public_key, "signature" => signature}
        
      end

      i += 1 # go to next input
    end
    
  end

  # 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 == 0 ? nil : signatures)}
  
end

#summarize_prepared_transaction(data) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/block_io/client.rb', line 57

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)]

  data['data']['outputs'].each do |output|
    if output['output_category'] == 'blockio-fee' then
      blockio_fees << BigDecimal(output['output_value'])
    elsif output['output_category'] == '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' => '%0.8f' % network_fee,
    "blockio_fee" => '%0.8f' % blockio_fee,
    "total_amount_to_send" => '%0.8f' % output_sum
  }
  
end