class Radiator::Transaction

Constants

VALID_OPTIONS

Public Class Methods

new(options = {}) click to toggle source
# File lib/radiator/transaction.rb, line 18
def initialize(options = {})
  options = options.dup
  options.each do |k, v|
    k = k.to_sym
    if VALID_OPTIONS.include?(k.to_sym)
      options.delete(k)
      send("#{k}=", v)
    end
  end

  @url = options[:url] || url
  @chain ||= 'hive'
  @chain = @chain.to_sym
  @chain_id = chain_id options[:chain_id]
  @operations = options[:operations] || []
  
  @self_logger = false
  @logger = if options[:logger].nil?
    @self_logger = true
    Radiator.logger
  else
    options[:logger]
  end
  
  unless NETWORK_CHAIN_IDS.include? @chain_id
    warning "Unknown chain id: #{@chain_id}"
  end

  if !!wif && !!private_key
    raise TransactionError, "Do not pass both wif and private_key.  That's confusing."
  end

  if !!wif
    @private_key = Bitcoin::Key.from_base58 wif
  end

  @ref_block_num ||= nil
  @ref_block_prefix ||= nil
  @expiration ||= nil
  @immutable_expiration = !!@expiration

  options = options.merge(
    url: @url,
    chain: @chain,
    pool_size: 1,
    persist: false,
    reuse_ssl_sessions: false
  )

  @api = Api.new(options)
  @network_broadcast_api = NetworkBroadcastApi.new(options)
  
  @use_condenser_namespace = if options.keys.include? :use_condenser_namespace
    options[:use_condenser_namespace]
  else
    true
  end
  
  ObjectSpace.define_finalizer(self, self.class.finalize(@api, @network_broadcast_api, @self_logger, @logger))
end

Private Class Methods

finalize(api, network_broadcast_api, self_logger, logger) click to toggle source
# File lib/radiator/transaction.rb, line 320
def self.finalize(api, network_broadcast_api, self_logger, logger)
  proc {
    if !!api && !api.stopped?
      puts "DESTROY: #{api.inspect}" if ENV['LOG'] == 'TRACE'
      api.shutdown
      api = nil
    end

    if !!network_broadcast_api && !network_broadcast_api.stopped?
      puts "DESTROY: #{network_broadcast_api.inspect}" if ENV['LOG'] == 'TRACE'
      network_broadcast_api.shutdown
      network_broadcast_api = nil
    end

    begin
      if self_logger
        if !!logger && defined?(logger.close)
          if defined?(logger.closed?)
            logger.close unless logger.closed?
          end
        end
      end
    rescue IOError, NoMethodError => _; end
  }
end

Public Instance Methods

chain_id(chain_id = nil) click to toggle source
# File lib/radiator/transaction.rb, line 79
def chain_id(chain_id = nil)
  return chain_id if !!chain_id

  case chain.to_s.downcase.to_sym
  when :steem then NETWORKS_STEEM_CHAIN_ID
  when :hive
    database_api = Hive::DatabaseApi.new(url: @url)
    database_api.get_config do |config|
      config['HIVE_CHAIN_ID']
    end rescue nil || NETWORKS_HIVE_CHAIN_ID
  when :test then NETWORKS_TEST_CHAIN_ID
  end
end
inspect() click to toggle source
# File lib/radiator/transaction.rb, line 177
def inspect
  properties = %w(
    url ref_block_num ref_block_prefix expiration chain
    use_condenser_namespace immutable_expiration payload
  ).map do |prop|
    if !!(v = instance_variable_get("@#{prop}"))
      "@#{prop}=#{v}" 
    end
  end.compact.join(', ')
  
  "#<#{self.class.name} [#{properties}]>"
end
operations() click to toggle source
# File lib/radiator/transaction.rb, line 147
def operations
  @operations = @operations.map do |op|
    case op
    when Operation then op
    else; Operation.new(op.merge(chain: @chain))
    end
  end
end
operations=(operations) click to toggle source
# File lib/radiator/transaction.rb, line 156
def operations=(operations)
  @operations = operations
end
process(broadcast = false) click to toggle source
# File lib/radiator/transaction.rb, line 101
def process(broadcast = false)
  prepare

  if broadcast
    loop do
      response = broadcast_payload(payload)

      if !!response.error
        parser = ErrorParser.new(response)

        if parser.can_reprepare?
          debug "Error code: #{parser}, repreparing transaction ..."
          prepare
          redo
        end
      end

      return response
    end
  else
    self
  end
rescue OperationError => e
  trx_builder, network_api = case @chain.to_sym
  when :steem then [
    Steem::TransactionBuilder.new(wif: @wif),
    Steem::NetworkBroadcastApi.new(url: @url)
  ]
  when :hive then [
    Hive::TransactionBuilder.new(wif: @wif),
    Hive::NetworkBroadcastApi.new(url: @url)
  ]
  end
  
  raise e if trx_builder.nil?
  
  @operations.each do |op|
    type = op.delete(:type)
    trx_builder.put({type => op})
  end
  
  network_api.broadcast_transaction_synchronous(trx_builder.transaction)
ensure
  shutdown
