Class: Steem::TransactionBuilder

Inherits:
Object
  • Object
show all
Includes:
ChainConfig, Retriable, Utils
Defined in:
lib/steem/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 = Steem::TransactionBuilder.new(wif: wif)
builder.put(vote: {
  voter: 'alice',
  author: 'bob',
  permlink: 'my-burgers',
  weight: 10000
})

trx = builder.transaction
network_broadcast_api = Steem::NetworkBroadcastApi.new
network_broadcast_api.broadcast_transaction_synchronous(trx: trx)

Constant Summary

Constants included from ChainConfig

ChainConfig::EXPIRE_IN_SECS, ChainConfig::EXPIRE_IN_SECS_PROPOSAL, ChainConfig::NETWORKS_STEEM_ADDRESS_PREFIX, ChainConfig::NETWORKS_STEEM_CHAIN_ID, ChainConfig::NETWORKS_STEEM_CORE_ASSET, ChainConfig::NETWORKS_STEEM_DEBT_ASSET, ChainConfig::NETWORKS_STEEM_DEFAULT_NODE, ChainConfig::NETWORKS_STEEM_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

#backoff, #can_retry?

Constructor Details

#initialize(options = {}) ⇒ TransactionBuilder

Returns a new instance of TransactionBuilder.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/steem/transaction_builder.rb', line 28

def initialize(options = {})
  @database_api = options[:database_api] || Steem::DatabaseApi.new(options)
  @block_api = options[:block_api] || Steem::BlockApi.new(options)
  @wif = options[:wif]
  @ref_block_num = options[:ref_block_num]
  @ref_block_prefix = options[:ref_block_prefix]
  @expiration = nil
  @operations = options[:operations] || []
  @extensions = []
  @signatures = []
  @chain = options[:chain] || :steem
  @error_pipe = options[:error_pipe] || STDERR
  @chain_id = case @chain
  when :steem then NETWORKS_STEEM_CHAIN_ID
  when :test then NETWORKS_TEST_CHAIN_ID
  else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
  end
end

Instance Attribute Details

#block_apiObject

Returns the value of attribute block_api.



26
27
28
# File 'lib/steem/transaction_builder.rb', line 26

def block_api
  @block_api
end

#database_apiObject

Returns the value of attribute database_api.



26
27
28
# File 'lib/steem/transaction_builder.rb', line 26

def database_api
  @database_api
end

#expirationObject

Returns the value of attribute expiration.



26
27
28
# File 'lib/steem/transaction_builder.rb', line 26

def expiration
  @expiration
end

#operationsObject

Returns the value of attribute operations.



26
27
28
# File 'lib/steem/transaction_builder.rb', line 26

def operations
  @operations
end

#wifObject

Returns the value of attribute wif.



26
27
28
# File 'lib/steem/transaction_builder.rb', line 26

def wif
  @wif
end

Instance Method Details

#expired?Boolean

Returns:

  • (Boolean)


71
72
73
# File 'lib/steem/transaction_builder.rb', line 71

def expired?
  @expiration.nil? || @expiration < Time.now
end

#inspectObject



47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/steem/transaction_builder.rb', line 47

def inspect
  properties = %w(
    ref_block_num ref_block_prefix expiration operations
    extensions signatures
  ).map do |prop|
    if !!(v = instance_variable_get("@#{prop}"))
      "@#{prop}=#{v}" 
    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.



246
247
248
249
250
# File 'lib/steem/transaction_builder.rb', line 246

def potential_signatures
  @database_api.get_potential_signatures(trx: transaction) do |result|
    result[:keys]
  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:



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

def prepare
  if expired?
    catch :prepare_header do; begin
      @database_api.get_dynamic_global_properties do |properties|
        block_number = properties.last_irreversible_block_num
      
        @block_api.get_block_header(block_num: block_number) do |result|
          header = result.header
          
          @ref_block_num = (block_number - 1) & 0xFFFF
          @ref_block_prefix = unhexlify(header.previous[8..-1]).unpack('V*')[0]
          @expiration = (Time.parse(properties.time + 'Z') + EXPIRE_IN_SECS).utc
        end
      end
    rescue => e
      if can_retry? e
        @error_pipe.puts "#{e} ... retrying."
        throw :prepare_header
      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 = Steem::TransactionBuilder.new
builder.put(vote: vote1).put(vote: vote2)

Returns:



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

def put(type, op = nil)
  @expiration = nil
  
  ## Saving this for later.  This block, or something like it, might replace
  ## API broadcast operation structure.
  # case type
  # when Symbol, String
  #   type_value = "#{type}_operation"
  #   @operations << {type: type_value, value: op}
  # when Hash
  #   type_value = "#{type.keys.first}_operation"
  #   @operations << {type: type_value, value: type.values.first}
  # when Array
  #   type_value = "#{type[0]}_operation"
  #   @operations << {type: type_value, value: type[1]}
  # else
  #   # don't know what to do with it, skipped
  # end
  
  case type
  when Symbol then @operations << [type, op]
  when String then @operations << [type.to_sym, op]
  when Hash then @operations << [type.keys.first.to_sym, type.values.first]
  when Array then @operations << type
  else
    # don't know what to do with it, skipped
  end
  
  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.



257
258
259
260
261
# File 'lib/steem/transaction_builder.rb', line 257

def required_signatures
  @database_api.get_required_signatures(trx: transaction) do |result|
    result[:keys]
  end
end

#resetObject



60
61
62
63
64
65
66
67
68
69
# File 'lib/steem/transaction_builder.rb', line 60

def reset
  @ref_block_num = nil
  @ref_block_prefix = nil
  @expiration = nil
  @operations = []
  @extensions = []
  @signatures = []
  
  self
end

#signHash | TransactionBuilder

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

Returns:



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/steem/transaction_builder.rb', line 198

def sign
  return self unless !!@wif
  return self if expired?
  
  trx = {
    ref_block_num: @ref_block_num,
    ref_block_prefix: @ref_block_prefix, 
    expiration: @expiration.strftime('%Y-%m-%dT%H:%M:%S'),
    operations: @operations,
    extensions: @extensions,
    signatures: @signatures
  }
  
  catch :serialize do; begin
    @database_api.get_transaction_hex(trx: trx) do |result|
      hex = @chain_id + result.hex[0..-4] # Why do we have to chop the last two bytes?
      digest = unhexlify(hex)
      digest_hex = Digest::SHA256.digest(digest)
      private_key = Bitcoin::Key.from_base58 @wif
      public_key_hex = private_key.pub
      ec = Bitcoin::OpenSSL_EC
      count = 0
      sig = nil
      
      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.priv, public_key_hex, false)
        
        next if public_key_hex != ec.recover_compact(digest_hex, sig)
        break if canonical? sig
      end
      
      trx[:signatures] = @signatures = [hexlify(sig)]
    end
  rescue => e
    if can_retry? e
      @error_pipe.puts "#{e} ... retrying."
      throw :serialize
    else
      raise e
    end
  end; end
  
  trx
end

#transactionObject

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

    }



189
190
191
192
# File 'lib/steem/transaction_builder.rb', line 189

def transaction
  prepare
  sign
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.



264
265
266
267
268
# File 'lib/steem/transaction_builder.rb', line 264

def valid?
  @database_api.verify_authority(trx: transaction) do |result|
    result.valid
  end
end