Class: Hive::TransactionBuilder

Inherits:
Object
  • Object
show all
Includes:
ChainConfig, Retriable, Utils
Defined in:
lib/hive/transaction_builder.rb

Overview

TransactionBuilder can be used to create a transaction that the NetworkBroadcastApi can broadcast to the rest of the platform. The main feature of this class is the ability to cryptographically sign the transaction so that it conforms to the consensus rules that are required by the blockchain.

wif = '5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC'
builder = Hive::TransactionBuilder.new(wif: wif)
builder.put(vote: {
  voter: 'alice',
  author: 'bob',
  permlink: 'my-burgers',
  weight: 10000
})

trx = builder.transaction
network_broadcast_api = Hive::CondenserApi.new
network_broadcast_api.broadcast_transaction_synchronous(trx: trx)

The ‘wif` value may also be an array, when signing with multiple signatures (multisig).

Constant Summary collapse

MAX_CANONICAL_SIGNATURE_ATTEMPTS =
100

Constants included from ChainConfig

ChainConfig::EXPIRE_IN_SECS, ChainConfig::EXPIRE_IN_SECS_PROPOSAL, ChainConfig::NETWORKS_HIVE_ADDRESS_PREFIX, ChainConfig::NETWORKS_HIVE_CHAIN_ID, ChainConfig::NETWORKS_HIVE_CORE_ASSET, ChainConfig::NETWORKS_HIVE_DEBT_ASSET, ChainConfig::NETWORKS_HIVE_DEFAULT_NODE, ChainConfig::NETWORKS_HIVE_LEGACY_CHAIN_ID, ChainConfig::NETWORKS_HIVE_VEST_ASSET, ChainConfig::NETWORKS_TEST_ADDRESS_PREFIX, ChainConfig::NETWORKS_TEST_CHAIN_ID, ChainConfig::NETWORKS_TEST_CORE_ASSET, ChainConfig::NETWORKS_TEST_DEBT_ASSET, ChainConfig::NETWORKS_TEST_DEFAULT_NODE, ChainConfig::NETWORKS_TEST_VEST_ASSET, ChainConfig::NETWORK_CHAIN_IDS

Constants included from Retriable

Retriable::MAX_BACKOFF, Retriable::MAX_RETRY_COUNT, Retriable::MAX_RETRY_ELAPSE, Retriable::RETRYABLE_EXCEPTIONS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Utils

#hexlify, #unhexlify

Methods included from Retriable

#can_retry?

Constructor Details

#initialize(options = {}) ⇒ TransactionBuilder

Returns a new instance of TransactionBuilder.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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
# File 'lib/hive/transaction_builder.rb', line 39

def initialize(options = {})
  @app_base = !!options[:app_base] # default false
  @database_api = options[:database_api]
  @block_api = options[:block_api]
  
  if app_base?
    @database_api ||= Hive::DatabaseApi.new(options)
    @block_api ||= Hive::BlockApi.new(options)
  else
    @database_api ||= Hive::CondenserApi.new(options)
    @block_api ||= Hive::CondenserApi.new(options)
  end
  
  @wif = [options[:wif]].flatten
  @signed = false
  @testnet = !!options[:testnet]
  @force_serialize = !!options[:force_serialize]
  
  if !!(trx = options[:trx])
    trx = case trx
    when String then JSON[trx]
    else; trx
    end
    
    @trx = Transaction.new(trx)
  end
  
  @trx ||= Transaction.new
  @chain = options[:chain] || :hive
  @error_pipe = options[:error_pipe] || STDERR
  @chain_id = options[:chain_id] || ENV['HIVE_CHAIN_ID']
  
  @network_chain_id ||= case @chain
  when :hive then @database_api.get_config{|config| config['HIVE_CHAIN_ID']} rescue NETWORKS_HIVE_CHAIN_ID
  when :test then @database_api.get_config{|config| config['HIVE_CHAIN_ID']} rescue NETWORKS_TEST_CHAIN_ID
  else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
  end
  
  @chain_id ||= @network_chain_id
  
  if testnet? && (@chain_id == NETWORKS_HIVE_CHAIN_ID || @chain_id == NETWORKS_HIVE_LEGACY_CHAIN_ID)
    raise UnsupportedChainError, "Unsupported testnet chain id: #{@chain_id}"
  end
  
  if @chain_id != @network_chain_id
    raise UnsupportedChainError, "Unsupported chain id (expected: #{@chain_id}, network was: #{@network_chain_id})"
  end
end

Instance Attribute Details

#app_baseObject Also known as: app_base?

Returns the value of attribute app_base.



31
32
33
# File 'lib/hive/transaction_builder.rb', line 31

def app_base
  @app_base
end

#block_apiObject

Returns the value of attribute block_api.



31
32
33
# File 'lib/hive/transaction_builder.rb', line 31

def block_api
  @block_api
end

#database_apiObject

Returns the value of attribute database_api.



31
32
33
# File 'lib/hive/transaction_builder.rb', line 31

def database_api
  @database_api
end

#force_serializeObject (readonly) Also known as: force_serialize?

Returns the value of attribute force_serialize.



33
34
35
# File 'lib/hive/transaction_builder.rb', line 33

def force_serialize
  @force_serialize
end

#operationsObject

Returns the value of attribute operations.



31
32
33
# File 'lib/hive/transaction_builder.rb', line 31

def operations
  @operations
end

#signedObject (readonly)

Returns the value of attribute signed.



33
34
35
# File 'lib/hive/transaction_builder.rb', line 33

def signed
  @signed
end

#testnetObject (readonly) Also known as: testnet?

Returns the value of attribute testnet.



33
34
35
# File 'lib/hive/transaction_builder.rb', line 33

def testnet
  @testnet
end

#wif=(value) ⇒ Object (writeonly)

Sets the attribute wif

Parameters:

  • value

    the value to set the attribute wif to.



32
33
34
# File 'lib/hive/transaction_builder.rb', line 32

def wif=(value)
  @wif = value
end

Instance Method Details

#expirationObject



105
106
107
# File 'lib/hive/transaction_builder.rb', line 105

def expiration
  @trx.expiration
end

#expiration=(value) ⇒ Object



109
110
111
112
# File 'lib/hive/transaction_builder.rb', line 109

def expiration=(value)
  @trx.expiration = value
  @signed = false
end

#inspectObject



88
89
90
91
92
93
94
95
96
# File 'lib/hive/transaction_builder.rb', line 88

def inspect
  properties = %w(trx).map do |prop|
    if !!(v = instance_variable_get("@#{prop}"))
      "@#{prop}=#{v.inspect}" 
    end
  end.compact.join(', ')
  
  "#<#{self.class.name} [#{properties}]>"
end

#potential_signaturesArray

Returns All public keys that could possibly sign for a given transaction.

Returns:

  • (Array)

    All public keys that could possibly sign for a given transaction.



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/hive/transaction_builder.rb', line 352

def potential_signatures
  potential_signatures_args = if app_base?
    {trx: transaction}
  else
    transaction
  end
  
  @database_api.get_potential_signatures(potential_signatures_args) do |result|
    if app_base?
      result[:keys]
    else
      result
    end
  end
end

#prepareTransactionBuilder

If the transaction can be prepared, this method will do so and set the expiration. Once the expiration is set, it will not re-prepare. If you call #put, the expiration is set Nil so that it can be re-prepared.

Usually, this method is called automatically by #put and/or #transaction.

Returns:



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
# File 'lib/hive/transaction_builder.rb', line 121

def prepare
  if @trx.expired?
    loop do
      begin
        properties = nil
        header = nil
        block_number = nil

        @database_api.get_dynamic_global_properties do |result|
          properties = result
          nil
        end

        block_number = properties.last_irreversible_block_num
        block_header_args = if app_base?
          {block_num: block_number}
        else
          block_number
        end

        @block_api.get_block_header(block_header_args) do |result|
          header = if app_base?
            result.header
          else
            result
          end
          nil
        end

        @trx.ref_block_num = (block_number - 1) & 0xFFFF
        @trx.ref_block_prefix = unhexlify(header.previous[8..-1]).unpack('V*')[0]
        @trx.expiration = (Time.parse(properties.time + 'Z') + EXPIRE_IN_SECS).utc
        break
      rescue => e
        if can_retry? e
          @error_pipe.puts "#{e} ... retrying."
          next
        else
          raise e
        end
      end
    end
  end

  self
end

#put(type, op = nil) ⇒ TransactionBuilder

A quick and flexible way to append a new operation to the transaction. This method uses ducktyping to figure out how to form the operation.

There are three main ways you can call this method. These assume that ‘op_type` is a Symbol (or String) representing the type of operation and `op` is the operation Hash.

