# frozen_string_literal: true
require_relative 'util'
class Nanook
# The Nanook::Block class contains methods to discover
# publicly-available information about blocks on the nano network.
#
# A block is represented by a unique id like this:
#
# "FBF8B0E6623A31AB528EBD839EEAA91CAFD25C12294C46754E45FD017F7939EB"
#
# Initialize this class through the convenient Nanook#block method:
#
# nanook = Nanook.new
# block = nanook.block("FBF8B0E...")
#
# 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.to_s
end
# Returns the block hash id.
#
# ==== Example:
#
# 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
#
# @return [Boolean] signalling if the action was successful
def cancel_work
rpc(:work_cancel, :hash).empty?
end
# Returns a consecutive list of block hashes in the account chain
# starting at block back to count (direction from frontier back to
# open block, from newer blocks to older). Will list all blocks back
# to the open block of this chain when count is set to "-1".
# The requested block hash is included in the answer.
#
# See also #successors.
#
# ==== Example:
#
# block.chain(limit: 2)
#
# ==== Example reponse:
#
# [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)
params = {
count: limit,
offset: offset,
_access: :blocks,
_coerce: Array
}
rpc(:chain, :block, params).map do |block|
as_block(block)
end
end
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.
#
# ==== Example:
# block.confirm # => true
#
# @return [Boolean] if the confirmation request was sent successful
def confirm
rpc(:block_confirm, :hash, _access: :started) == 1
end
# 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, _access: :work)
end
# Returns a Hash of information about the block.
#
# ==== Examples:
#
# block.info
# block.info(allow_unchecked: true)
#
# ==== Example response:
#
# {
# "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).
# @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
parse_info_response(response, unit)
end
# Returns true if work is valid for the block.
#
# ==== Example:
#
# 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 valid_work?(work)
response = rpc(:work_validate, :hash, work: work)
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] blocks that were republished
#
# ==== Example:
#
# block.republish # => [Nanook::Block, ...]
def republish(destinations: nil, sources: nil)
if !destinations.nil? && !sources.nil?
raise ArgumentError, 'You must provide either destinations or sources but not both'
end
params = {
_access: :blocks,
_coerce: Array
}
params[:destinations] = destinations unless destinations.nil?
params[:sources] = sources unless sources.nil?
params[:count] = 1 if destinations || sources
rpc(:republish, :hash, params).map do |block|
as_block(block)
end
end
# Returns true if block is a pending block.
#
# ==== Example:
#
# block.pending? #=> false
#
# @return [Boolean] signalling if the block is a pending block.
def pending?
rpc(:pending_exists, :hash, _access: :exists) == 1
end
# Returns an Array of block hashes in the account chain ending at
# this block.
#
# See also #chain.
#
# ==== Example:
#
# block.successors # => [Nanook::Block, .. ]
#
# @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] blocks in the account chain ending at this block
def successors(limit: 1000, offset: 0)
params = {
count: limit,
offset: offset,
_access: :blocks,
_coerce: Array
}
rpc(:successors, :block, params).map do |block|
as_block(block)
end
end
# 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 = { param_name.to_sym => @block }
@rpc.call(action, p.merge(params))
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, unit)
response.merge!(id: id)
contents = response.delete(:contents)
response.merge!(contents) if contents
response.delete(:block_account) # duplicate of contents.account
response[:last_modified_at] = response.delete(:modified_timestamp) # rename key
# `type` can be "open", "send", "receive", "change", "epoch" or "state".
# blocks with `type` == "state" may have a `subtype` that gives more information
# about the block ("send", "receive", "change", "epoch"), in which case replace
# the `type` with this value.
if response[:type] == 'state' && (subtype = response.delete(:subtype))
response[:type] = subtype
end
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.compact
end
end
end