Class: Hive::TransactionBuilder
- Inherits:
-
Object
- Object
- Hive::TransactionBuilder
- 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
-
#app_base ⇒ Object
(also: #app_base?)
Returns the value of attribute app_base.
-
#block_api ⇒ Object
Returns the value of attribute block_api.
-
#database_api ⇒ Object
Returns the value of attribute database_api.
-
#force_serialize ⇒ Object
(also: #force_serialize?)
readonly
Returns the value of attribute force_serialize.
-
#operations ⇒ Object
Returns the value of attribute operations.
-
#signed ⇒ Object
readonly
Returns the value of attribute signed.
-
#testnet ⇒ Object
(also: #testnet?)
readonly
Returns the value of attribute testnet.
-
#wif ⇒ Object
writeonly
Sets the attribute wif.
Instance Method Summary collapse
- #expiration ⇒ Object
- #expiration=(value) ⇒ Object
-
#initialize(options = {}) ⇒ TransactionBuilder
constructor
A new instance of TransactionBuilder.
- #inspect ⇒ Object
-
#potential_signatures ⇒ Array
All public keys that could possibly sign for a given transaction.
-
#prepare ⇒ TransactionBuilder
If the transaction can be prepared, this method will do so and set the expiration.
-
#put(type, op = nil) ⇒ TransactionBuilder
A quick and flexible way to append a new operation to the transaction.
-
#required_signatures ⇒ Array
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.
- #reset ⇒ Object
-
#sign ⇒ Transaction
Appends to the ‘signatures` array of the transaction, built from a serialized digest.
-
#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.
- #transaction_hex ⇒ Object
-
#valid? ⇒ Boolean
True if the transaction has all of the required signatures.
Methods included from Utils
Methods included from Retriable
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( = {}) @app_base = !![:app_base] # default false @database_api = [:database_api] @block_api = [:block_api] if app_base? @database_api ||= Hive::DatabaseApi.new() @block_api ||= Hive::BlockApi.new() else @database_api ||= Hive::CondenserApi.new() @block_api ||= Hive::CondenserApi.new() end @wif = [[:wif]].flatten @signed = false @testnet = !![:testnet] @force_serialize = !![:force_serialize] if !!(trx = [:trx]) trx = case trx when String then JSON[trx] else; trx end @trx = Transaction.new(trx) end @trx ||= Transaction.new @chain = [:chain] || :hive @error_pipe = [:error_pipe] || STDERR @chain_id = [: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_base ⇒ Object 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_api ⇒ Object
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_api ⇒ Object
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_serialize ⇒ Object (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 |
#operations ⇒ Object
Returns the value of attribute operations.
31 32 33 |
# File 'lib/hive/transaction_builder.rb', line 31 def operations @operations end |
#signed ⇒ Object (readonly)
Returns the value of attribute signed.
33 34 35 |
# File 'lib/hive/transaction_builder.rb', line 33 def signed @signed end |
#testnet ⇒ Object (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
32 33 34 |
# File 'lib/hive/transaction_builder.rb', line 32 def wif=(value) @wif = value end |
Instance Method Details
#expiration ⇒ Object
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 |
#inspect ⇒ Object
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_signatures ⇒ Array
Returns 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 |
#prepare ⇒ TransactionBuilder
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.
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)
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_signatures ⇒ Array
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.
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 |
#reset ⇒ Object
98 99 100 101 102 103 |
# File 'lib/hive/transaction_builder.rb', line 98 def reset @trx = Transaction.new @signed = false self end |
#sign ⇒ Transaction
Appends to the ‘signatures` array of the transaction, built from a serialized digest.
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.
222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/hive/transaction_builder.rb', line 222 def transaction( = {prepare: true, sign: true}) [:prepare] = true unless .has_key? :prepare [:sign] = true unless .has_key? :sign prepare if !![:prepare] if !![:sign] sign else @trx end end |
#transaction_hex ⇒ Object
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.
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? = if app_base? {trx: transaction} else transaction end @database_api.() do |result| if app_base? result.valid else result end end end |