end
shutdown() click to toggle source
# File lib/radiator/transaction.rb, line 160
def shutdown
  @api.shutdown if !!@api
  @network_broadcast_api.shutdown if !!@network_broadcast_api

  if @self_logger
    if !!@logger && defined?(@logger.close)
      if defined?(@logger.closed?)
        @logger.close unless @logger.closed?
      end
    end
  end
end
url() click to toggle source
# File lib/radiator/transaction.rb, line 93
def url
  case chain.to_s.downcase.to_sym
  when :steem then NETWORKS_STEEM_DEFAULT_NODE
  when :hive then NETWORKS_HIVE_DEFAULT_NODE
  when :test then NETWORKS_TEST_DEFAULT_NODE
  end
end
use_condenser_namespace?() click to toggle source
# File lib/radiator/transaction.rb, line 173
def use_condenser_namespace?
  !!@use_condenser_namespace
end

Private Instance Methods

broadcast_payload(payload) click to toggle source
# File lib/radiator/transaction.rb, line 190
def broadcast_payload(payload)
  if use_condenser_namespace?
    @api.broadcast_transaction_synchronous(payload)
  else
    @network_broadcast_api.broadcast_transaction_synchronous(trx: payload)
  end
end
canonical?(sig) click to toggle source

See: github.com/steemit/steem/issues/1944

# File lib/radiator/transaction.rb, line 309
def canonical?(sig)
  sig = sig.unpack('C*')

  !(
    ((sig[0] & 0x80 ) != 0) || ( sig[0] == 0 ) ||
    ((sig[1] & 0x80 ) != 0) ||
    ((sig[32] & 0x80 ) != 0) || ( sig[32] == 0 ) ||
    ((sig[33] & 0x80 ) != 0)
  )
end
digest() click to toggle source
# File lib/radiator/transaction.rb, line 286
def digest
  Digest::SHA256.digest(to_bytes)
end
payload() click to toggle source
# File lib/radiator/transaction.rb, line 198
def payload
  @payload ||= {
    expiration: @expiration.strftime('%Y-%m-%dT%H:%M:%S'),
    ref_block_num: @ref_block_num,
    ref_block_prefix: @ref_block_prefix,
    operations: operations.map { |op| op.payload },
    extensions: [],
    signatures: [hexlify(signature)]
  }
end
prepare() click to toggle source
# File lib/radiator/transaction.rb, line 209
def prepare
  raise TransactionError, "No wif or private key." unless !!@wif || !!@private_key

  @payload = nil

  while @expiration.nil? && @ref_block_num.nil? && @ref_block_prefix.nil?
    @api.get_dynamic_global_properties do |properties, error|
      if !!error
        raise TransactionError, "Unable to prepare transaction.", error
      end

      @properties = properties
    end

    # You can actually go back as far as the TaPoS buffer will allow, which
    # is something like 50,000 blocks.

    block_number = @properties.last_irreversible_block_num

    @api.get_block(block_number) do |block, error|
      if !!error
        ap error if defined?(ap) && ENV['DEBUG'] == 'true'
        raise TransactionError, "Unable to prepare transaction: #{error.message || 'Unknown cause.'}"
      end

      if !!block && !!block.previous
        @ref_block_num = (block_number - 1) & 0xFFFF
        @ref_block_prefix = unhexlify(block.previous[8..-1]).unpack('V*')[0]

        # The expiration allows for transactions to expire if they are not
        # included into a block by that time.  Always update it to the current
        # time + EXPIRE_IN_SECS.
        #
        # Note, as of #1215, expiration exactly 'now' will be rejected:
        # https://github.com/steemit/steem/blob/57451b80d2cf480dcce9b399e48e56aa7af1d818/libraries/chain/database.cpp#L2870
        # https://github.com/steemit/steem/issues/1215

        block_time = Time.parse(@properties.time + 'Z')
        @expiration ||= block_time + EXPIRE_IN_SECS
      else
        # Suspect this happens when there are microforks, but it should be
        # rare, especially since we're asking for the last irreversible
        # block.

        if block.nil?
          warning "Block missing while trying to prepare transaction, retrying ..."
        else
          debug block if %w(DEBUG TRACE).include? ENV['LOG']

          warning "Block structure while trying to prepare transaction, retrying ..."
        end

        @expiration = nil unless @immutable_expiration
      end
    end
  end

  self
end
signature() click to toggle source

May not find all non-canonicals, see: github.com/lian/bitcoin-ruby/issues/196

# File lib/radiator/transaction.rb, line 291
def signature
  public_key_hex = @private_key.pub
  ec = Bitcoin::OpenSSL_EC
  digest_hex = digest.freeze
  count = 0

  loop do
    count += 1
    debug "#{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)

    return sig if canonical? sig
  end
end
to_bytes() click to toggle source
# File lib/radiator/transaction.rb, line 269
def to_bytes
  bytes = unhexlify(@chain_id)
  bytes << pakS(@ref_block_num)
  bytes << pakI(@ref_block_prefix)
  bytes << pakI(@expiration.to_i)
  bytes << pakC(operations.size)

  operations.each do |op|
    bytes << op.to_bytes
  end

  # FIXME Should pakC(0) instead?
  bytes << 0x00 # extensions

  bytes
end