put(op_type, op)

… or …

put(op_type => op)

… or …

put([op_type, op])

You can also chain multiple operations:

builder = Hive::TransactionBuilder.new
builder.put(vote: vote1).put(vote: vote2)

Returns:



197
198
199
200
201
202
# File 'lib/hive/transaction_builder.rb', line 197

def put(type, op = nil)
  @trx.expiration = nil
  @trx.operations << normalize_operation(type, op)
  prepare
  self
end

#required_signaturesArray

This API will take a partially signed transaction and a set of public keys that the owner has the ability to sign for and return the minimal subset of public keys that should add signatures to the transaction.

Returns:

  • (Array)

    The minimal subset of public keys that should add signatures to the transaction.



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/hive/transaction_builder.rb', line 373

def required_signatures
  required_signatures_args = if app_base?
    {trx: transaction}
  else
    [transaction, []]
  end
  
  @database_api.get_required_signatures(*required_signatures_args) do |result|
    if app_base?
      result[:keys]
    else
      result
    end
  end
end

#resetObject



98
99
100
101
102
103
# File 'lib/hive/transaction_builder.rb', line 98

def reset
  @trx = Transaction.new
  @signed = false
  
  self
end

#signTransaction

Appends to the ‘signatures` array of the transaction, built from a serialized digest.

Returns:

  • (Transaction)

    The transaction payload. Even when signing is skipped (for example due to missing wif or an expired transaction), callers expect a concrete transaction object rather than the builder instance itself.



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/hive/transaction_builder.rb', line 241

def sign
  return @trx if @wif.empty?
  return @trx if @trx.expired?
  
  unless @signed
    catch :serialize do; begin
      transaction_hex.tap do |result|
        hex = if app_base?
          result.hex
        else
          result
        end

        unless force_serialize?
          derrived_trx = Transaction.new(hex: hex)
          derrived_ops = derrived_trx.operations
          derrived_trx.operations = derrived_ops.map do |op|
            op_name = if app_base?
              op[:type].to_sym
            else
              op[:type].to_s.sub(/_operation$/, '').to_sym
            end
            
            normalize_operation op_name, JSON[op[:value].to_json]
          end
          
          unless @trx == derrived_trx
            if defined? JsonCompare
              raise SerializationMismatchError, JSON.pretty_generate({trx: @trx, derrived_trx: derrived_trx})
            else
              raise SerializationMismatchError
            end
          end
        end
        
        hex = hex[0..-4] # drop empty signature array
        @trx.id = Digest::SHA256.hexdigest(unhexlify(hex))[0..39]
        
        hex = @chain_id + hex
        digest = unhexlify(hex)
        digest_hex = Digest::SHA256.digest(digest)
        legacy_bitcoin_ruby_signer = ENV['HIVE_USE_LEGACY_BITCOIN_RUBY_SIGNER'] == '1'
        private_keys = @wif.map do |wif|
          if legacy_bitcoin_ruby_signer
            Bitcoin::Key.from_base58(wif)
          else
            SigningKey.from_base58(wif)
          end
        end
        ec = legacy_bitcoin_ruby_signer ? Bitcoin::OpenSSL_EC : CompactSigner.default
        count = 0
        
        private_keys.each do |private_key|
          sig = nil
          public_key_hex = private_key.pub
          private_key_hex = private_key.respond_to?(:private_key_hex) ? private_key.private_key_hex : private_key.priv
          compressed = private_key.respond_to?(:compressed) ? private_key.compressed : false

          unless legacy_bitcoin_ruby_signer
            sig = ec.sign_compact(digest_hex, private_key_hex, public_key_hex, compressed)
          else
            loop do
              count += 1
              @error_pipe.puts "#{count} attempts to find canonical signature" if count % 40 == 0
              sig = ec.sign_compact(digest_hex, private_key_hex, public_key_hex, compressed)
              
              break if public_key_hex == ec.recover_compact(digest_hex, sig) && canonical?(sig)

              if count >= MAX_CANONICAL_SIGNATURE_ATTEMPTS
                raise Hive::BaseError, "Unable to find canonical signature after #{MAX_CANONICAL_SIGNATURE_ATTEMPTS} attempts"
              end
            end
          end
          
          @trx.signatures << hexlify(sig)
        end
        
        @signed = true
      end
    rescue => e
      if can_retry? e
        @error_pipe.puts "#{e} ... retrying."
        throw :serialize
      else
        raise e
      end
    end; end
  end
    
  @trx
end

#transaction(options = {prepare: true, sign: true}) ⇒ Object

If all of the required values are set, this returns a fully formed transaction that is ready to broadcast.

Returns:

  • {

       :ref_block_num => 18912,
    :ref_block_prefix => 575781536,
          :expiration => "2018-04-26T15:26:12",
          :extensions => [],
          :operations => [[:vote, {
               :voter => "alice",
              :author => "bob",
            :permlink => "my-burgers",
              :weight => 10000
            }
        ]],
          :signatures => ["1c45b65740b4b2c17c4bcf6bcc3f8d90ddab827d50532729fc3b8f163f2c465a532b0112ae4bf388ccc97b7c2e0bc570caadda78af48cf3c261037e65eefcd941e"]
    

    }



222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/hive/transaction_builder.rb', line 222

def transaction(options = {prepare: true, sign: true})
  options[:prepare] = true unless options.has_key? :prepare
  options[:sign] = true unless options.has_key? :sign
  
  prepare if !!options[:prepare]
  
  if !!options[:sign]
    sign
  else
    @trx
  end
end

#transaction_hexObject



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/hive/transaction_builder.rb', line 333

def transaction_hex
  trx = transaction(prepare: true, sign: false)
  
  transaction_hex_args = if app_base?
    {trx: trx}
  else
    trx
  end
  
  @database_api.get_transaction_hex(transaction_hex_args) do |result|
    if app_base?
      result[:hex]
    else
      result
    end
  end
end

#valid?Boolean

Returns True if the transaction has all of the required signatures.

Returns:

  • (Boolean)

    True if the transaction has all of the required signatures.



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/hive/transaction_builder.rb', line 390

def valid?
  verify_authority_args = if app_base?
    {trx: transaction}
  else
    transaction
  end
  
  @database_api.verify_authority(verify_authority_args) do |result|
    if app_base?
      result.valid
    else
      result
    end
  end
end