From c584f8c6823cbb5eb0b5f99f67c627be6b18d853 Mon Sep 17 00:00:00 2001
From: Anthony Martin <github@martin-studio.com>
Date: Mon, 29 Oct 2018 20:25:59 -0700
Subject: [PATCH] initial deserializer #14

---
 Gemfile.lock                                  |   8 +-
 README.md                                     |   2 +-
 lib/steem.rb                                  |  48 +++
 lib/steem/base_error.rb                       |   8 +-
 lib/steem/broadcast.rb                        |  19 +-
 lib/steem/marshal.rb                          | 224 +++++++++++
 lib/steem/mixins/jsonable.rb                  |  33 ++
 lib/steem/mixins/serializable.rb              |  37 ++
 lib/steem/operation.rb                        | 118 ++++++
 lib/steem/operation/account_create.rb         |  10 +
 .../account_create_with_delegation.rb         |  12 +
 lib/steem/operation/account_update.rb         |   8 +
 lib/steem/operation/account_witness_proxy.rb  |   4 +
 lib/steem/operation/account_witness_vote.rb   |   5 +
 .../operation/cancel_transfer_from_savings.rb |   4 +
 lib/steem/operation/challenge_authority.rb    |   5 +
 .../operation/change_recovery_account.rb      |   5 +
 lib/steem/operation/claim_account.rb          |   5 +
 lib/steem/operation/claim_reward_balance.rb   |   6 +
 lib/steem/operation/comment.rb                |   9 +
 lib/steem/operation/comment_options.rb        |  10 +
 lib/steem/operation/convert.rb                |   5 +
 lib/steem/operation/create_claimed_account.rb |  10 +
 lib/steem/operation/custom.rb                 |   5 +
 lib/steem/operation/custom_binary.rb          |   8 +
 lib/steem/operation/custom_json.rb            |   6 +
 lib/steem/operation/decline_voting_rights.rb  |   4 +
 .../operation/delegate_vesting_shares.rb      |   5 +
 lib/steem/operation/delete_comment.rb         |   4 +
 lib/steem/operation/escrow_approve.rb         |   8 +
 lib/steem/operation/escrow_dispute.rb         |   7 +
 lib/steem/operation/escrow_release.rb         |  10 +
 lib/steem/operation/escrow_transfer.rb        |  12 +
 lib/steem/operation/feed_publish.rb           |   4 +
 lib/steem/operation/limit_order_cancel.rb     |   4 +
 lib/steem/operation/limit_order_create.rb     |   8 +
 lib/steem/operation/limit_order_create2.rb    |   8 +
 lib/steem/operation/prove_authority.rb        |   4 +
 lib/steem/operation/recover_account.rb        |   6 +
 lib/steem/operation/report_over_production.rb |   5 +
 .../operation/request_account_recovery.rb     |   6 +
 lib/steem/operation/reset_account.rb          |   5 +
 lib/steem/operation/set_reset_account.rb      |   5 +
 .../operation/set_withdraw_vesting_route.rb   |   6 +
 lib/steem/operation/transfer.rb               |   6 +
 lib/steem/operation/transfer_from_savings.rb  |   7 +
 lib/steem/operation/transfer_to_savings.rb    |   6 +
 lib/steem/operation/transfer_to_vesting.rb    |   5 +
 lib/steem/operation/vote.rb                   |   6 +
 lib/steem/operation/withdraw_vesting.rb       |   4 +
 lib/steem/operation/witness_set_properties.rb |   5 +
 lib/steem/operation/witness_update.rb         |   7 +
 lib/steem/transaction.rb                      |  86 ++++
 lib/steem/transaction_builder.rb              | 135 ++++---
 lib/steem/type/amount.rb                      |   2 +
 steem-ruby.gemspec                            |   2 +
 test/steem/api_test.rb                        |   6 +-
 test/steem/broadcast_test.rb                  |   8 +-
 test/steem/database_api_test.rb               |  36 ++
 test/steem/jsonrpc_test.rb                    | 221 +++++++++-
 test/steem/marshal_test.rb                    | 377 ++++++++++++++++++
 test/steem/transaction_builder_test.rb        |  12 +-
 test/steem/witness_api_test.rb                |  52 ---
 test/test_helper.rb                           |   1 +
 64 files changed, 1567 insertions(+), 142 deletions(-)
 create mode 100644 lib/steem/marshal.rb
 create mode 100644 lib/steem/mixins/jsonable.rb
 create mode 100644 lib/steem/mixins/serializable.rb
 create mode 100644 lib/steem/operation.rb
 create mode 100644 lib/steem/operation/account_create.rb
 create mode 100644 lib/steem/operation/account_create_with_delegation.rb
 create mode 100644 lib/steem/operation/account_update.rb
 create mode 100644 lib/steem/operation/account_witness_proxy.rb
 create mode 100644 lib/steem/operation/account_witness_vote.rb
 create mode 100644 lib/steem/operation/cancel_transfer_from_savings.rb
 create mode 100644 lib/steem/operation/challenge_authority.rb
 create mode 100644 lib/steem/operation/change_recovery_account.rb
 create mode 100644 lib/steem/operation/claim_account.rb
 create mode 100644 lib/steem/operation/claim_reward_balance.rb
 create mode 100644 lib/steem/operation/comment.rb
 create mode 100644 lib/steem/operation/comment_options.rb
 create mode 100644 lib/steem/operation/convert.rb
 create mode 100644 lib/steem/operation/create_claimed_account.rb
 create mode 100644 lib/steem/operation/custom.rb
 create mode 100644 lib/steem/operation/custom_binary.rb
 create mode 100644 lib/steem/operation/custom_json.rb
 create mode 100644 lib/steem/operation/decline_voting_rights.rb
 create mode 100644 lib/steem/operation/delegate_vesting_shares.rb
 create mode 100644 lib/steem/operation/delete_comment.rb
 create mode 100644 lib/steem/operation/escrow_approve.rb
 create mode 100644 lib/steem/operation/escrow_dispute.rb
 create mode 100644 lib/steem/operation/escrow_release.rb
 create mode 100644 lib/steem/operation/escrow_transfer.rb
 create mode 100644 lib/steem/operation/feed_publish.rb
 create mode 100644 lib/steem/operation/limit_order_cancel.rb
 create mode 100644 lib/steem/operation/limit_order_create.rb
 create mode 100644 lib/steem/operation/limit_order_create2.rb
 create mode 100644 lib/steem/operation/prove_authority.rb
 create mode 100644 lib/steem/operation/recover_account.rb
 create mode 100644 lib/steem/operation/report_over_production.rb
 create mode 100644 lib/steem/operation/request_account_recovery.rb
 create mode 100644 lib/steem/operation/reset_account.rb
 create mode 100644 lib/steem/operation/set_reset_account.rb
 create mode 100644 lib/steem/operation/set_withdraw_vesting_route.rb
 create mode 100644 lib/steem/operation/transfer.rb
 create mode 100644 lib/steem/operation/transfer_from_savings.rb
 create mode 100644 lib/steem/operation/transfer_to_savings.rb
 create mode 100644 lib/steem/operation/transfer_to_vesting.rb
 create mode 100644 lib/steem/operation/vote.rb
 create mode 100644 lib/steem/operation/withdraw_vesting.rb
 create mode 100644 lib/steem/operation/witness_set_properties.rb
 create mode 100644 lib/steem/operation/witness_update.rb
 create mode 100644 lib/steem/transaction.rb
 create mode 100644 test/steem/marshal_test.rb
 delete mode 100644 test/steem/witness_api_test.rb

diff --git a/Gemfile.lock b/Gemfile.lock
index f3c6a18..5dc8dac 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,6 +2,8 @@ PATH
   remote: .
   specs:
     steem-ruby (0.9.3)
+      base58 (~> 0.2, >= 0.2.3)
+      bindata (~> 2.4, >= 2.4.4)
       bitcoin-ruby (~> 0.0, >= 0.0.18)
       ffi (~> 1.9, >= 1.9.23)
       hashie (~> 3.5, >= 3.5.7)
@@ -14,6 +16,8 @@ GEM
     addressable (2.5.2)
       public_suffix (>= 2.0.2, < 4.0)
     awesome_print (1.8.0)
+    base58 (0.2.3)
+    bindata (2.4.4)
     bitcoin-ruby (0.0.18)
     coderay (1.1.2)
     crack (0.4.3)
@@ -37,7 +41,7 @@ GEM
     pry (0.11.3)
       coderay (~> 1.1.0)
       method_source (~> 0.9.0)
-    public_suffix (3.0.2)
+    public_suffix (3.0.3)
     rake (12.3.1)
     safe_yaml (1.0.4)
     simplecov (0.16.1)
@@ -50,7 +54,7 @@ GEM
       addressable (>= 2.3.6)
       crack (>= 0.3.2)
       hashdiff
-    yard (0.9.15)
+    yard (0.9.16)
 
 PLATFORMS
   ruby
diff --git a/README.md b/README.md
index 1b207f5..fee66ff 100644
--- a/README.md
+++ b/README.md
@@ -186,7 +186,7 @@ trx = open('trx.json').read
 builder = Steem::TransactionBuilder.new(wif: wif2, trx: trx)
 api = Steem::CondenserApi.new
 trx = builder.transaction
