lib/nanook/block.rb in nanook-2.5.1 vs lib/nanook/block.rb in nanook-3.0.0

- old
+ new

@@ -1,7 +1,10 @@ -class Nanook +# frozen_string_literal: true +require_relative 'util' + +class Nanook # The <tt>Nanook::Block</tt> class contains methods to discover # publicly-available information about blocks on the nano network. # # A block is represented by a unique id like this: # @@ -15,27 +18,44 @@ # Or compose the longhand way like this: # # rpc_conn = Nanook::Rpc.new # block = Nanook::Block.new(rpc_conn, "FBF8B0E...") class Block + include Nanook::Util def initialize(rpc, block) @rpc = rpc - @block = block - block_required! # All methods expect a block + @block = block.to_s end - # Returns the {Nanook::Account} of the block. + # Returns the block hash id. # # ==== Example: - # block.account # => Nanook::Account # - # @return [Nanook::Account] the account of the block - def account - Nanook::Account.new(@rpc, rpc(:block_account, :hash)[:account]) + # block.id #=> "FBF8B0E..." + # + # @return [String] the block hash id + def id + @block end + # @param other [Nanook::Block] block to compare + # @return [Boolean] true if blocks are equal + def ==(other) + other.class == self.class && + other.id == id + end + alias eql? == + + # The hash value is used along with #eql? by the Hash class to determine if two objects + # reference the same hash key. + # + # @return [Integer] + def hash + id.hash + end + # Stop generating work for a block. # # ==== Example: # # block.cancel_work # => true @@ -57,256 +77,429 @@ # # block.chain(limit: 2) # # ==== Example reponse: # - # [ - # "36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E", - # "FBF8B0E6623A31AB528EBD839EEAA91CAFD25C12294C46754E45FD017F7939EB" - # ] + # [Nanook::Block, ...] # # @param limit [Integer] maximum number of block hashes to return (default is 1000) # @param offset [Integer] return the account chain block hashes offset by the specified number of blocks (default is 0) def chain(limit: 1000, offset: 0) - response = rpc(:chain, :block, count: limit, offset: offset)[:blocks] - Nanook::Util.coerce_empty_string_to_type(response, Array) + params = { + count: limit, + offset: offset, + _access: :blocks, + _coerce: Array + } + + rpc(:chain, :block, params).map do |block| + as_block(block) + end end - alias_method :ancestors, :chain + alias ancestors chain # Request confirmation for a block from online representative nodes. # Will return immediately with a boolean to indicate if the request for # confirmation was successful. Note that this boolean does not indicate - # the confirmation status of the block. If confirmed, your block should - # appear in {Nanook::Node#confirmation_history} within a short amount of - # time, or you can use the convenience method {Nanook::Block#confirmed_recently?} + # the confirmation status of the block. # # ==== Example: # block.confirm # => true # # @return [Boolean] if the confirmation request was sent successful def confirm - rpc(:block_confirm, :hash)[:started] == 1 + rpc(:block_confirm, :hash, _access: :started) == 1 end - # This call is for internal diagnostics/debug purposes only. Do not - # rely on this interface being stable and do not use in a production system. - # - # Check if the block appears in the list of recently confirmed blocks by - # online representatives. The full list of blocks can be queried for with {Nanook::Node#confirmation_history}. - # - # This method can work in conjunction with {Nanook::Block#confirm}, - # whereby you can send any block (old or new) out to online representatives to - # confirm. The confirmation process can take up to a couple of minutes. - # - # The method returning +false+ can indicate that the block is still in the process of being - # confirmed and that you should call the method again soon, or that it - # was confirmed earlier than the list available in {Nanook::Node#confirmation_history}, - # or that it was not confirmed. - # - # ==== Example: - # block.confirmed_recently? # => true - # - # @return [Boolean] +true+ if the block has been recently confirmed by - # online representatives. - def confirmed_recently? - @rpc.call(:confirmation_history)[:confirmations].map{|h| h[:hash]}.include?(@block) - end - alias_method :recently_confirmed?, :confirmed_recently? - # Generate work for a block. # # ==== Example: # block.generate_work # => "2bf29ef00786a6bc" # # @param use_peers [Boolean] if set to +true+, then the node will query # its work peers (if it has any, see {Nanook::WorkPeer#list}). # When +false+, the node will only generate work locally (default is +false+) # @return [String] the work id of the work completed. def generate_work(use_peers: false) - rpc(:work_generate, :hash, use_peers: use_peers)[:work] + rpc(:work_generate, :hash, use_peers: use_peers, _access: :work) end - # Returns Array of Hashes containing information about a chain of - # send/receive blocks, starting from this block. - # - # ==== Example: - # - # block.history(limit: 1) - # - # ==== Example response: - # - # [ - # { - # :account=>"nano_3x7cjioqahgs5ppheys6prpqtb4rdknked83chf97bot1unrbdkaux37t31b", - # :amount=>539834279601145558517940224, - # :hash=>"36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E", - # :type=>"send" - # } - # ] - # - # @param limit [Integer] maximum number of send/receive block hashes - # to return in the chain (default is 1000) - def history(limit: 1000) - rpc(:history, :hash, count: limit)[:history] - end - - # Returns the block hash id. - # - # ==== Example: - # - # block.id #=> "FBF8B0E..." - # - # @return [String] the block hash id - def id - @block - end - # Returns a Hash of information about the block. # # ==== Examples: # # block.info # block.info(allow_unchecked: true) # # ==== Example response: # # { - # :id=>"36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E", - # :type=>"send", - # :previous=>"FBF8B0E6623A31AB528EBD839EEAA91CAFD25C12294C46754E45FD017F7939EB", - # :destination=>"nano_3x7cjioqahgs5ppheys6prpqtb4rdknked83chf97bot1unrbdkaux37t31b", - # :balance=>"00000000000000000000000000000000", - # :work=>"44cc24b60705083a", - # :signature=>"42ADFEFE7C3FFF188AE92A202F8A5734DE91779C454613E446EEC93D001D6C953E9FD16730AF32C891791BA8EDAECEB059A213E2FE1EEB7ADF9D5D0815464D06" + # "account": Nanook::Account, + # "amount": 34.2, + # "balance": 2.3 + # "height": 58, + # "local_timestamp": Time, + # "confirmed": true, + # "type": "send", + # "account": Nanook::Account, + # "previous": Nanook::Block, + # "representative": Nanook::Account, + # "link": Nanook::Block, + # "link_as_account": Nanook::Account, + # "signature": "82D41BC16F313E4B2243D14DFFA2FB04679C540C2095FEE7EAE0F2F26880AD56DD48D87A7CC5DD760C5B2D76EE2C205506AA557BF00B60D8DEE312EC7343A501", + # "work": "8a142e07a10996d5" # } # # @param allow_unchecked [Boolean] (default is +false+). If +true+, # information can be returned about blocks that are unchecked (unverified). - def info(allow_unchecked: false) - if allow_unchecked - response = rpc(:unchecked_get, :hash) - unless response.has_key?(:error) - return _parse_info_response(response) - end - # If unchecked not found, continue to checked block + # @raise [Nanook::NanoUnitError] if `unit` is invalid + # @raise [Nanook::NodeRpcError] if block is not found on the node. + def info(allow_unchecked: false, unit: Nanook.default_unit) + validate_unit!(unit) + + # Params for both `unchecked_get` and `block_info` calls + params = { + json_block: true, + _coerce: Hash + } + + begin + response = rpc(:block_info, :hash, params) + response.merge!(confirmed: true) + rescue Nanook::NodeRpcError => e + raise e unless allow_unchecked + + response = rpc(:unchecked_get, :hash, params) + response.merge!(confirmed: false) end - response = rpc(:block, :hash) - _parse_info_response(response) + parse_info_response(response, unit) end # ==== Example: # - # block.is_valid_work?("2bf29ef00786a6bc") # => true + # block.valid_work?("2bf29ef00786a6bc") # => true # # @param work [String] the work id to check is valid # @return [Boolean] signalling if work is valid for the block - def is_valid_work?(work) + def valid_work?(work) response = rpc(:work_validate, :hash, work: work) - !response.empty? && response[:valid] == 1 + response[:valid_all] == 1 || response[:valid_receive] == 1 end # Republish blocks starting at this block up the account chain # back to the nano network. # - # @return [Array<String>] block hashes that were republished + # @return [Array<Nanook::Block>] blocks that were republished # # ==== Example: # - # block.republish - # - # ==== Example response: - # - # ["36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E"] - def republish(destinations:nil, sources:nil) + # block.republish # => [Nanook::Block, ...] + def republish(destinations: nil, sources: nil) if !destinations.nil? && !sources.nil? - raise ArgumentError.new("You must provide either destinations or sources but not both") + raise ArgumentError, 'You must provide either destinations or sources but not both' end - # Add in optional arguments - params = {} + params = { + _access: :blocks, + _coerce: Array + } + params[:destinations] = destinations unless destinations.nil? params[:sources] = sources unless sources.nil? - params[:count] = 1 unless params.empty? + params[:count] = 1 if destinations || sources - rpc(:republish, :hash, params)[:blocks] + rpc(:republish, :hash, params).map do |block| + as_block(block) + end end # ==== Example: # # block.pending? #=> false # # @return [Boolean] signalling if the block is a pending block. def pending? - response = rpc(:pending_exists, :hash) - !response.empty? && response[:exists] == 1 + rpc(:pending_exists, :hash, _access: :exists) == 1 end - # Publish the block to the nano network. - # - # Note, if block has previously been published, use #republish instead. - # - # ==== Examples: - # - # block.publish # => "FBF8B0E..." - # - # @return [String] the block hash, or false. - def publish - rpc(:process, :block)[:hash] || false - end - alias_method :process, :publish - # Returns an Array of block hashes in the account chain ending at # this block. # # See also #chain. # # ==== Example: # - # block.successors + # block.successors # => [Nanook::Block, .. ] # - # ==== Example response: - # - # ["36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E"] - # # @param limit [Integer] maximum number of send/receive block hashes # to return in the chain (default is 1000) # @param offset [Integer] return the account chain block hashes offset # by the specified number of blocks (default is 0) - # @return [Array<String>] block hashes in the account chain ending at this block + # @return [Array<Nanook::Block>] blocks in the account chain ending at this block def successors(limit: 1000, offset: 0) - response = rpc(:successors, :block, count: limit, offset: offset)[:blocks] - Nanook::Util.coerce_empty_string_to_type(response, Array) + params = { + count: limit, + offset: offset, + _access: :blocks, + _coerce: Array + } + + rpc(:successors, :block, params).map do |block| + as_block(block) + end end - def inspect - "#{self.class.name}(id: \"#{id}\", object_id: \"#{"0x00%x" % (object_id << 1)}\")" + # Returns the {Nanook::Account} of the block representative. + # + # ==== Example: + # block.representative # => Nanook::Account + # + # @return [Nanook::Account] representative account of the block. Can be nil. + def representative + memoized_info[:representative] end + # Returns the {Nanook::Account} of the block. + # + # ==== Example: + # block.account # => Nanook::Account + # + # @return [Nanook::Account] the account of the block. Can be nil. + def account + memoized_info[:account] + end + + # Returns the amount of the block. + # + # ==== Example: + # block.amount # => 3.01 + # + # @param unit (see Nanook::Account#balance) + # @raise [Nanook::NanoUnitError] if `unit` is invalid + # @return [Float] + def amount(unit: Nanook.default_unit) + validate_unit!(unit) + + amount = memoized_info[:amount] + return amount unless unit == :nano + + raw_to_NANO(amount) + end + + # Returns the balance of the account at the time the block was created. + # + # ==== Example: + # block.balance # => 3.01 + # + # @param unit (see Nanook::Account#balance) + # @raise [Nanook::NanoUnitError] if `unit` is invalid + # @return [Float] + def balance(unit: Nanook.default_unit) + validate_unit!(unit) + + balance = memoized_info[:balance] + return balance unless unit == :nano + + raw_to_NANO(balance) + end + + # Returns true if block is confirmed. + # + # ==== Example: + # block.confirmed # => true + # + # @return [Boolean] + def confirmed? + memoized_info[:confirmed] + end + alias checked? confirmed? + + # Returns true if block is unconfirmed. + # + # ==== Example: + # block.unconfirmed? # => true + # + # @return [Boolean] + def unconfirmed? + !confirmed? + end + alias unchecked? unconfirmed? + + # Returns true if block exists in the node's ledger. This will return + # false for blocks that exist on the nano ledger but have not yet + # synchronized to the node. + # + # ==== Example: + # + # block.exists? # => false + # block.exists?(allow_unchecked: true) # => true + # + # @param allow_unchecked [Boolean] defaults to +false+ + # @return [Boolean] + def exists?(allow_unchecked: false) + begin + allow_unchecked ? memoized_info : info + rescue Nanook::NodeRpcError + return false + end + + true + end + + # Returns the height of the block. + # + # ==== Example: + # block.height # => 5 + # + # @return [Integer] + def height + memoized_info[:height] + end + + # Returns the block work. + # + # ==== Example: + # block.work # => "8a142e07a10996d5" + # + # @return [String] + def work + memoized_info[:work] + end + + # Returns the block signature. + # + # ==== Example: + # block.signature # => "82D41BC16F313E4B2243D14DFFA2FB04679C540C2095FEE7EAE0F2F26880AD56DD48D87A7CC5DD760C5B2D76EE2C205506AA557BF00B60D8DEE312EC7343A501" + # + # @return [String] + def signature + memoized_info[:signature] + end + + # Returns the timestamp of when the node saw the block. + # + # ==== Example: + # block.timestamp # => 2018-05-30 16:41:48 UTC + # + # @return [Time] Time in UTC of when the node saw the block. Can be nil. + def timestamp + memoized_info[:local_timestamp] + end + + # Returns the {Nanook::Block} of the previous block in the chain. + # + # ==== Example: + # block.previous # => Nanook::Block + # + # @return [Nanook::Block] previous block in the chain. Can be nil. + def previous + memoized_info[:previous] + end + + # Returns the type of the block. One of "open", "send", "receive", "change", "epoch". + # + # ==== Example: + # block.type # => "open" + # + # @return [String] type of block. Returns nil for unconfirmed blocks. + def type + memoized_info[:type] + end + + # Returns true if block is type "send". + # + # ==== Example: + # block.send? # => true + # + # @return [Boolean] + def send? + type == 'send' + end + + # Returns true if block is type "open". + # + # ==== Example: + # block.open? # => true + # + # @return [Boolean] + def open? + type == 'open' + end + + # Returns true if block is type "receive". + # + # ==== Example: + # block.receive? # => true + # + # @return [Boolean] + def receive? + type == 'receive' + end + + # Returns true if block is type "change" (change of representative). + # + # ==== Example: + # block.change? # => true + # + # @return [Boolean] + def change? + type == 'change' + end + + # Returns true if block is type "epoch". + # + # ==== Example: + # block.epoch? # => true + # + # @return [Boolean] + def epoch? + type == 'epoch' + end + + # @return [String] + def to_s + "#{self.class.name}(id: \"#{short_id}\")" + end + alias inspect to_s + private # Some RPC calls expect the param that represents the block to be named # "hash", and others "block". # The param_name argument allows us to specify which it should be for this call. - def rpc(action, param_name, params={}) - p = @block.nil? ? {} : { param_name.to_sym => @block } + def rpc(action, param_name, params = {}) + p = { param_name.to_sym => @block } @rpc.call(action, p.merge(params)) end - def block_required! - if @block.nil? - raise ArgumentError.new("Block must be present") - end + # Memoize the `#info` response as we can refer to it for other methods (`type`, `#open?`, `#send?` etc.) + def memoized_info + @memoized_info ||= info(allow_unchecked: true, unit: :raw) end - def _parse_info_response(response) - # The contents is a stringified JSON - if response[:contents] - r = JSON.parse(response[:contents]).to_symbolized_hash - return r.merge(id: id) + def parse_info_response(response, unit) + response.merge!(id: id) + contents = response.delete(:contents) + response.merge!(contents) if contents + + response.delete(:block_account) # duplicate of contents.account + response[:type] = response.delete(:subtype) # rename key + response[:last_modified_at] = response.delete(:modified_timestamp) # rename key + + response[:account] = as_account(response[:account]) if response[:account] + response[:representative] = as_account(response[:representative]) if response[:representative] + response[:previous] = as_block(response[:previous]) if response[:previous] + response[:link] = as_block(response[:link]) if response[:link] + response[:link_as_account] = as_account(response[:link_as_account]) if response[:link_as_account] + response[:local_timestamp] = as_time(response[:local_timestamp]) + response[:last_modified_at] = as_time(response[:last_modified_at]) + + if unit == :nano + response[:amount] = raw_to_NANO(response[:amount]) + response[:balance] = raw_to_NANO(response[:balance]) end - response + response.compact end - end end