-api.broadcast_transaction_synchronous(trx: trx)
+api.broadcast_transaction_synchronous(trx)
 ```
 
 ### Get Accounts
diff --git a/lib/steem.rb b/lib/steem.rb
index 868f82f..b67bb21 100644
--- a/lib/steem.rb
+++ b/lib/steem.rb
@@ -6,10 +6,58 @@ require 'hashie'
 require 'steem/version'
 require 'steem/utils'
 require 'steem/base_error'
+require 'steem/mixins/serializable'
+require 'steem/mixins/jsonable'
 require 'steem/mixins/retriable'
 require 'steem/chain_config'
 require 'steem/type/base_type'
 require 'steem/type/amount'
+require 'steem/operation'
+require 'steem/operation/account_create.rb'
+require 'steem/operation/account_create_with_delegation.rb'
+require 'steem/operation/account_update.rb'
+require 'steem/operation/account_witness_proxy.rb'
+require 'steem/operation/account_witness_vote.rb'
+require 'steem/operation/cancel_transfer_from_savings.rb'
+require 'steem/operation/challenge_authority.rb'
+require 'steem/operation/change_recovery_account.rb'
+require 'steem/operation/claim_account.rb'
+require 'steem/operation/claim_reward_balance.rb'
+require 'steem/operation/comment.rb'
+require 'steem/operation/comment_options.rb'
+require 'steem/operation/convert.rb'
+require 'steem/operation/create_claimed_account.rb'
+require 'steem/operation/custom.rb'
+require 'steem/operation/custom_binary.rb'
+require 'steem/operation/custom_json.rb'
+require 'steem/operation/decline_voting_rights.rb'
+require 'steem/operation/delegate_vesting_shares.rb'
+require 'steem/operation/delete_comment.rb'
+require 'steem/operation/escrow_approve.rb'
+require 'steem/operation/escrow_dispute.rb'
+require 'steem/operation/escrow_release.rb'
+require 'steem/operation/escrow_transfer.rb'
+require 'steem/operation/feed_publish.rb'
+require 'steem/operation/limit_order_cancel.rb'
+require 'steem/operation/limit_order_create.rb'
+require 'steem/operation/limit_order_create2.rb'
+require 'steem/operation/prove_authority.rb'
+require 'steem/operation/recover_account.rb'
+require 'steem/operation/report_over_production.rb'
+require 'steem/operation/request_account_recovery.rb'
+require 'steem/operation/reset_account.rb'
+require 'steem/operation/set_reset_account.rb'
+require 'steem/operation/set_withdraw_vesting_route.rb'
+require 'steem/operation/transfer.rb'
+require 'steem/operation/transfer_from_savings.rb'
+require 'steem/operation/transfer_to_savings.rb'
+require 'steem/operation/transfer_to_vesting.rb'
+require 'steem/operation/vote.rb'
+require 'steem/operation/withdraw_vesting.rb'
+require 'steem/operation/witness_update.rb'
+require 'steem/operation/witness_set_properties.rb'
+require 'steem/marshal'
+require 'steem/transaction'
 require 'steem/transaction_builder'
 require 'steem/rpc/base_client'
 require 'steem/rpc/http_client'
diff --git a/lib/steem/base_error.rb b/lib/steem/base_error.rb
index 8a7f7b3..400c847 100644
--- a/lib/steem/base_error.rb
+++ b/lib/steem/base_error.rb
@@ -10,7 +10,7 @@ module Steem
       detail[:error] = @error if !!@error
       detail[:cause] = @cause if !!@cause
       
-      JSON[detail] rescue detai.to_s
+      JSON[detail] rescue detail.to_s
     end
     
     def self.build_error(error, context)
@@ -19,11 +19,11 @@ module Steem
       end
       
       if error.message.include? 'Internal Error'
-        raise Steem::RemoteNodeError.new, error.message, build_backtrace(error)
+        raise Steem::RemoteNodeError, error.message, build_backtrace(error)
       end
       
       if error.message.include? 'Server error'
-        raise Steem::RemoteNodeError.new, error.message, build_backtrace(error)
+        raise Steem::RemoteNodeError, error.message, build_backtrace(error)
       end
       
       if error.message.include? 'plugin not enabled'
@@ -176,6 +176,8 @@ module Steem
     end
   end
   
+  class DeserializationError < BaseError; end
+  class SerializationMismatchError < BaseError; end
   class UnsupportedChainError < BaseError; end
   class ArgumentError < BaseError; end
   class TypeError < BaseError; end
diff --git a/lib/steem/broadcast.rb b/lib/steem/broadcast.rb
index 4eb1f4e..b53fc69 100644
--- a/lib/steem/broadcast.rb
+++ b/lib/steem/broadcast.rb
@@ -203,7 +203,7 @@ module Steem
       }
       
       if !!params[:beneficiaries]
-        comment_options[:extensions] << [0, {beneficiaries: params[:beneficiaries]}]
+        comment_options[:extensions] << [comment_options[:extensions].size, normalize_beneficiaries(options.merge(beneficiaries: params[:beneficiaries]))]
       end
       
       ops << [:comment_options, comment_options]
@@ -714,13 +714,21 @@ module Steem
       end
       
       if !!(sbd_exchange_rate = params[:props][:sbd_exchange_rate] rescue nil)
-        params[:props][:sbd_exchange_rate] = normalize_amount(options.merge amount: sbd_exchange_rate, serialize: true)
+        params[:props][:sbd_exchange_rate][:base] = normalize_amount(options.merge amount: sbd_exchange_rate[:base], serialize: true)
+        params[:props][:sbd_exchange_rate][:quote] = normalize_amount(options.merge amount: sbd_exchange_rate[:quote], serialize: true)
+        params[:props][:sbd_exchange_rate] = params[:props][:sbd_exchange_rate].to_json
+      end
+      
+      %i(key new_signing_key).each do |key|
+        if !!params[key] && params[key].size == 53
+          params[key] = params[key][3..-1]
+        end
       end
       
       %i(account_creation_fee sbd_exchange_rate url new_signing_key).each do |key|
         next unless !!params[:props][key]
         
-        val = params[:props][key]
+        val = params[:props][key].to_s
           
         params[:props][key] = hexlify val unless val =~ /^[0-9A-F]+$/i
       end
@@ -1282,6 +1290,11 @@ module Steem
       end
     end
     
+    def self.normalize_beneficiaries(options)
+      # Type::Beneficiaries.new(options[:beneficiaries])
+      {beneficiaries: options[:beneficiaries]}
+    end
+    
     # @private
     def self.database_api(options)
       options[:database_api] ||= if !!options[:app_base]
diff --git a/lib/steem/marshal.rb b/lib/steem/marshal.rb
new file mode 100644
index 0000000..7d45667
--- /dev/null
+++ b/lib/steem/marshal.rb
@@ -0,0 +1,224 @@
+require 'bindata'
+require 'base58'
+
+module Steem
+  class Marshal
+    include Utils
+    include ChainConfig
+    
+    attr_reader :bytes, :cursor
+
+    def initialize(options = {})
+      @bytes = if !!(hex = options[:hex])
+        unhexlify hex
+      else
+        options[:bytes]
+      end
+      
+      @chain = options[:chain] || :steem
+      @prefix ||= case @chain
+      when :steem then NETWORKS_STEEM_ADDRESS_PREFIX
+      when :test then NETWORKS_TEST_ADDRESS_PREFIX
+      else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
+      end
+      @cursor = 0
+    end
+    
+    def hex
+      hexlify bytes
+    end
+    
+    def rewind!
+      @cursor = 0
+    end
+    
+    def step(n = 0)
+      @cursor += n
+    end
+    
+    def scan(len)
+      bytes.slice(@cursor..(@cursor - 1) + len).tap { |_| @cursor += len }
+    end
+    
+    def operation_type
+      Operation::IDS[unsigned_char]
+    end
+    
+    def unsigned_char; BinData::Uint8le.read(scan(1)); end # 8-bit unsigned 
+    def uint16; BinData::Uint16le.read(scan(2)); end # 16-bit unsigned, VAX (little-endian) byte order
+    def uint32; BinData::Uint32le.read(scan(4)); end # 32-bit unsigned, VAX (little-endian) byte order
+    def uint64; BinData::Uint64le.read(scan(8)); end # 64-bit unsigned, little-endian
+
+    def signed_char; BinData::Int8le.read(scan(1)); end # 8-bit signed 
+    def int16; BinData::Int16le.read(scan(2)); end # 16-bit signed, little-endian
+    def int32; BinData::Int32le.read(scan(4)); end # 32-bit signed, little-endian
+    def int64; BinData::Int64le.read(scan(8)); end # 64-bit signed, little-endian
+
+    def boolean; BinData::Bit1.read(scan(1)) != 0; end
+    
+    def varint
+      shift = 0
+      result = 0
+      bytes = []
+      
+      while (n = unsigned_char) >> 7 == 1
+        bytes << n
+      end
+      
+      bytes << n
+      
+      bytes.each do |b|
+        result += ((b & 0x7f) << shift)
+        break unless (b & 0x80)
+        shift += 7
+      end
+      
+      result
+    end
+    
+    def string(len = nil); scan(len || varint); end
+    
+    def raw_bytes(len = nil); scan(len || varint).force_encoding('BINARY'); end
+  
+    def point_in_time
+      if (time = uint32) == 2**32-1
+        Time.at -1
+      else
+        Time.at time
+      end
+    end
+    
+    def public_key(prefix = @prefix)
+      raw_public_key = raw_bytes(33)
+      checksum = OpenSSL::Digest::RIPEMD160.digest(raw_public_key)
+      key = Base58.binary_to_base58(raw_public_key + checksum.slice(0, 4), :bitcoin)
+      
+      prefix + key
+    end
+    
+    def amount
+      amount = uint64.to_f
+      precision = signed_char
+      asset = scan(7).strip
+      
+      amount = "%.#{precision}f #{asset}" % (amount / 10 ** precision)
+
+      Steem::Type::Amount.new(amount)
+    end
+    
+    def price
+      {base: amount, quote: amount}
+    end
+    
+    def authority(options = {optional: false})
+      return if !!options[:optional] && unsigned_char == 0
+      
+      {
+        weight_threshold: uint32,
+        account_auths: varint.times.map { [string, uint16] },
+        key_auths: varint.times.map { [public_key, uint16] }
+      }
+    end
+    
+    def optional_authority
+      authority(optional: true)
+    end
+    
+    def comment_options_extensions
+      beneficiaries
+    end
+    
+    def beneficiaries
+      varint.times.map {
+        {account: string, weight: uint16}
+      }
+    end
+    
+    def chain_properties
+      {
+        account_creation_fee: amount,
+        maximum_block_size: uint32,
+        sbd_interest_rate: uint16
+      }
+    end
+    
+    def required_auths
+      varint.times.map { string }
+    end
+    
+    def witness_properties
+      properties = {}
+      
+      varint.times do
+        key = string.to_sym
+        properties[key] = case key
+        when :account_creation_fee then Steem::Type::Amount.new(string)
+        when :account_subsidy_budget then scan(3)
+        when :account_subsidy_decay, :maximum_block_size then uint32
+        when :url then string
+        when :sbd_exchange_rate
+          JSON[string].tap do |rate|
+            rate["base"] = Steem::Type::Amount.new(rate["base"])
+            rate["quote"] = Steem::Type::Amount.new(rate["quote"])
+          end
+        when :sbd_interest_rate then uint16
+        when :key, :new_signing_key then @prefix + scan(50)
+        else; raise "Unknown witness property: #{key}"
+        end
+      end
+      
+      properties
+    end
+    
+    def empty_array
+      unsigned_char == 0 and [] or raise "Found non-empty array."
+    end
+    
+    def transaction(options = {})
+      trx = options[:trx] || Transaction.new
+      
+      trx.ref_block_num = uint16
+      trx.ref_block_prefix = uint32
+      trx.expiration = point_in_time
+      
+      trx.operations = operations
+      
+      trx
+    rescue => e
+      raise DeserializationError.new("Transaction failed\nOriginal serialized bytes:\n[#{hex[0..(@cursor * 2) - 1]}]#{hex[((@cursor) * 2)..-1]}", e)
+    end
+
+    def operations
+      operations_len = signed_char
+      operations = []
+      
+      while operations.size < operations_len do
+        begin
+          type = operation_type
+          break if type.nil?
+          
+          op_class_name = type.to_s.sub!(/_operation$/, '')
+          op_class_name = "Steem::Operation::" + op_class_name.split('_').map(&:capitalize).join
+          op_class = Object::const_get(op_class_name)
+          op = op_class.new
+          
+          op_class::serializable_types.each do |k, v|
+            begin
+              op.send("#{k}=", send(v))
+            rescue => e
+              raise DeserializationError.new("#{type}.#{k} (#{v}) failed", e)
+            end
+          end
+          
+          operations << {type: type, value: op}
+        rescue => e
+          raise DeserializationError.new("#{type} failed", e)
+        end
+      end
+      
+      operations
+    rescue => e
+      raise DeserializationError.new("Operations failed", e)
+    end
+  end
+end
diff --git a/lib/steem/mixins/jsonable.rb b/lib/steem/mixins/jsonable.rb
new file mode 100644
index 0000000..dc8630f
--- /dev/null
+++ b/lib/steem/mixins/jsonable.rb
@@ -0,0 +1,33 @@
+module Steem
+  module JSONable
+    module ClassMethods
+      attr_accessor :attributes
+
+      def attr_accessor *attrs
+        self.attributes = Array attrs
+        
+        super
+      end
+    end
+
+    def self.included(base)
+      base.extend(ClassMethods)
+    end
+
+    def as_json options = {}
+      serialized = Hash.new
+      
+      self.class.attributes.each do |attribute|
+        if !!(value = self.public_send attribute)
+          serialized[attribute] = value
+        end
+      end
+      
+      serialized
+    end
+
+    def to_json *a
+      as_json.to_json *a
+    end
+  end
+end
diff --git a/lib/steem/mixins/serializable.rb b/lib/steem/mixins/serializable.rb
new file mode 100644
index 0000000..93fc8ea
--- /dev/null
+++ b/lib/steem/mixins/serializable.rb
@@ -0,0 +1,37 @@
+module Steem
+  module Serializable
+    KNOWN_TYPES = %i(unsigned_char uint16 uint32 uint64 signed_char int16 int32
+      int64 boolean varint string raw_bytes point_in_time public_key amount
+      price authority optional_authority comment_options_extensions
+      beneficiaries chain_properties required_auths witness_properties
+      empty_array lambda)
+    
+    module ClassMethods
+      def def_attr key_pair
+        name = key_pair.keys.first
+        type = key_pair.values.first
+        
+        self.attributes ||= []
+        self.attributes << name
+        
+        attr_accessor *attributes
+        add_type name, type
+      end
+      
+      def add_type name, type
+        raise "Unknown type: #{type}" unless KNOWN_TYPES.include? type
+        
+        @serializable_types ||= {}
+        @serializable_types[name] = type
+      end
+      
+      def serializable_types
+        @serializable_types
+      end
+    end
+
+    def self.included(base)
+      base.extend(ClassMethods)
+    end
+  end
+end
diff --git a/lib/steem/operation.rb b/lib/steem/operation.rb
new file mode 100644
index 0000000..1e71d07
--- /dev/null
+++ b/lib/steem/operation.rb
@@ -0,0 +1,118 @@
+module Steem
+  class Operation
+    include JSONable
+    include Serializable
+    include Utils
+    
+    # IDs derrived from:
+    # https://github.com/steemit/steem/blob/127a441fbac2f06804359968bda83b66e602c891/libraries/protocol/include/steem/protocol/operations.hpp
+    
+    IDS = [
+      :vote_operation,
+      :comment_operation,
+      
+      :transfer_operation,
+      :transfer_to_vesting_operation,
+      :withdraw_vesting_operation,
+      
+      :limit_order_create_operation,
+      :limit_order_cancel_operation,
+      
+      :feed_publish_operation,
+      :convert_operation,
+      
+      :account_create_operation,
+      :account_update_operation,
+      
+      :witness_update_operation,
+      :account_witness_vote_operation,
+      :account_witness_proxy_operation,
+      
+      :pow_operation,
+      
+      :custom_operation,
+      
+      :report_over_production_operation,
+      
+      :delete_comment_operation,
+      :custom_json_operation,
+      :comment_options_operation,
+      :set_withdraw_vesting_route_operation,
+      :limit_order_create2_operation,
+      :claim_account_operation,
+      :create_claimed_account_operation,
+      :request_account_recovery_operation,
+      :recover_account_operation,
+      :change_recovery_account_operation,
+      :escrow_transfer_operation,
+      :escrow_dispute_operation,
+      :escrow_release_operation,
+      :pow2_operation,
+      :escrow_approve_operation,
+      :transfer_to_savings_operation,
+      :transfer_from_savings_operation,
+      :cancel_transfer_from_savings_operation,
+      :custom_binary_operation,
+      :decline_voting_rights_operation,
+      :reset_account_operation,
+      :set_reset_account_operation,
+      :claim_reward_balance_operation,
+      :delegate_vesting_shares_operation,
+      :account_create_with_delegation_operation,
+      :witness_set_properties_operation,
+      
+      # SMT operations
+      :claim_reward_balance2_operation,
+      
+      :smt_setup_operation,
+      :smt_cap_reveal_operation,
+      :smt_refund_operation,
+      :smt_setup_emissions_operation,
+      :smt_set_setup_parameters_operation,
+      :smt_set_runtime_parameters_operation,
+      :smt_create_operation,
+      
+      # virtual operations below this point
+      :fill_convert_request_operation,
+      :author_reward_operation,
+      :curation_reward_operation,
+      :comment_reward_operation,
+      :liquidity_reward_operation,
+      :interest_operation,
+      :fill_vesting_withdraw_operation,
+      :fill_order_operation,
+      :shutdown_witness_operation,
+      :fill_transfer_from_savings_operation,
+      :hardfork_operation,
+      :comment_payout_update_operation,
+      :return_vesting_delegation_operation,
+      :comment_benefactor_reward_operation,
+      :producer_reward_operation,
+      :clear_null_account_balance_operation
+    ]
+    
+    def self.op_id(op)
+      IDS.find_index op
+    end
+
+    def inspect
+      properties = self.class.attributes.map do |prop|
+        if !!(v = instance_variable_get("@#{prop}"))
+          "@#{prop}=#{v}" 
+        end
+      end.compact.join(', ')
+      
+      "#<#{self.class.name} [#{properties}]>"
+    end
+    
+    def [](key)
+      key = key.to_sym
+      send(key) if self.class.attributes.include?(key)
+    end
+
+    def []=(key, value)
+      key = key.to_sym
+      send("#{key}=", value) if self.class.attributes.include?(key)
+    end    
+  end
+end
diff --git a/lib/steem/operation/account_create.rb b/lib/steem/operation/account_create.rb
new file mode 100644
index 0000000..d69a8eb
--- /dev/null
+++ b/lib/steem/operation/account_create.rb
@@ -0,0 +1,10 @@
+class Steem::Operation::AccountCreate < Steem::Operation
+  def_attr fee: :amount
+  def_attr creator: :string
+  def_attr new_account_name: :string
+  def_attr owner: :authority
+  def_attr active: :authority
+  def_attr posting: :authority
+  def_attr memo_key: :public_key
+  def_attr json_metadata: :string
+end
diff --git a/lib/steem/operation/account_create_with_delegation.rb b/lib/steem/operation/account_create_with_delegation.rb
new file mode 100644
index 0000000..995c54c
--- /dev/null
+++ b/lib/steem/operation/account_create_with_delegation.rb
@@ -0,0 +1,12 @@
+class Steem::Operation::AccountCreateWithDelegation < Steem::Operation
+  def_attr fee: :amount
+  def_attr delegation: :amount
+  def_attr creator: :string
+  def_attr new_account_name: :string
+  def_attr owner: :authority
+  def_attr active: :authority
+  def_attr posting: :authority
+  def_attr memo_key: :public_key
+  def_attr json_metadata: :string
+  def_attr extensions: :empty_array
+end
diff --git a/lib/steem/operation/account_update.rb b/lib/steem/operation/account_update.rb
new file mode 100644
index 0000000..fe6ab87
--- /dev/null
+++ b/lib/steem/operation/account_update.rb
@@ -0,0 +1,8 @@
+class Steem::Operation::AccountUpdate < Steem::Operation
+  def_attr account: :string
+  def_attr owner: :optional_authority
+  def_attr active: :optional_authority
+  def_attr posting: :optional_authority
+  def_attr memo_key: :public_key
+  def_attr json_metadata: :string
+end
diff --git a/lib/steem/operation/account_witness_proxy.rb b/lib/steem/operation/account_witness_proxy.rb
new file mode 100644
index 0000000..488238e
--- /dev/null
+++ b/lib/steem/operation/account_witness_proxy.rb
@@ -0,0 +1,4 @@
+class Steem::Operation::AccountWitnessProxy < Steem::Operation
+  def_attr account: :string
+  def_attr proxy: :string
+end
diff --git a/lib/steem/operation/account_witness_vote.rb b/lib/steem/operation/account_witness_vote.rb
new file mode 100644
index 0000000..799ef58
--- /dev/null
+++ b/lib/steem/operation/account_witness_vote.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::AccountWitnessVote < Steem::Operation
+  def_attr account: :string
+  def_attr witness: :string
+  def_attr approve: :boolean
+end
diff --git a/lib/steem/operation/cancel_transfer_from_savings.rb b/lib/steem/operation/cancel_transfer_from_savings.rb
new file mode 100644
index 0000000..ce262de
--- /dev/null
+++ b/lib/steem/operation/cancel_transfer_from_savings.rb
@@ -0,0 +1,4 @@
+class Steem::Operation::CancelTransferFromSavings < Steem::Operation
+  def_attr from: :string
+  def_attr request_id: :uint32
+end
diff --git a/lib/steem/operation/challenge_authority.rb b/lib/steem/operation/challenge_authority.rb
new file mode 100644
index 0000000..53f685e
--- /dev/null
+++ b/lib/steem/operation/challenge_authority.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::ChallengeAuthority < Steem::Operation
+  def_attr challenger: :string
+  def_attr challenged: :string
+  def_attr require_owner: :boolean
+end
diff --git a/lib/steem/operation/change_recovery_account.rb b/lib/steem/operation/change_recovery_account.rb
new file mode 100644
index 0000000..1d9f924
--- /dev/null
+++ b/lib/steem/operation/change_recovery_account.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::ChangeRecoveryAccount < Steem::Operation
+  def_attr account_to_recover: :string
+  def_attr new_recovery_account: :string
+  def_attr extensions: :empty_array
+end
diff --git a/lib/steem/operation/claim_account.rb b/lib/steem/operation/claim_account.rb
new file mode 100644
index 0000000..f36032b
--- /dev/null
+++ b/lib/steem/operation/claim_account.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::ClaimAccount < Steem::Operation
+  def_attr creator: :string
+  def_attr fee: :amount
+  def_attr extensions: :empty_array
+end
diff --git a/lib/steem/operation/claim_reward_balance.rb b/lib/steem/operation/claim_reward_balance.rb
new file mode 100644
index 0000000..55a1e88
--- /dev/null
+++ b/lib/steem/operation/claim_reward_balance.rb
@@ -0,0 +1,6 @@
+class Steem::Operation::ClaimRewardBalance < Steem::Operation
+  def_attr account: :string
+  def_attr reward_steem: :amount
+  def_attr reward_sbd: :amount
+  def_attr reward_vests: :amount
+end
diff --git a/lib/steem/operation/comment.rb b/lib/steem/operation/comment.rb
new file mode 100644
index 0000000..fdb203f
--- /dev/null
+++ b/lib/steem/operation/comment.rb
@@ -0,0 +1,9 @@
+class Steem::Operation::Comment < Steem::Operation
+  def_attr parent_author: :string
+  def_attr parent_permlink: :string
+  def_attr author: :string
+  def_attr permlink: :string
+  def_attr title: :string
+  def_attr body: :string
+  def_attr json_metadata: :string
+end
diff --git a/lib/steem/operation/comment_options.rb b/lib/steem/operation/comment_options.rb
new file mode 100644
index 0000000..d7a9a87
--- /dev/null
+++ b/lib/steem/operation/comment_options.rb
@@ -0,0 +1,10 @@
+class Steem::Operation::CommentOptions < Steem::Operation
+  def_attr author: :string
+  def_attr permlink: :string
+  def_attr max_accepted_payout: :amount
+  def_attr percent_steem_dollars: :uint32
+  def_attr allow_replies: :boolean
+  def_attr allow_votes: :boolean
+  def_attr allow_curation_rewards: :boolean
+  def_attr extensions: :comment_options_extensions
+end
diff --git a/lib/steem/operation/convert.rb b/lib/steem/operation/convert.rb
new file mode 100644
index 0000000..80d971a
--- /dev/null
+++ b/lib/steem/operation/convert.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::Convert < Steem::Operation
+  def_attr owner: :string
+  def_attr requestid: :uint32
+  def_attr amount: :amount
+end
diff --git a/lib/steem/operation/create_claimed_account.rb b/lib/steem/operation/create_claimed_account.rb
new file mode 100644
index 0000000..5f5b956
--- /dev/null
+++ b/lib/steem/operation/create_claimed_account.rb
@@ -0,0 +1,10 @@
+class Steem::Operation::CreateClaimedAccount < Steem::Operation
+  def_attr creator: :string
+  def_attr new_account_name: :string
+  def_attr owner: :authority
+  def_attr active: :authority
+  def_attr posting: :authority
+  def_attr memo_key: :public_key
+  def_attr json_metadata: :string
+  def_attr extensions: :empty_array
+end
diff --git a/lib/steem/operation/custom.rb b/lib/steem/operation/custom.rb
new file mode 100644
index 0000000..ed6ede4
--- /dev/null
+++ b/lib/steem/operation/custom.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::Custom < Steem::Operation
+  def_attr required_auths: :required_auths
+  def_attr id: :uint32
+  def_attr data: :raw_bytes
+end
diff --git a/lib/steem/operation/custom_binary.rb b/lib/steem/operation/custom_binary.rb
new file mode 100644
index 0000000..1b19edd
--- /dev/null
+++ b/lib/steem/operation/custom_binary.rb
@@ -0,0 +1,8 @@
+class Steem::Operation::CustomBinary < Steem::Operation
+  def_attr required_owner_auths: :required_auths
+  def_attr required_active_auths: :required_auths
+  def_attr required_posting_auths: :required_auths
+  def_attr required_auths: :required_auths
+  def_attr id: :string
+  def_attr data: :raw_bytes
+end
diff --git a/lib/steem/operation/custom_json.rb b/lib/steem/operation/custom_json.rb
new file mode 100644
index 0000000..c564e7d
--- /dev/null
+++ b/lib/steem/operation/custom_json.rb
@@ -0,0 +1,6 @@
+class Steem::Operation::CustomJson < Steem::Operation
+  def_attr required_auths: :required_auths
+  def_attr required_posting_auths: :required_auths
+  def_attr id: :string
+  def_attr json: :string
+end
diff --git a/lib/steem/operation/decline_voting_rights.rb b/lib/steem/operation/decline_voting_rights.rb
new file mode 100644
index 0000000..7f5fe38
--- /dev/null
+++ b/lib/steem/operation/decline_voting_rights.rb
@@ -0,0 +1,4 @@
+class Steem::Operation::DeclineVotingRights < Steem::Operation
+  def_attr account: :string
+  def_attr decline: :boolean
+end
diff --git a/lib/steem/operation/delegate_vesting_shares.rb b/lib/steem/operation/delegate_vesting_shares.rb
new file mode 100644
index 0000000..73b9740
--- /dev/null
+++ b/lib/steem/operation/delegate_vesting_shares.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::DelegateVestingShares < Steem::Operation
+  def_attr delegator: :string
+  def_attr delegatee: :string
+  def_attr vesting_shares: :amount
+end
diff --git a/lib/steem/operation/delete_comment.rb b/lib/steem/operation/delete_comment.rb
new file mode 100644
index 0000000..638bc58
--- /dev/null
+++ b/lib/steem/operation/delete_comment.rb
@@ -0,0 +1,4 @@
+class Steem::Operation::DeleteComment < Steem::Operation
+  def_attr author: :string
+  def_attr permlink: :string
+end
diff --git a/lib/steem/operation/escrow_approve.rb b/lib/steem/operation/escrow_approve.rb
new file mode 100644
index 0000000..9922a9b
--- /dev/null
+++ b/lib/steem/operation/escrow_approve.rb
@@ -0,0 +1,8 @@
+class Steem::Operation::EscrowApprove < Steem::Operation
+  def_attr from: :string
+  def_attr to: :string
+  def_attr agent: :string
+  def_attr who: :string
+  def_attr escrow_id: :uint32
+  def_attr approve: :boolean
+end
diff --git a/lib/steem/operation/escrow_dispute.rb b/lib/steem/operation/escrow_dispute.rb
new file mode 100644
index 0000000..e4b748f
--- /dev/null
+++ b/lib/steem/operation/escrow_dispute.rb
@@ -0,0 +1,7 @@
+class Steem::Operation::EscrowDispute < Steem::Operation
+  def_attr from: :string
+  def_attr to: :string
+  def_attr agent: :string
+  def_attr who: :string
+  def_attr escrow_id: :uint32
+end
diff --git a/lib/steem/operation/escrow_release.rb b/lib/steem/operation/escrow_release.rb
new file mode 100644
index 0000000..4fb4769
--- /dev/null
+++ b/lib/steem/operation/escrow_release.rb
@@ -0,0 +1,10 @@
+class Steem::Operation::EscrowRelease < Steem::Operation
+  def_attr from: :string
+  def_attr to: :string
+  def_attr agent: :string
+  def_attr who: :string
+  def_attr receiver: :string
+  def_attr escrow_id: :uint32
+  def_attr sbd_amount: :amount
+  def_attr steem_amount: :amount
+end
diff --git a/lib/steem/operation/escrow_transfer.rb b/lib/steem/operation/escrow_transfer.rb
new file mode 100644
index 0000000..e715550
--- /dev/null
+++ b/lib/steem/operation/escrow_transfer.rb
@@ -0,0 +1,12 @@
+class Steem::Operation::EscrowTransfer < Steem::Operation
+  def_attr from: :string
+  def_attr to: :string
+  def_attr sbd_amount: :amount
+  def_attr steem_amount: :amount
+  def_attr escrow_id: :uint32
+  def_attr agent: :string
+  def_attr fee: :amount
+  def_attr json_metadata: :string
+  def_attr ratification_deadline: :point_in_time
+  def_attr escrow_expiration: :point_in_time
+end
diff --git a/lib/steem/operation/feed_publish.rb b/lib/steem/operation/feed_publish.rb
new file mode 100644
index 0000000..c6680e6
--- /dev/null
+++ b/lib/steem/operation/feed_publish.rb
@@ -0,0 +1,4 @@
+class Steem::Operation::FeedPublish < Steem::Operation
+  def_attr publisher: :string
+  def_attr exchange_rate: :price
+end
diff --git a/lib/steem/operation/limit_order_cancel.rb b/lib/steem/operation/limit_order_cancel.rb
new file mode 100644
index 0000000..7d74eb6
--- /dev/null
+++ b/lib/steem/operation/limit_order_cancel.rb
@@ -0,0 +1,4 @@
+class Steem::Operation::LimitOrderCancel < Steem::Operation
+  def_attr owner: :string
+  def_attr orderid: :uint32
+end
diff --git a/lib/steem/operation/limit_order_create.rb b/lib/steem/operation/limit_order_create.rb
new file mode 100644
index 0000000..1041b53
--- /dev/null
+++ b/lib/steem/operation/limit_order_create.rb
@@ -0,0 +1,8 @@
+class Steem::Operation::LimitOrderCreate < Steem::Operation
+  def_attr owner: :string
+  def_attr orderid: :uint32
+  def_attr amount_to_sell: :amount
+  def_attr min_to_receive: :amount
+  def_attr fill_or_kill: :boolean
+  def_attr expiration: :point_in_time
+end
diff --git a/lib/steem/operation/limit_order_create2.rb b/lib/steem/operation/limit_order_create2.rb
new file mode 100644
index 0000000..7801f18
--- /dev/null
+++ b/lib/steem/operation/limit_order_create2.rb
@@ -0,0 +1,8 @@
+class Steem::Operation::LimitOrderCreate2 < Steem::Operation
+  def_attr owner: :string
+  def_attr orderid: :uint32
+  def_attr amount_to_sell: :amount
+  def_attr fill_or_kill: :boolean
+  def_attr exchange_rate: :price
+  def_attr expiration: :point_in_time
+end
diff --git a/lib/steem/operation/prove_authority.rb b/lib/steem/operation/prove_authority.rb
new file mode 100644
index 0000000..1b5e18e
--- /dev/null
+++ b/lib/steem/operation/prove_authority.rb
@@ -0,0 +1,4 @@
+class Steem::Operation::ProveAuthority < Steem::Operation
+  def_attr challenged: :string
+  def_attr require_owner: :boolean
+end
diff --git a/lib/steem/operation/recover_account.rb b/lib/steem/operation/recover_account.rb
new file mode 100644
index 0000000..6da4218
--- /dev/null
+++ b/lib/steem/operation/recover_account.rb
@@ -0,0 +1,6 @@
+class Steem::Operation::RecoverAccount < Steem::Operation
+  def_attr account_to_recover: :string
+  def_attr new_owner_authority: :authority
+  def_attr recent_owner_authority: :authority
+  def_attr extensions: :empty_array
+end
diff --git a/lib/steem/operation/report_over_production.rb b/lib/steem/operation/report_over_production.rb
new file mode 100644
index 0000000..33e0f1d
--- /dev/null
+++ b/lib/steem/operation/report_over_production.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::ReportOverProduction < Steem::Operation
+  def_attr reporter: :string
+  def_attr first_block: :string # FIXME signed_block_header
+  def_attr second_block: :string # FIXME signed_block_header
+end
diff --git a/lib/steem/operation/request_account_recovery.rb b/lib/steem/operation/request_account_recovery.rb
new file mode 100644
index 0000000..dc82849
--- /dev/null
+++ b/lib/steem/operation/request_account_recovery.rb
@@ -0,0 +1,6 @@
+class Steem::Operation::RequestAccountRecovery < Steem::Operation
+  def_attr recovery_account: :string
+  def_attr account_to_recover: :string
+  def_attr new_owner_authority: :authority
+  def_attr extensions: :empty_array
+end
diff --git a/lib/steem/operation/reset_account.rb b/lib/steem/operation/reset_account.rb
new file mode 100644
index 0000000..8e288df
--- /dev/null
+++ b/lib/steem/operation/reset_account.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::ResetAccount < Steem::Operation
+  def_attr reset_account: :string
+  def_attr account_to_reset: :string
+  def_attr new_owner_authority: :authority
+end
diff --git a/lib/steem/operation/set_reset_account.rb b/lib/steem/operation/set_reset_account.rb
new file mode 100644
index 0000000..bebb6e6
--- /dev/null
+++ b/lib/steem/operation/set_reset_account.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::SetResetAccount < Steem::Operation
+  def_attr account: :string
+  def_attr current_reset_account: :string
+  def_attr reset_account: :string
+end
diff --git a/lib/steem/operation/set_withdraw_vesting_route.rb b/lib/steem/operation/set_withdraw_vesting_route.rb
new file mode 100644
index 0000000..5338b39
--- /dev/null
+++ b/lib/steem/operation/set_withdraw_vesting_route.rb
@@ -0,0 +1,6 @@
+class Steem::Operation::SetWithdrawVestingRoute < Steem::Operation
+  def_attr from: :string
+  def_attr to: :string
+  def_attr percent: :uint16
+  def_attr auto_vest: :boolean
+end
diff --git a/lib/steem/operation/transfer.rb b/lib/steem/operation/transfer.rb
new file mode 100644
index 0000000..55cd58d
--- /dev/null
+++ b/lib/steem/operation/transfer.rb
@@ -0,0 +1,6 @@
+class Steem::Operation::Transfer < Steem::Operation
+  def_attr from: :string
+  def_attr to: :string
+  def_attr amount: :amount
+  def_attr memo: :string
+end
diff --git a/lib/steem/operation/transfer_from_savings.rb b/lib/steem/operation/transfer_from_savings.rb
new file mode 100644
index 0000000..d49ea5c
--- /dev/null
+++ b/lib/steem/operation/transfer_from_savings.rb
@@ -0,0 +1,7 @@
+class Steem::Operation::TransferFromSavings < Steem::Operation
+  def_attr from: :string
+  def_attr request_id: :uint32
+  def_attr to: :string
+  def_attr amount: :amount
+  def_attr memo: :string
+end
diff --git a/lib/steem/operation/transfer_to_savings.rb b/lib/steem/operation/transfer_to_savings.rb
new file mode 100644
index 0000000..42bb20c
--- /dev/null
+++ b/lib/steem/operation/transfer_to_savings.rb
@@ -0,0 +1,6 @@
+class Steem::Operation::TransferToSavings < Steem::Operation
+  def_attr from: :string
+  def_attr to: :string
+  def_attr amount: :amount
+  def_attr memo: :string
+end
diff --git a/lib/steem/operation/transfer_to_vesting.rb b/lib/steem/operation/transfer_to_vesting.rb
new file mode 100644
index 0000000..0b84bdd
--- /dev/null
+++ b/lib/steem/operation/transfer_to_vesting.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::TransferToVesting < Steem::Operation
+  def_attr from: :string
+  def_attr to: :string
+  def_attr amount: :amount
+end
diff --git a/lib/steem/operation/vote.rb b/lib/steem/operation/vote.rb
new file mode 100644
index 0000000..837d96a
--- /dev/null
+++ b/lib/steem/operation/vote.rb
@@ -0,0 +1,6 @@
+class Steem::Operation::Vote < Steem::Operation
+  def_attr voter: :string
+  def_attr author: :string
+  def_attr permlink: :string
+  def_attr weight: :int16
+end
diff --git a/lib/steem/operation/withdraw_vesting.rb b/lib/steem/operation/withdraw_vesting.rb
new file mode 100644
index 0000000..7b1a0f5
--- /dev/null
+++ b/lib/steem/operation/withdraw_vesting.rb
@@ -0,0 +1,4 @@
+class Steem::Operation::WithdrawVesting < Steem::Operation
+  def_attr account: :string
+  def_attr vesting_shares: :amount
+end
diff --git a/lib/steem/operation/witness_set_properties.rb b/lib/steem/operation/witness_set_properties.rb
new file mode 100644
index 0000000..e92a19b
--- /dev/null
+++ b/lib/steem/operation/witness_set_properties.rb
@@ -0,0 +1,5 @@
+class Steem::Operation::WitnessSetProperties < Steem::Operation
+  def_attr owner: :string
+  def_attr props: :witness_properties
+  def_attr extensions: :empty_array
+end
diff --git a/lib/steem/operation/witness_update.rb b/lib/steem/operation/witness_update.rb
new file mode 100644
index 0000000..f34e7ce
--- /dev/null
+++ b/lib/steem/operation/witness_update.rb
@@ -0,0 +1,7 @@
+class Steem::Operation::WitnessUpdate < Steem::Operation
+  def_attr owner: :string
+  def_attr url: :string
+  def_attr block_signing_key: :public_key
+  def_attr props: :chain_properties
+  def_attr fee: :amount
+end
diff --git a/lib/steem/transaction.rb b/lib/steem/transaction.rb
new file mode 100644
index 0000000..56cbd5e
--- /dev/null
+++ b/lib/steem/transaction.rb
@@ -0,0 +1,86 @@
+module Steem
+  class Transaction
+    include JSONable
+    include Utils
+    
+    ATTRIBUTES = %i(id ref_block_num ref_block_prefix expiration operations
+      extensions signatures)
+    
+    attr_accessor *ATTRIBUTES
+    
+    def initialize(options = {})
+      if !!(hex = options.delete(:hex))
+        marshal = Marshal.new(hex: hex)
+        marshal.transaction(trx: self)
+      end
+      
+      options.each do |k, v|
+        raise Steem::ArgumentError, "Invalid option specified: #{k}" unless ATTRIBUTES.include?(k.to_sym)
+
+        send("#{k}=", v)
+      end
+      
+      self.operations ||= []
+      self.extensions ||= []
+      self.signatures ||= []
+      
+      self.expiration = case @expiration
+      when String then Time.parse(@expiration + 'Z')
+      else; @expiration
+      end
+    end
+    
+    def inspect
+      properties = ATTRIBUTES.map do |prop|
+        if !!(v = instance_variable_get("@#{prop}"))
+          "@#{prop}=#{v}" 
+        end
+      end.compact.join(', ')
+      
+      "#<#{self.class.name} [#{properties}]>"
+    end
+    
+    def expiration
+      if @expiration.respond_to? :strftime
+        @expiration.strftime('%Y-%m-%dT%H:%M:%S')
+      else
+        @expiration
+      end
+    end
+    
+    def expired?
+      @expiration.nil? || @expiration < Time.now
+    end
+    
+    def [](key)
+      key = key.to_sym
+      send(key) if self.class.attributes.include?(key)
+    end
+
+    def []=(key, value)
+      key = key.to_sym
+      send("#{key}=", value) if self.class.attributes.include?(key)
+    end
+    
+    def ==(other_trx)
+      begin
+        return false if self[:ref_block_num].to_i != other_trx[:ref_block_num].to_i
+        return false if self[:ref_block_prefix].to_i != other_trx[:ref_block_prefix].to_i
+        return false if self[:expiration].to_i != other_trx[:expiration].to_i
+        return false if self[:operations].size != other_trx[:operations].size
+        
+        vals = self[:operations].sort.map do |k, v|
+          v.values.join.gsub(/[^a-zA-Z0-9]/, '')
+        end.join
+
+        ovals = other_trx[:operations].sort.map do |k, v|
+          v.values.join.gsub(/[^a-zA-Z0-9]/, '')
+        end.join
+        # binding.pry if vals != ovals
+        return vals == ovals
+      rescue
+        false
+      end
+    end
+  end
+end
diff --git a/lib/steem/transaction_builder.rb b/lib/steem/transaction_builder.rb
index f83cb41..63f74d8 100644
--- a/lib/steem/transaction_builder.rb
+++ b/lib/steem/transaction_builder.rb
@@ -56,28 +56,10 @@ module Steem
         else; trx
         end
         
-        trx_options = {
-          ref_block_num: trx['ref_block_num'],
-          ref_block_prefix: trx['ref_block_prefix'],
-          extensions: (trx['extensions']),
-          operations: trx['operations'],
-          signatures: (trx['signatures']),
-        }
-        
-        trx_options[:expiration] = case trx['expiration']
-        when String then Time.parse(trx['expiration'] + 'Z')
-        else; trx['expiration']
-        end
-        
-        options = options.merge(trx_options)
+        @trx = Transaction.new(trx)
       end
       
-      @ref_block_num = options[:ref_block_num]
-      @ref_block_prefix = options[:ref_block_prefix]
-      @operations = options[:operations] || []
-      @expiration = options[:expiration]
-      @extensions = options[:extensions] || []
-      @signatures = options[:signatures] || []
+      @trx ||= Transaction.new
       @chain = options[:chain] || :steem
       @error_pipe = options[:error_pipe] || STDERR
       @chain_id = options[:chain_id]
@@ -93,12 +75,9 @@ module Steem
     end
     
     def inspect
-      properties = %w(
-        ref_block_num ref_block_prefix expiration operations extensions
-        signatures
-      ).map do |prop|
+      properties = %w(trx).map do |prop|
         if !!(v = instance_variable_get("@#{prop}"))
-          "@#{prop}=#{v}" 
+          "@#{prop}=#{v.inspect}" 
         end
       end.compact.join(', ')
       
@@ -106,21 +85,12 @@ module Steem
     end
     
     def reset
-      @ref_block_num = nil
-      @ref_block_prefix = nil
-      @expiration = nil
-      @operations = []
-      @extensions = []
-      @signatures = []
+      @trx = Transaction.new
       @signed = false
       
       self
     end
     
-    def expired?
-      @expiration.nil? || @expiration < Time.now
-    end
-    
     # 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.
@@ -129,7 +99,7 @@ module Steem
     #
     # @return {TransactionBuilder}
     def prepare
-      if expired?
+      if @trx.expired?
         catch :prepare_header do; begin
           @database_api.get_dynamic_global_properties do |properties|
             block_number = properties.last_irreversible_block_num
@@ -146,9 +116,9 @@ module Steem
                 result
               end
               
-              @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
+              @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
             end
           end
         rescue => e
@@ -166,9 +136,9 @@ module Steem
     
     # Sets operations all at once, then prepares.
     def operations=(operations)
-      @operations = operations.map{ |op| normalize_operation(op) }
+      @trx.operations = operations.map{ |op| normalize_operation(op) }
       prepare
-      @operations
+      @trx.operations
     end
     
     # A quick and flexible way to append a new operation to the transaction.
@@ -194,8 +164,8 @@ module Steem
     #     builder.put(vote: vote1).put(vote: vote2)
     # @return {TransactionBuilder}
     def put(type, op = nil)
-      @expiration = nil
-      @operations << normalize_operation(type, op)
+      @trx.expiration = nil
+      @trx.operations << normalize_operation(type, op)
       prepare
       self
     end
@@ -218,9 +188,17 @@ module Steem
     #             ]],
     #               :signatures => ["1c45b65740b4b2c17c4bcf6bcc3f8d90ddab827d50532729fc3b8f163f2c465a532b0112ae4bf388ccc97b7c2e0bc570caadda78af48cf3c261037e65eefcd941e"]
     #     }
-    def transaction
-      prepare
-      sign
+    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
     
     # Appends to the `signatures` array of the transaction, built from a
@@ -229,39 +207,43 @@ module Steem
     # @return {Hash | TransactionBuilder} The fully signed transaction if a `wif` is provided or the instance of the {TransactionBuilder} if a `wif` has not yet been provided.
     def sign
       return self if @wif.empty?
-      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
-      }
+      return self if @trx.expired?
       
       unless @signed
         catch :serialize do; begin
-          transaction_hex_args = if app_base?
-            {trx: trx}
-          else
-            trx
-          end
-          
-          @database_api.get_transaction_hex(transaction_hex_args) do |result|
+          transaction_hex.tap do |result|
             hex = if app_base?
               result.hex
             else
               result
             end
+
+            # TODO Now that we have the hex from steemd, we will make our own
+            # and compare them for better errors and debugging.
+            
+            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
+            
+            raise SerializationMismatchError unless @trx == derrived_trx
             
-            hex = @chain_id + hex[0..-4] # Why do we have to chop the last two bytes?
+            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)
             private_keys = @wif.map{ |wif| Bitcoin::Key.from_base58 wif }
             ec = Bitcoin::OpenSSL_EC
             count = 0
-            sigs = []
             
             private_keys.each do |private_key|
               sig = nil
@@ -276,11 +258,10 @@ module Steem
                 break if canonical? sig
               end
               
-              @signatures << hexlify(sig)
+              @trx.signatures << hexlify(sig)
             end
             
             @signed = true
-            trx[:signatures] = @signatures
           end
         rescue => e
           if can_retry? e
@@ -292,7 +273,25 @@ module Steem
         end; end
       end
         
-      Hashie::Mash.new trx
+      @trx
+    end
+    
+    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
     
     # @return [Array] All public keys that could possibly sign for a given transaction.
diff --git a/lib/steem/type/amount.rb b/lib/steem/type/amount.rb
index 7aa9e99..69bbf4b 100644
--- a/lib/steem/type/amount.rb
+++ b/lib/steem/type/amount.rb
@@ -55,6 +55,8 @@ module Steem
           when 'STEEM' then 3
           when 'VESTS' then 6
           when 'SBD' then 3
+          when 'TESTS' then 3
+          when 'TBD' then 3
           else; raise TypeError, "Asset #{@asset} unknown."
           end
         end
diff --git a/steem-ruby.gemspec b/steem-ruby.gemspec
index 641193b..85de301 100644
--- a/steem-ruby.gemspec
+++ b/steem-ruby.gemspec
@@ -34,4 +34,6 @@ Gem::Specification.new do |spec|
   spec.add_dependency 'hashie', '~> 3.5', '>= 3.5.7'
   spec.add_dependency 'bitcoin-ruby', '~> 0.0', '>= 0.0.18'
   spec.add_dependency 'ffi', '~> 1.9', '>= 1.9.23'
+  spec.add_dependency 'bindata', '~> 2.4', '>= 2.4.4'
+  spec.add_dependency 'base58', '~> 0.2', '>= 0.2.3'
 end
diff --git a/test/steem/api_test.rb b/test/steem/api_test.rb
index 3f1b1e2..7ac2982 100644
--- a/test/steem/api_test.rb
+++ b/test/steem/api_test.rb
@@ -17,7 +17,7 @@ module Steem
       get_tags_used_by_author get_transaction_hex
       get_witness_by_account verify_authority)
     
-    METHOD_NAMES_2_ARGS = %i(get_account_bandwidth get_account_reputations
+    METHOD_NAMES_2_ARGS = %i(get_account_reputations
       get_active_votes get_content get_content_replies get_escrow
       get_expiring_vesting_delegations get_ops_in_block
       get_reblogged_by get_required_signatures get_trending_tags
@@ -76,13 +76,13 @@ module Steem
     end
     
     def test_inspect
-      assert_equal "#<CondenserApi [@chain=steem, @methods=<85 elements>]>", @api.inspect
+      assert_equal "#<CondenserApi [@chain=steem, @methods=<84 elements>]>", @api.inspect
     end
     
     def test_inspect_testnet
       vcr_cassette("#{@api.class.api_name}_testnet") do
         api = Api.new(chain: :test)
-        assert_equal "#<CondenserApi [@chain=test, @methods=<85 elements>]>", api.inspect
+        assert_equal "#<CondenserApi [@chain=test, @methods=<84 elements>]>", api.inspect
       end
     end
     
diff --git a/test/steem/broadcast_test.rb b/test/steem/broadcast_test.rb
index c93f11d..8e6a8e1 100644
--- a/test/steem/broadcast_test.rb
+++ b/test/steem/broadcast_test.rb
@@ -217,7 +217,7 @@ module Steem
         }
       }
       
-      vcr_cassette('broadcast_comment') do
+      vcr_cassette('broadcast_comment_with_metadata') do
         Broadcast.comment(@broadcast_options.merge(options)) do |result|
           if result.respond_to? :valid
             assert result.valid
@@ -655,8 +655,8 @@ module Steem
             sbd_interest_rate: 1000,
             account_subsidy_budget: 50000,
             account_subsidy_decay: 330782,
-            sbd_exchange_rate: '1.000 STEEM',
-            url: "https://steemit.com",
+            sbd_exchange_rate: {base: '1.000 SBD', quote: '1.000 STEEM'},
+            url: 'https://steemit.com',
             new_signing_key: 'STM8LoQjQqJHvotqBo7HjnqmUbFW9oJ2theyqonzUd9DdJ7YYHsvD'
           }
         }
@@ -853,7 +853,7 @@ module Steem
           from: @account_name,
           to: 'alice',
           agent: 'bob',
-          escrow_id: '1234',
+          escrow_id: 1234,
           sbd_amount: '0.000 SBD',
           steem_amount: '0.000 STEEM',
           fee: '0.000 STEEM',
diff --git a/test/steem/database_api_test.rb b/test/steem/database_api_test.rb
index 7aace57..bceeafc 100644
--- a/test/steem/database_api_test.rb
+++ b/test/steem/database_api_test.rb
@@ -2,6 +2,8 @@ require 'test_helper'
 
 module Steem
   class DatabaseApiTest < Steem::Test
+    include Utils
+    
     def setup
       @api = Steem::DatabaseApi.new(url: TEST_NODE)
       @jsonrpc = Jsonrpc.new(url: TEST_NODE)
@@ -633,5 +635,39 @@ module Steem
         end
       end
     end
+    
+    def test_computed_trx_id
+      trx = {
+        ref_block_num: 20,
+        ref_block_prefix: 2890012981,
+        expiration: '2018-10-15T19:52:09',
+        operations: [{type: :account_create_operation, value: {
+          fee: {amount: '0', precision: 3, nai: '@@000000021'},
+          creator: 'porter',
+          new_account_name: 'a2i-06e13981',
+          owner: {weight_threshold: 1, account_auths: [['porter', 1]], key_auths: []},
+          active: {weight_threshold: 1, account_auths: [['porter', 1]], key_auths: []},
+          posting: {weight_threshold: 1, account_auths: [['porter', 1]], key_auths: []},
+          memo_key: 'TST77yiRp7pgK52V7BPgq8mEYtyi9XLHKxCH6TDgKA86inFRYgWju',
+          json_metadata: ''
+        }}, {type: :transfer_to_vesting_operation, value: {
+          from: 'porter',
+          to: 'a2i-06e13981',
+          amount: {amount: '8204', precision: 3, nai: '@@000000021'}
+        }}],
+        extensions: []
+      }
+      
+      api = Steem::DatabaseApi.new(url: 'https://testnet.steemitdev.com')
+      
+      api.get_transaction_hex(trx: trx) do |result|
+        expected_hex = '1400351942ace9efc45b02090000000000000000035445535453000006706f727465720c6132692d3036653133393831010000000106706f72746572010000010000000106706f72746572010000010000000106706f7274657201000003260545a135c05a8adec1ad4676d046cd1312f16f41b2fb1c01cb2276cf2536e8000306706f727465720c6132692d30366531333938310c2000000000000003544553545300000000'
+        assert_equal expected_hex, result.hex
+        
+        hex = result.hex[0..-4] # drop empty signature array
+        trx_id = Digest::SHA256.hexdigest(unhexlify(hex))[0..39]
+        assert_equal trx_id, 'c68ad4eb64b3deb1033e002546481a2c9dfd9e9e'
+      end
+    end
   end
 end
diff --git a/test/steem/jsonrpc_test.rb b/test/steem/jsonrpc_test.rb
index 78b3968..7a2bd0f 100644
--- a/test/steem/jsonrpc_test.rb
+++ b/test/steem/jsonrpc_test.rb
@@ -14,6 +14,225 @@ module Steem
       vcr_cassette('jsonrpc_get_methods', record: :once) do
         apis = @jsonrpc.get_api_methods
         assert_equal Hashie::Mash, apis.class
+        
+        expected_apis = {
+          account_by_key_api: [
+            "get_key_references"
+          ],
+          block_api: [
+            "get_block",
+            "get_block_header"
+          ],
+          condenser_api: [
+            "broadcast_block",
+            "broadcast_transaction",
+            "broadcast_transaction_synchronous",
+            "get_account_count",
+            "get_account_history",
+            "get_account_references",
+            "get_account_reputations",
+            "get_account_votes",
+            "get_accounts",
+            "get_active_votes",
+            "get_active_witnesses",
+            "get_block",
+            "get_block_header",
+            "get_blog",
+            "get_blog_authors",
+            "get_blog_entries",
+            "get_chain_properties",
+            "get_comment_discussions_by_payout",
+            "get_config",
+            "get_content",
+            "get_content_replies",
+            "get_conversion_requests",
+            "get_current_median_history_price",
+            "get_discussions_by_active",
+            "get_discussions_by_author_before_date",
+            "get_discussions_by_blog",
+            "get_discussions_by_cashout",
+            "get_discussions_by_children",
+            "get_discussions_by_comments",
+            "get_discussions_by_created",
+            "get_discussions_by_feed",
+            "get_discussions_by_hot",
+            "get_discussions_by_promoted",
+            "get_discussions_by_trending",
+            "get_discussions_by_votes",
+            "get_dynamic_global_properties",
+            "get_escrow",
+            "get_expiring_vesting_delegations",
+            "get_feed",
+            "get_feed_entries",
+            "get_feed_history",
+            "get_follow_count",
+            "get_followers",
+            "get_following",
+            "get_hardfork_version",
+            "get_key_references",
+            "get_market_history",
+            "get_market_history_buckets",
+            "get_next_scheduled_hardfork",
+            "get_open_orders",
+            "get_ops_in_block",
+            "get_order_book",
+            "get_owner_history",
+            "get_post_discussions_by_payout",
+            "get_potential_signatures",
+            "get_reblogged_by",
+            "get_recent_trades",
+            "get_recovery_request",
+            "get_replies_by_last_update",
+            "get_required_signatures",
+            "get_reward_fund",
+            "get_savings_withdraw_from",
+            "get_savings_withdraw_to",
+            "get_state",
+            "get_tags_used_by_author",
+            "get_ticker",
+            "get_trade_history",
+            "get_transaction",
+            "get_transaction_hex",
+            "get_trending_tags",
+            "get_version",
+            "get_vesting_delegations",
+            "get_volume",
+            "get_withdraw_routes",
+            "get_witness_by_account",
+            "get_witness_count",
+            "get_witness_schedule",
+            "get_witnesses",
+            "get_witnesses_by_vote",
+            "lookup_account_names",
+            "lookup_accounts",
+            "lookup_witness_accounts",
+            "verify_account_authority",
+            "verify_authority"
+          ],
+          database_api: [
+            "find_account_recovery_requests",
+            "find_accounts",
+            "find_change_recovery_account_requests",
+            "find_comments",
+            "find_decline_voting_rights_requests",
+            "find_escrows",
+            "find_limit_orders",
+            "find_owner_histories",
+            "find_savings_withdrawals",
+            "find_sbd_conversion_requests",
+            "find_vesting_delegation_expirations",
+            "find_vesting_delegations",
+            "find_votes",
+            "find_withdraw_vesting_routes",
+            "find_witnesses",
+            "get_active_witnesses",
+            "get_config",
+            "get_current_price_feed",
+            "get_dynamic_global_properties",
+            "get_feed_history",
+            "get_hardfork_properties",
+            "get_order_book",
+            "get_potential_signatures",
+            "get_required_signatures",
+            "get_reward_funds",
+            "get_transaction_hex",
+            "get_version",
+            "get_witness_schedule",
+            "list_account_recovery_requests",
+            "list_accounts",
+            "list_change_recovery_account_requests",
+            "list_comments",
+            "list_decline_voting_rights_requests",
+            "list_escrows",
+            "list_limit_orders",
+            "list_owner_histories",
+            "list_savings_withdrawals",
+            "list_sbd_conversion_requests",
+            "list_vesting_delegation_expirations",
+            "list_vesting_delegations",
+            "list_votes",
+            "list_withdraw_vesting_routes",
+            "list_witness_votes",
+            "list_witnesses",
+            "verify_account_authority",
+            "verify_authority",
+            "verify_signatures"
+          ],
+          follow_api: [
+            "get_account_reputations",
+            "get_blog",
+            "get_blog_authors",
+            "get_blog_entries",
+            "get_feed",
+            "get_feed_entries",
+            "get_follow_count",
+            "get_followers",
+            "get_following",
+            "get_reblogged_by"
+          ],
+          jsonrpc: [
+            "get_methods",
+            "get_signature"
+          ],
+          market_history_api: [
+            "get_market_history",
+            "get_market_history_buckets",
+            "get_order_book",
+            "get_recent_trades",
+            "get_ticker",
+            "get_trade_history",
+            "get_volume"
+          ],
+          network_broadcast_api: [
+            "broadcast_block",
+            "broadcast_transaction"
+          ],
+          rc_api: [
+            "find_rc_accounts",
+            "get_resource_params",
+            "get_resource_pool"
+          ],
+          tags_api: [
+            "get_active_votes",
+            "get_comment_discussions_by_payout",
+            "get_content_replies",
+            "get_discussion",
+            "get_discussions_by_active",
+            "get_discussions_by_author_before_date",
+            "get_discussions_by_blog",
+            "get_discussions_by_cashout",
+            "get_discussions_by_children",
+            "get_discussions_by_comments",
+            "get_discussions_by_created",
+            "get_discussions_by_feed",
+            "get_discussions_by_hot",
+            "get_discussions_by_promoted",
+            "get_discussions_by_trending",
+            "get_discussions_by_votes",
+            "get_post_discussions_by_payout",
+            "get_replies_by_last_update",
+            "get_tags_used_by_author",
+            "get_trending_tags"
+          ]
+        }
+        
+        api_names = expected_apis.keys.map(&:to_s)
+        unexpected_apis = (api_names + apis.keys).uniq - api_names
+        missing_apis = (api_names + apis.keys).uniq - apis.keys
+        assert_equal [], unexpected_apis, "found unexpected apis"
+        assert_equal [], missing_apis, "missing expected apis"
+        
+        assert_equal expected_apis.size, apis.size, "expected #{expected_apis.size} apis, found: #{apis.size}"
+        
+        expected_apis.each do |api, methods|
+          method_names = apis[api].map(&:to_s)
+          unexpected_methods = (methods + method_names).uniq - methods
+          missing_methods = (methods + method_names).uniq - method_names
+          
+          assert_equal [], unexpected_methods, "found unexpected methods for api: #{api}"
+          assert_equal [], missing_methods, "missing expected methods for api: #{api}"
+          assert_equal expected_apis[api].size, apis[api].size, "expected #{expected_apis[api].size} methods for #{api}, found: #{apis[api].size}"
+        end
       end
     end
     
@@ -42,7 +261,7 @@ module Steem
             
             if api == :condenser_api
               if %i(
-                get_account_bandwidth get_block get_block_header get_escrow
+                get_block get_block_header get_escrow
                 get_witness_by_account get_recovery_request
               ).include? method
                 assert_nil signature.ret, "expect #{api}.#{method} to have nil ret"
diff --git a/test/steem/marshal_test.rb b/test/steem/marshal_test.rb
new file mode 100644
index 0000000..effeb66
--- /dev/null
+++ b/test/steem/marshal_test.rb
@@ -0,0 +1,377 @@
+require 'test_helper'
+
+module Steem
+  class MarshalTest < Steem::Test
+    include Utils
+    def setup
+      @database_api = Steem::DatabaseApi.new
+    end
+    
+    def test_trx_example_1
+      # block: 2997469, trx_id: 677040fdb081c1e67928ccc1320b51e57df1b86a
+      
+      trx = {
+        "ref_block_num": 48262,
+        "ref_block_prefix": 4209344763,
+        "expiration": "2016-07-07T19:18:15",
+        "operations": [
+          {
+            "type": "limit_order_create_operation",
+            "value": {
+              "owner": "gavvet",
+              "orderid": 1467919074,
+              "amount_to_sell": {"amount": "19477", "precision": 3, "nai": "@@000000013"},
+              "min_to_receive": {"amount": "67164", "precision": 3, "nai": "@@000000021"},
+              "fill_or_kill": false,
+              "expiration": "1969-12-31T23:59:59"
+            }
+          }
+        ]
+      }
+      
+      hex = @database_api.get_transaction_hex(trx: trx) do |result|
+        result.hex
+      end
+
+      marshal = Marshal.new(hex: hex)
+      
+      assert_equal 48262, marshal.uint16, 'expect ref_block_num: 48262'
+      assert_equal 4209344763, marshal.uint32, 'expect ref_block_prefix: 4209344763'
+      assert_equal Time.parse('2016-07-07T19:18:15Z'), marshal.point_in_time, 'expect expiration: 2016-07-07T19:18:15Z'
+      
+      assert_equal 1, marshal.signed_char, 'expect operations: 1'
+
+      assert_equal :limit_order_create_operation, marshal.operation_type, 'expect operation type: limit_order_create_operation'
+      assert_equal 'gavvet', marshal.string, 'expect owner: gavvet'
+      assert_equal 1467919074, marshal.uint32, 'expect order_id: 1467919074'
+      assert_equal Type::Amount.new('19.477 SBD').to_s, marshal.amount.to_s, 'expect amount_to_sell: 19.477 SBD'
+      assert_equal Type::Amount.new('67.164 STEEM').to_s, marshal.amount.to_s, 'expect min_to_receive: 67.164 STEEM'
+      assert_equal false, marshal.boolean, 'expect fill_or_kill: false'
+      assert_equal Time.parse('1969-12-31T23:59:59Z'), marshal.point_in_time, 'expect expiration: 1969-12-31T23:59:59Z'
+    end
+    
+    def test_trx_example_2
+      # block: 20000000, trx_id: 8ae2c3e1561462b2c7ed4c9128058e53ba9ca54f
+      
+      trx = {
+        "ref_block_num": 11501,
+        "ref_block_prefix": 655107659,
+        "expiration": "2018-02-19T07:26:45",
+        "operations": [
+          {
+            "type": "claim_reward_balance_operation",
+            "value": {
+              "account": "teacherpearline",
+              "reward_steem": {"amount": "0", "precision": 3, "nai": "@@000000021"},
+              "reward_sbd": {"amount": "845", "precision": 3, "nai": "@@000000013"},
+              "reward_vests": {"amount": "404731593", "precision": 6, "nai": "@@000000037"}
+            }
+          }
+        ]
+      }
+      
+      hex = @database_api.get_transaction_hex(trx: trx) do |result|
+        result.hex
+      end
+
+      marshal = Marshal.new(hex: hex)
+      
+      assert_equal 11501, marshal.uint16, 'expect ref_block_num: 11501'
+      assert_equal 655107659, marshal.uint32, 'expect ref_block_prefix: 655107659'
+      assert_equal Time.parse('2018-02-19T07:26:45Z'), marshal.point_in_time, 'expect expiration: 2018-02-19T07:26:45Z'
+      
+      assert_equal 1, marshal.signed_char, 'expect operations: 1'
+      
+      assert_equal :claim_reward_balance_operation, marshal.operation_type, 'expect operation type: claim_reward_balance_operation'
+      assert_equal 'teacherpearline', marshal.string, 'expect account: teacherpearline'
+      assert_equal Type::Amount.new('0.000 STEEM').to_s, marshal.amount.to_s, 'expect amount: 0.000 STEEM'
+      assert_equal Type::Amount.new('0.845 SBD').to_s, marshal.amount.to_s, 'expect amount: 0.845 SBD'
+      assert_equal Type::Amount.new('404.731593 VESTS').to_s, marshal.amount.to_s, 'expect amount: 0.000 VESTS'
+    end
+    
+    def test_trx_ad_hoc_1
+      builder = Steem::TransactionBuilder.new
+      
+      builder.put(account_update_operation: {
+        account: 'social',
+        memo_key: 'STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG',
+        json_metadata: '{}'
+      })
+      
+      marshal = Marshal.new(hex: builder.transaction_hex)
+      
+      assert marshal.uint16, 'expect ref_block_num'
+      assert marshal.uint32, 'expect ref_block_prefix'
+      assert marshal.point_in_time, 'expect expiration'
+      
+      assert_equal 1, marshal.signed_char, 'expect operations: 1'
+      assert_equal :account_update_operation, marshal.operation_type, 'expect operation type: account_update_operation'
+      assert_equal 'social', marshal.string, 'expect account: social'
+      assert 0, marshal.authority(optional: true)
+      assert 0, marshal.authority(optional: true)
+      assert 0, marshal.authority(optional: true)
+      assert_equal 'STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG', marshal.public_key, 'expect memo_key: STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG'
+      assert_equal '{}', marshal.string, 'expect json_metadata: {}'
+    end
+    
+    def test_trx_ad_hoc_2
+      builder = Steem::TransactionBuilder.new
+      
+      builder.put(comment_operation: {
+        "parent_author": "",
+        "parent_permlink": "dlive",
+        "author": "zaku",
+        "permlink": "98bb0d05-9e15-11e8-b733-0242ac110003",
+        "title": "MODERN COMBAT 5 [BLACK OUT] : DAILY GAMEPLAY #11-August-2018",
+        "body": "[![Thumbnail](https:\/\/images.dlive.io\/4a85e837-9e18-11e8-9a43-0242ac110002)](https:\/\/dlive.io\/video\/zaku\/98bb0d05-9e15-11e8-b733-0242ac110003)\n\n\n# MC5: MODERN COMBAT 5 Videos Upload:\n\n![flower-squiggle-2s.png](https:\/\/cdn.steemitimages.com\/DQmcFuQRq8xwnjitZ1VPK7JvyCjAP6DJ1W4xqUSdvAugWiV\/flower-squiggle-2s.png)\n\n* [MODERN COMBAT 5 (BLACKOUT) : Multiplayer Fight + COMBAT PACK Unlock + Level up to 5](https:\/\/steemit.com\/games\/@zaku\/modern-combat-5-blackout-multiplayer-fight-combat-pack-unlock-level-up-to-5)\n\n* [MODERN COMBAT 5 (BLACK OUT) : Multiplayer Fight ( Free-For-All + VIP ) + Reward Redeem + GOLDEN BOSK Purchase](https:\/\/steemit.com\/dlive\/@zaku\/768946c6-98a6-11e8-8af8-0242ac110003)\n\n* [MODERN COMBAT GAMEPLAY : Multiplayer fights](https:\/\/dlive.io\/video\/zaku\/c2fb655a-9a5c-11e8-9e1e-0242ac110003)\n\n* [MODERN COMBAT 5 (BLACK OUT) : Multiplayer Fight ( Free-For-All + VIP ) + Reward Redeem + GOLDEN BOSK Purchase](https:\/\/steemit.com\/dlive\/@zaku\/768946c6-98a6-11e8-8af8-0242ac110003)\n\n* [MODERN COMBAT GAMEPLAY : Multiplayer fights](https:\/\/steemit.com\/dlive\/@zaku\/c2fb655a-9a5c-11e8-9e1e-0242ac110003)\n\n* [MODERN COMBAT 5 [BLACK OUT] : GAMEPLAY #08-Aug-2018](https:\/\/steemit.com\/dlive\/@zaku\/8fb4c940-9bb9-11e8-9a98-0242ac110003)\n\n# MODERN COMBAT VERSUS Videos Upload:\n\n![flower-squiggle-2s.png](https:\/\/cdn.steemitimages.com\/DQmcFuQRq8xwnjitZ1VPK7JvyCjAP6DJ1W4xqUSdvAugWiV\/flower-squiggle-2s.png)\n\n* [MODERN COMBAT VERSUS : Kult Unlock (POISON BOSS)](https:\/\/steemit.com\/dlive\/@zaku\/acefa790-870a-11e8-adb2-bf4283a63cb9)\n\n* [MODERN COMBAT VERSUS : Ultimate Kult Agent + Multiplayer Fight & Agent Upgrade [480p]](https:\/\/steemit.com\/dlive\/@zaku\/e7f2eaa0-8809-11e8-adb2-bf4283a63cb9)\n\n* [MODERN COMBAT VERSUS : LEAGUE PROMOTION TO GOLD LEAGUE](https:\/\/steemit.com\/dlive\/@zaku\/c76a52e0-882d-11e8-adb2-bf4283a63cb9)\n\n* [MODERN COMBAT VERSUS : League Promotion + Reward Collect + Multiplayer Fight](https:\/\/steemit.com\/dlive\/@zaku\/0da0ccf0-8a57-11e8-b2de-f7be8f055a16)\n\n* [Modern Combat Versus : 20$ Worth Pack buy using xbox gift cards + Kult Ultimate Upgrade + Beast mode](https:\/\/steemit.com\/dlive\/@zaku\/2306b3c0-8e80-11e8-b2de-f7be8f055a16)\n\n* [Modern Combat Versus: Turrent vs Enemy ](https:\/\/steemit.com\/dlive\/@zaku\/321eb460-917e-11e8-b2de-f7be8f055a16)\n\n* [MODERN COMBAT VERSUS : Multiplayer Game play + New Agent + Reward Redeem](https:\/\/steemit.com\/dlive\/@zaku\/1a46d3ae-989c-11e8-9e1e-0242ac110003)\n\n*[MODERN COMBAT VERSUS : Multiplayer Fight's + Loot Open + Reward Redeem](https:\/\/steemit.com\/dlive\/@zaku\/199e4a06-996d-11e8-9e1e-0242ac110003)\n\n* [MODERN COMBAT VERSUS : Daily Gameplay #07-Aug-2018](https:\/\/steemit.com\/dlive\/@zaku\/e5b61976-9a70-11e8-a04f-0242ac110003)\n\n* [MODERN COMBAT VERSUS : GAMEPLAY #8-Aug-2018](https:\/\/steemit.com\/dlive\/@zaku\/b0da8459-9bd1-11e8-a04f-0242ac110003)\n\nhttps:\/\/steemitimages.com\/0x0\/https:\/\/cdn.steemitimages.com\/DQmaKdWkztaw7QsWpgFLiWYma491XfZPBCitb1oo9gYMn7V\/DLive_br.gif\n\n# Some Important Post that might help you:\n\n![stars.png](https:\/\/cdn.steemitimages.com\/DQmYfAmraAMmuMvGM7UGxMpSS6jiPTimaGbGbFAcjU36E1r\/stars.png)\n\n* [Introducing Instant Voting Bot - @bdvoter with guaranteed profit for Buyer and Delegator](https:\/\/steemit.com\/bdvoter\/@bdvoter\/introducing-instant-voting-bot-bdvoter-with-granteed-profit-for-buyer-and-delegator)\n\n\n* [STEEMIT \u098f \u09b8\u09a0\u09bf\u0995 \u09ad\u09be\u09ac\u09c7 \u0989\u09aa\u09be\u09b0\u09cd\u099c\u09a8 \u0995\u09b0\u09be\u09b0 \u09a8\u09bf\u09df\u09ae \u0993 \u09ac\u09bf\u09a1 \u09ac\u09cb\u099f\u09c7\u09b0 \u09ac\u09bf\u09b8\u09cd\u09a4\u09be\u09b0\u09bf\u09a4 \u09b8\u09ae\u09cd\u09aa\u09b0\u09cd\u0995\u09c7 \u0986\u09b2\u09cb\u099a\u09a8\u09be](https:\/\/steemit.com\/bidbot\/@zaku\/steemit)\n\n* [Minnowbooster \u098f\u09b0 \u09b8\u0995\u09b2 \u09b8\u09be\u09b0\u09cd\u09ad\u09bf\u09b8 \u09a8\u09bf\u09df\u09c7 \u09ac\u09be\u0982\u09b2\u09be \u0986\u09b2\u09cb\u099a\u09a8\u09be \u0964 ( Part - 1 )](https:\/\/steemit.com\/minnowbooster\/@zaku\/minnowbooster)\n\n* [MinnowBooster \u098f\u09b0 \u09b8\u0995\u09b2 \u09b8\u09be\u09b0\u09cd\u09ad\u09bf\u09b8 \u09a8\u09bf\u09df\u09c7 \u09ac\u09be\u0982\u09b2\u09be \u0986\u09b2\u09cb\u099a\u09a8\u09be \u0964 ( Part - 2 )](https:\/\/steemit.com\/minnowbooster\/@zaku\/minnowbooster-part-2)\n\n* [\u09ae\u09bf\u09a8\u09cb\u09ac\u09c1\u09b8\u09cd\u099f\u09be\u09b0 \u098f\u09b0 \u09b8\u09a0\u09bf\u0995 \u09ac\u09cd\u09af\u09ac\u09b9\u09be\u09b0 \u0993 \u09ac\u09cd\u09b2\u0995\u09b2\u09bf\u09b8\u09cd\u099f \u09a5\u09c7\u0995\u09c7 \u09aa\u09b0\u09bf\u09a4\u09cd\u09b0\u09be\u09a8 \u098f\u09b0 \u099c\u09a8\u09cd\u09af \u0995\u09b0\u09a8\u09c0\u09df \u09aa\u09a6\u0995\u09cd\u09b7\u09c7\u09aa](https:\/\/steemit.com\/minnowbooster\/@zaku\/569dxc)\n\n\n* [\u09af\u09c7\u09ad\u09be\u09ac\u09c7 @steemitbd \u09ac\u09be\u0982\u09b2\u09be\u09a6\u09c7\u09b6\u09c0 \u0987\u0989\u099c\u09be\u09b0\u09a6\u09c7\u09b0 \u09b8\u09b9\u09af\u09cb\u0997\u09bf\u09a4\u09be\u09df \u098f\u0997\u09bf\u09df\u09c7 \u0986\u09b8\u099b\u09c7\u0964](https:\/\/steemit.com\/steemitbd\/@zaku\/steemitbd) \n\n* [Why @steemitbd is the best communication for Bangladeshi Newbie Users. (Find Out Here)](https:\/\/steemit.com\/community\/@zaku\/why-steemitbd-is-the-best-communication-for-bangladeshi-newbie-users-find-out-here)\n\n* [ANNOUNCED: STEEMITBD PROMOTION CONTEST](https:\/\/steemit.com\/promo-steemitbd\/@zaku\/announced-steemitbd-promotion-contest)\n\nhttps:\/\/steemitimages.com\/0x0\/https:\/\/cdn.steemitimages.com\/DQmaKdWkztaw7QsWpgFLiWYma491XfZPBCitb1oo9gYMn7V\/DLive_br.gif\n\n\n\n# Gaming Youtube Channels:\n\n![9.gif](https:\/\/cdn.steemitimages.com\/DQmRU6fYkhsKC4CkjVS7zXDW4994rqoDmoyKJXZnxTSfpdU\/9.gif)\n\n* [MC5 Official Youtube Channel](https:\/\/www.youtube.com\/channel\/UCF1C_Ptm9Pdtpbo1n0C42LQ)\n\n* [Jwae - Modern Combat 5](https:\/\/www.youtube.com\/user\/fskjwae)\n\n* [Hybrid Gamer](https:\/\/www.youtube.com\/channel\/UCl8cWr8TFvNwiQKVoLwwUyw)\n\n* [Techzamazing](https:\/\/www.youtube.com\/user\/Techzamazing)\n\n* [GameRiot](https:\/\/www.youtube.com\/user\/GameRiotArmy)\n\n<sub>***INFORMATION TAKEN FROM MC5 WEBSITE***<\/sub>\n\n[![zakucustomfooter2.gif](https:\/\/cdn.steemitimages.com\/DQmcdJWyg3fYukURPqrwb4nqoRPvEQVd6d1RqkUopxTtBPR\/zakucustomfooter2.gif)](https:\/\/discord.gg\/Z3P6bbt)<\/center>\n\n\nMy video is at [DLive](https:\/\/dlive.io\/video\/zaku\/98bb0d05-9e15-11e8-b733-0242ac110003)",
+        "json_metadata": "{\"tags\":[\"dlive\",\"dlive-video\",\"Gaming\",\"steemgamingcommunity\",\"games\",\"dailygames\",\"mc5\",\"moderncombat5blackout\"],\"app\":\"dlive\/0.1\",\"format\":\"markdown\",\"language\":\"English\",\"thumbnail\":\"https:\/\/images.dlive.io\/4a85e837-9e18-11e8-9a43-0242ac110002\"}"
+      })
+      
+      marshal = Marshal.new(hex: builder.transaction_hex)
+      
+      assert marshal.uint16, 'expect ref_block_num'
+      assert marshal.uint32, 'expect ref_block_prefix'
+      assert marshal.point_in_time, 'expect expiration'
+      
+      assert_equal 1, marshal.signed_char, 'expect operations: 1'
+      assert_equal :comment_operation, marshal.operation_type, 'expect operation type: comment_operation'
+      assert_equal '', marshal.string, 'expect parent_author: <empty>'
+      assert_equal 'dlive', marshal.string, 'expect parent_permlink: dlive'
+      assert_equal 'zaku', marshal.string, 'expect author: zaku'
+      assert_equal '98bb0d05-9e15-11e8-b733-0242ac110003', marshal.string, 'expect permlink: 98bb0d05-9e15-11e8-b733-0242ac110003'
+      assert_equal 'MODERN COMBAT 5 [BLACK OUT] : DAILY GAMEPLAY #11-August-2018', marshal.string, 'expect title: MODERN COMBAT 5 [BLACK OUT] : DAILY GAMEPLAY #11-August-2018'
+      assert_equal 5588, marshal.string.size, 'expect body length: 5588'
+      assert_equal "{\"tags\":[\"dlive\",\"dlive-video\",\"Gaming\",\"steemgamingcommunity\",\"games\",\"dailygames\",\"mc5\",\"moderncombat5blackout\"],\"app\":\"dlive\/0.1\",\"format\":\"markdown\",\"language\":\"English\",\"thumbnail\":\"https:\/\/images.dlive.io\/4a85e837-9e18-11e8-9a43-0242ac110002\"}", marshal.string, 'expect json_metadata: {...}'
+    end
+    
+    def test_trx_ad_hoc_3
+      builder = Steem::TransactionBuilder.new
+      
+      builder.put(account_update_operation: {
+        account: 'social',
+        owner: {
+          weight_threshold: 1,
+          account_auths: [],
+          key_auths: [['STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG', 1]],
+        },
+        active: {
+          weight_threshold: 1,
+          account_auths: [],
+          key_auths: [['STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG', 1]],
+        },
+        posting: {
+          weight_threshold: 1,
+          account_auths: [],
+          key_auths: [['STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG', 1]],
+        },
+        memo_key: 'STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG',
+        json_metadata: '{}'
+      })
+      
+      marshal = Marshal.new(hex: builder.transaction_hex)
+      
+      assert marshal.uint16, 'expect ref_block_num'
+      assert marshal.uint32, 'expect ref_block_prefix'
+      assert marshal.point_in_time, 'expect expiration'
+      
+      assert_equal 1, marshal.signed_char, 'expect operations: 1'
+      assert_equal :account_update_operation, marshal.operation_type, 'expect operation type: account_update_operation'
+      assert_equal 'social', marshal.string, 'expect account: social'
+      
+      marshal.authority(optional: true).tap do |owner|
+        assert_equal 1, owner[:weight_threshold], 'expect owner authority weight_threshold: 1'
+        assert_equal [], owner[:account_auths], 'expect owner authority account_auths: []'
+        assert_equal [["STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG", 1]], owner[:key_auths], 'expect owner authority key_auths: [["STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG", 1]]'
+      end
+
+      marshal.authority(optional: true).tap do |active|
+        assert_equal 1, active[:weight_threshold], 'expect active authority weight_threshold: 1'
+        assert_equal [], active[:account_auths], 'expect active authority account_auths: []'
+        assert_equal [["STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG", 1]], active[:key_auths], 'expect active authority key_auths: [["STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG", 1]]'
+      end
+
+      marshal.authority(optional: true).tap do |posting|
+        assert_equal 1, posting[:weight_threshold], 'expect posting authority weight_threshold: 1'
+        assert_equal [], posting[:account_auths], 'expect posting authority account_auths: []'
+        assert_equal [["STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG", 1]], posting[:key_auths], 'expect posting authority key_auths: [["STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG", 1]]'
+      end
+      
+      assert_equal 'STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG', marshal.public_key, 'expect memo_key: STM8ZSyzjPm48GmUuMSRufkVYkwYbZzbxeMysAVp7KFQwbTf98TcG'
+      assert_equal '{}', marshal.string, 'expect json_metadata: {}'
+    end
+    
+    def test_trx_ad_hoc_4
+      builder = Steem::TransactionBuilder.new
+      
+      builder.put(escrow_transfer: { # FIXME Why do we have to use escrow_transfer and not :escrow_transfer_operation here?
+        from: 'social',
+        to: 'alice',
+        agent: 'bob',
+        escrow_id: 1234,
+        sbd_amount: '0.000 SBD',
+        steem_amount: '0.000 STEEM',
+        fee: '0.000 STEEM',
+        ratification_deadline: '2018-10-15T19:52:09',
+        escrow_expiration: '2018-10-15T19:52:09',
+        json_meta: '{}'
+      })
+      
+      marshal = Marshal.new(hex: builder.transaction_hex)
+      
+      assert marshal.uint16, 'expect ref_block_num'
+      assert marshal.uint32, 'expect ref_block_prefix'
+      assert marshal.point_in_time, 'expect expiration'
+      
+      assert_equal 1, marshal.signed_char, 'expect operations: 1'
+      assert_equal :escrow_transfer_operation, marshal.operation_type, 'expect operation type: escrow_transfer_operation'
+      assert_equal 'social', marshal.string, 'expect from: social'
+      assert_equal 'alice', marshal.string, 'expect to: alice'
+      assert_equal '0.000 SBD', marshal.amount.to_s, 'expect sbd_amount: 0.000 SBD'
+      assert_equal '0.000 STEEM', marshal.amount.to_s, 'expect steem_amount: 0.000 STEEM'
+      assert_equal 1234, marshal.uint32, 'expect escrow_id: 1234'
+      assert_equal 'bob', marshal.string, 'expect agent: bob'
+      assert_equal '0.000 STEEM', marshal.amount.to_s, 'expect fee: 0.000 STEEM'
+      assert_equal '{}', marshal.string, 'expect json_meta: {}'
+      assert_equal Time.parse('2018-10-15 12:52:09 -0700'), marshal.point_in_time, 'expect escrow_expiration: 2018-10-15 12:52:09 -0700'
+      assert_equal Time.parse('2018-10-15 12:52:09 -0700'), marshal.point_in_time, 'expect escrow_expiration: 2018-10-15 12:52:09 -0700'
+    end
+    
+    def test_trx_ad_hoc_5
+      builder = Steem::TransactionBuilder.new
+      
+      builder.put(change_recovery_account_operation: {
+        account_to_recover: 'alice',
+        new_recovery_account: 'bob',
+        extensions: []
+      })
+      
+      marshal = Marshal.new(hex: builder.transaction_hex)
+      
+      assert marshal.uint16, 'expect ref_block_num'
+      assert marshal.uint32, 'expect ref_block_prefix'
+      assert marshal.point_in_time, 'expect expiration'
+      
+      assert_equal 1, marshal.signed_char, 'expect operations: 1'
+      assert_equal :change_recovery_account_operation, marshal.operation_type, 'expect operation type: change_recovery_account_operation'
+      assert_equal 'alice', marshal.string, 'expect account_to_recover: alice'
+      assert_equal 'bob', marshal.string, 'expect new_recovery_account: bob'
+    end
+    
+    def test_trx_ad_hoc_6
+      builder = Steem::TransactionBuilder.new
+      
+      builder.put(comment_operation: {
+        author: 'alice',
+        permlink: 'permlink',
+        parent_permlink: 'parent_permlink',
+        title: 'title',
+        body: 'body'
+      })
+
+      builder.put(comment_options: { # FIXME Why do we have to use comment_options and not :comment_options_operation here?
+        author: 'alice',
+        permlink: 'permlink',
+        max_accepted_payout: '1000000.000 SBD',
+        percent_steem_dollars: 10000,
+        allow_votes: true,
+        allow_curation_rewards: true,
+        extensions: []
+      })
+      
+      builder.put(vote_operation: {
+        voter: 'alice',
+        author: 'alice',
+        permlink: 'permlink',
+        weight: 10000
+      })
+      
+      marshal = Marshal.new(hex: builder.transaction_hex)
+      
+      assert marshal.uint16, 'expect ref_block_num'
+      assert marshal.uint32, 'expect ref_block_prefix'
+      assert marshal.point_in_time, 'expect expiration'
+      
+      assert_equal 3, marshal.signed_char, 'expect operations: 3'
+      
+      assert_equal :comment_operation, marshal.operation_type, 'expect operation type: comment_operation'
+      assert_equal '', marshal.string, 'expect parent_author: <empty>'
+      assert_equal 'parent_permlink', marshal.string, 'expect parent_permlink: parent_permlink'
+      assert_equal 'alice', marshal.string, 'expect author: alice'
+      assert_equal 'permlink', marshal.string, 'expect permlink: permlink'
+      assert_equal 'title', marshal.string, 'expect title: title'
+      assert_equal 'body', marshal.string, 'expect body: body'
+      assert_equal '', marshal.string, 'expect json_metadata: <empty>'
+
+      assert_equal :comment_options_operation, marshal.operation_type, 'expect operation type: comment_options_operation'
+      assert_equal 'alice', marshal.string, 'expect author: alice'
+      assert_equal 'permlink', marshal.string, 'expect permlink: permlink'
+      assert_equal '1000000.000 SBD', marshal.amount.to_s, 'expect max_accepted_payout: 1000000.000 SBD'
+      assert_equal 10000, marshal.uint16, 'expect percent_steem_dollars: 10000'
+      assert_equal false, marshal.boolean, 'expect allow_replies: false'
+      assert_equal false, marshal.boolean, 'expect allow_votes: false'
+      assert_equal false, marshal.boolean, 'expect allow_curation_rewards: false'
+      
+      assert_equal :vote_operation, marshal.operation_type, 'expect operation type: vote_operation'
+      assert_equal 'alice', marshal.string, 'expect voter: alice'
+      assert_equal 'alice', marshal.string, 'expect author: alice'
+      assert_equal 'permlink', marshal.string, 'expect permlink: permlink'
+      assert_equal 10000, marshal.int16, 'expect weight: 10000'
+    end
+    
+    def test_trx_ad_hoc_9
+      # Example transaction:
+      #
+      # ref_block_num: 20,
+      # ref_block_prefix: 2890012981,
+      # expiration: '2018-10-15T19:52:09',
+      # operations: [{type: :account_create_operation, value: {
+      #   fee: Type::Amount.new('0.000 TESTS'),
+      #   creator: 'porter',
+      #   new_account_name: 'a2i-06e13981',
+      #   owner: {weight_threshold: 1, account_auths: [['porter', 1]], key_auths: []},
+      #   active: {weight_threshold: 1, account_auths: [['porter', 1]], key_auths: []},
+      #   posting: {weight_threshold: 1, account_auths: [['porter', 1]], key_auths: []},
+      #   memo_key: 'TST77yiRp7pgK52V7BPgq8mEYtyi9XLHKxCH6TDgKA86inFRYgWju',
+      #   json_metadata: ''
+      # }}, {type: :transfer_to_vesting_operation, value: {
+      #   from: 'porter',
+      #   to: 'a2i-06e13981',
+      #   amount: Type::Amount.new('8.204 TESTS')
+      # }}]
+
+      marshal = Marshal.new(chain: :test, hex: '1400351942ace9efc45b02090000000000000000035445535453000006706f727465720c6132692d3036653133393831010000000106706f72746572010000010000000106706f72746572010000010000000106706f7274657201000003260545a135c05a8adec1ad4676d046cd1312f16f41b2fb1c01cb2276cf2536e8000306706f727465720c6132692d30366531333938310c2000000000000003544553545300000000')
+      
+      assert_equal 20, marshal.uint16, 'expect ref_block_num: 20'
+      assert_equal 2890012981, marshal.uint32, 'expect ref_block_prefix: 2890012981'
+      assert_equal Time.parse('2018-10-15 12:52:09 -0700'), marshal.point_in_time, 'expect expiration: 2018-10-15 12:52:09 -0700'
+      
+      assert_equal 2, marshal.signed_char, 'expect operations: 2'
+      
+      assert_equal :account_create_operation, marshal.operation_type, 'expect operation type: account_create_operation'
+      assert_equal Type::Amount.new('0.000 TESTS').to_s, marshal.amount.to_s, 'expect amount: 0.000 TESTS'
+      assert_equal 'porter', marshal.string, 'expect creator: porter'
+      assert_equal 'a2i-06e13981', marshal.string, 'expect new_account_name: a2i-06e13981'
+
+      marshal.authority.tap do |owner|
+        assert_equal 1, owner[:weight_threshold], 'expect owner authority weight_threshold: 1'
+        assert_equal [["porter", 1]], owner[:account_auths], 'expect owner authority account_auths: [["porter", 1]]'
+        assert_equal [], owner[:key_auths], 'expect owner authority key_auths: []'
+      end
+      
+      marshal.authority.tap do |active|
+        assert_equal 1, active[:weight_threshold], 'expect active authority weight_threshold: 1'
+        assert_equal [["porter", 1]], active[:account_auths], 'expect active authority account_auths: [["porter", 1]]'
+        assert_equal [], active[:key_auths], 'expect active authority key_auths: []'
+      end
+      
+      marshal.authority.tap do |posting|
+        assert_equal 1, posting[:weight_threshold], 'expect posting authority weight_threshold: 1'
+        assert_equal [["porter", 1]], posting[:account_auths], 'expect posting authority account_auths: [["porter", 1]]'
+        assert_equal [], posting[:key_auths], 'expect posting authority key_auths: []'
+      end
+      
+      assert_equal 'TST77yiRp7pgK52V7BPgq8mEYtyi9XLHKxCH6TDgKA86inFRYgWju', marshal.public_key, 'expect memo_key: TST77yiRp7pgK52V7BPgq8mEYtyi9XLHKxCH6TDgKA86inFRYgWju'
+      assert_equal '', marshal.string, 'expect empty json_metata'
+    end
+  end
+end
diff --git a/test/steem/transaction_builder_test.rb b/test/steem/transaction_builder_test.rb
index daa1472..4bc743c 100644
--- a/test/steem/transaction_builder_test.rb
+++ b/test/steem/transaction_builder_test.rb
@@ -104,7 +104,9 @@ module Steem
           weight: 10000
         })
         
-        assert builder.sign
+        trx = builder.sign
+        assert trx
+        assert trx[:id]
       end
     end
     
@@ -169,7 +171,7 @@ module Steem
         })
       end
       
-      assert 1, builder.operations.size
+      assert 1, builder.transaction(sign: false).operations.size
     end
     
     def test_put_array
@@ -184,7 +186,7 @@ module Steem
         }])
       end
       
-      assert 1, builder.operations.size
+      assert 1, builder.transaction(sign: false).operations.size
     end
     
     def test_put_symbol
@@ -199,7 +201,7 @@ module Steem
         })
       end
       
-      assert 1, builder.operations.size
+      assert 1, builder.transaction(sign: false).operations.size
     end
     
     def test_put_string
@@ -214,7 +216,7 @@ module Steem
         })
       end
       
-      assert 1, builder.operations.size
+      assert 1, builder.transaction(sign: false).operations.size
     end
     
     def test_potential_signatures
diff --git a/test/steem/witness_api_test.rb b/test/steem/witness_api_test.rb
deleted file mode 100644
index d6eecec..0000000
--- a/test/steem/witness_api_test.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-require 'test_helper'
-
-module Steem
-  class WitnessApiTest < Steem::Test
-    def setup
-      @api = Steem::WitnessApi.new(url: TEST_NODE)
-      @jsonrpc = Jsonrpc.new(url: TEST_NODE)
-      @methods = @jsonrpc.get_api_methods[@api.class.api_name]
-    end
-    
-    def test_api_class_name
-      assert_equal 'WitnessApi', Steem::WitnessApi::api_class_name
-    end
-    
-    def test_inspect
-      assert_equal "#<WitnessApi [@chain=steem, @methods=<2 elements>]>", @api.inspect
-    end
-    
-    def test_method_missing
-      assert_raises NoMethodError do
-        @api.bogus
-      end
-    end
-    
-    def test_all_respond_to
-      @methods.each do |key|
-        assert @api.respond_to?(key), "expect rpc respond to #{key}"
-      end
-    end
-    
-    def test_get_account_bandwidth
-      vcr_cassette('witness_api_get_account_bandwidth', record: :once) do
-        options = {
-          account: 'steemit',
-          type: 'forum'
-        }
-        
-        @api.get_account_bandwidth(options) do |result|
-          assert_equal Hashie::Mash, result.class
-        end
-      end
-    end
-    
-    def test_get_reserve_ratio
-      vcr_cassette('witness_api_get_reserve_ratio', record: :once) do
-        @api.get_reserve_ratio do |result|
-          assert_equal Hashie::Mash, result.class
-        end
-      end
-    end
-  end
-end
\ No newline at end of file
diff --git a/test/test_helper.rb b/test/test_helper.rb
index bd963b6..b7e2241 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -7,6 +7,7 @@ SimpleCov.merge_timeout 3600
 
 require 'steem'
 require 'minitest/autorun'
+require 'minitest/line/describe_track'
 require 'webmock/minitest'
 require 'vcr'
 require 'yaml'
-- 
GitLab