# frozen_string_literal: true
require_relative 'util'
class Nanook
# The Nanook::WalletAccount class lets you manage your nano accounts
# that are on your node, including paying and receiving payment.
#
# === Initializing
#
# Initialize this class through an instance of {Nanook::Wallet} like this:
#
# account = Nanook.new.wallet(wallet_id).account(account_id)
#
# Or compose the longhand way like this:
#
# rpc_conn = Nanook::Rpc.new
# account = Nanook::WalletAccount.new(rpc_conn, wallet_id, account_id)
class WalletAccount
include Nanook::Util
extend Forwardable
# @!method ==
# (see Nanook::Account#==)
# @!method balance(allow_unconfirmed: false, unit: Nanook.default_unit)
# (see Nanook::Account#balance)
# @!method block_count
# (see Nanook::Account#block_count)
# @!method blocks(limit: 1000, sort: :desc)
# (see Nanook::Account#blocks)
# @!method delegators(unit: Nanook.default_unit)
# (see Nanook::Account#delegators)
# @!method delegators_count
# (see Nanook::Account#delegators_count)
# @!method eql?
# (see Nanook::Account#eql?)
# @!method exists?
# (see Nanook::Account#exists?)
# @!method hash
# (see Nanook::Account#hash)
# @!method history(limit: 1000, unit: Nanook.default_unit, sort: :desc)
# (see Nanook::Account#history)
# @!method id
# (see Nanook::Account#id)
# @!method info(allow_unconfirmed: false, detailed: false, unit: Nanook.default_unit)
# (see Nanook::Account#info)
# @!method last_modified_at
# (see Nanook::Account#last_modified_at)
# @!method ledger(limit: 1, modified_since: nil, unit: Nanook.default_unit, sort: :desc)
# (see Nanook::Account#ledger)
# @!method open_block
# (see Nanook::Account#open_block)
# @!method pending(limit: 1000, detailed: false, allow_unconfirmed: false, unit: Nanook.default_unit, sorted: false)
# (see Nanook::Account#pending)
# @!method public_key
# (see Nanook::Account#public_key)
# @!method representative
# (see Nanook::Account#representative)
# @!method weight
# (see Nanook::Account#weight)
def_delegators :@nanook_account_instance,
:==, :balance, :block_count, :blocks, :delegators, :delegators_count,
:eql?, :exists?, :hash, :history, :id, :info, :last_modified_at, :ledger,
:open_block, :pending, :public_key, :representative, :weight
alias open? exists?
def initialize(rpc, wallet, account = nil)
@rpc = rpc
@wallet = wallet.to_s
@account = account.to_s if account
# Initialize an instance to delegate the RPC commands that do not
# need `enable_control` enabled (the read-only RPC commands).
@nanook_account_instance = nil
return if @account.nil?
# Wallet must contain the account
unless Nanook::Wallet.new(@rpc, @wallet).contains?(@account)
raise ArgumentError, "Account does not exist in wallet. Account: #{@account}, wallet: #{@wallet}"
end
@nanook_account_instance = as_account(@account)
end
# @param other [Nanook::WalletAccount] wallet account to compare
# @return [Boolean] true if accounts are equal
def ==(other)
other.class == self.class &&
other.id == @account
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
[@wallet, @account].join('+').hash
end
# Creates a new account, or multiple new accounts, in this wallet.
#
# ==== Examples:
#
# wallet.create # => Nanook::WalletAccount
# wallet.create(2) # => [Nanook::WalletAccount, Nanook::WalletAccount]
#
# @param n_accounts [Integer] number of accounts to create
#
# @return [Nanook::WalletAccount] returns a single {Nanook::WalletAccount}
# if invoked with no argument
# @return [Array] returns an Array of {Nanook::WalletAccount}
# if method was called with argument +n+ > 1
# @raise [ArgumentError] if +n+ is less than 1
def create(n_accounts = 1)
skip_account_required!
raise ArgumentError, 'number of accounts must be greater than 0' if n_accounts < 1
if n_accounts == 1
as_wallet_account(rpc(:account_create, _access: :account))
else
rpc(:accounts_create, count: n_accounts, _access: :accounts, _coerce: Array).map do |account|
as_wallet_account(account)
end
end
end
# Unlinks the account from the wallet.
#
# ==== Example:
#
# account.destroy # => true
#
# @return [Boolean] +true+ if action was successful, otherwise +false+
def destroy
rpc(:account_remove, _access: :removed) == 1
end
# @return [String]
def to_s
"#{self.class.name}(id: \"#{short_id}\")"
end
alias inspect to_s
# Makes a payment from this account to another account
# on the nano network. Returns a send block hash
# if successful, or a {Nanook::NodeRpcError} if unsuccessful.
#
# Note, there may be a delay in receiving a response due to Proof
# of Work being done. From the {Nano RPC}[https://docs.nano.org/commands/rpc-protocol/#send]:
#
# Proof of Work is precomputed for one transaction in the background. If it has been a while since your last transaction it will send instantly, the next one will need to wait for Proof of Work to be generated.
#
# ==== Examples:
#
# account.pay(to: "nano_...", amount: 1.1, id: "myUniqueId123") # => "9AE2311..."
# account.pay(to: "nano_...", amount: 54000000000000, id: "myUniqueId123", unit: :raw) # => "9AE2311..."
#
# @param to [String] account id of the recipient of your payment
# @param amount [Integer|Float]
# @param unit (see Nanook::Account#balance)
# @param id [String] must be unique per payment. It serves an important
# purpose; it allows you to make the same call multiple times with
# the same +id+ and be reassured that you will only ever send this
# nano payment once
# @return [Nanook::Block] the send block for the payment
# @raise [Nanook::NodeRpcError] if unsuccessful
# @raise [Nanook::NanoUnitError] if `unit` is invalid
def pay(to:, amount:, id:, unit: Nanook.default_unit)
validate_unit!(unit)
# Check that to account is a valid address
valid = @rpc.call(:validate_account_number, account: to, _access: :valid) == 1
raise ArgumentError, "Account address is invalid: #{to}" unless valid
# Determine amount in raw
raw = if unit.to_sym.eql?(:nano)
NANO_to_raw(amount)
else
amount
end
# account is called source, so don't use the normal rpc method
params = {
wallet: @wallet,
source: @account,
destination: to,
amount: raw,
id: id,
_access: :block
}
as_block(@rpc.call(:send, params))
end
# Receives a pending payment for this account.
#
# When called with no +block+ argument, the latest pending payment
# for the account will be received.
#
# Returns a receive block id
# if a receive was successful, or +false+ if there were no pending
# payments to receive.
#
# You can receive a specific pending block if you know it by
# passing the block in as an argument.
#
# ==== Examples:
#
# account.receive # => Nanook::Block
# account.receive("718CC21...") # => Nanook::Block
#
# @param block [String] optional block id of pending payment. If
# not provided, the latest pending payment will be received
# @return [Nanook::Block] the receive block
# @return [false] if there was no block to receive
def receive(block = nil)
return receive_without_block if block.nil?
receive_with_block(block)
end
# Sets the representative for the account.
#
# A representative is an account that will vote on your account's
# behalf on the nano network if your account is offline and there is
# a fork of the network that requires voting on.
#
# Returns the change block that was
# broadcast to the nano network. The block contains the information
# about the representative change for your account.
#
# Also see {Nanook::Wallet#change_default_representative} for how to set a default
# representative for all new accounts created in a wallet.
#
# ==== Example:
#
# account.change_representative("nano_...") # => Nanook::Block
#
# @param representative [String] the id of the representative account
# to set as this account's representative
# @return [Nanook::Block] change block created
# @raise [Nanook::Error] if setting the representative account fails
def change_representative(representative)
unless as_account(representative).exists?
raise Nanook::Error, "Representative account does not exist: #{representative}"
end
params = {
representative: representative,
_access: :block
}
as_block(rpc(:account_representative_set, params))
end
# Returns the work for the account.
#
# ==== Example:
#
# account.work # => "432e5cf728c90f4f"
#
# @return [String] work
def work
rpc(:work_get, _access: :work)
end
# Set work for account.
#
# ==== Example:
#
# account.set_work("432e5cf728c90f4f") # => true
#
# @return [Boolean] true if action was successful
def set_work(work)
rpc(:work_set, work: work).key?(:success)
end
private
def receive_without_block
# Discover the first pending block
block = @rpc.call(:pending, { account: @account, count: 1, _access: :blocks, _coerce: Array }).first
return false unless block
# Then call receive_with_block as normal
receive_with_block(block)
end
# Returns block if successful, otherwise false
def receive_with_block(block)
response = rpc(:receive, block: block, _access: :block)
response ? as_block(response) : false
end
def rpc(action, params = {})
check_account_required!
p = { wallet: @wallet, account: @account }.compact
@rpc.call(action, p.merge(params)).tap { reset_skip_account_required! }
end
def skip_account_required!
@skip_account_required_check = true
end
def reset_skip_account_required!
@skip_account_required_check = false
end
def check_account_required!
return if @account || @skip_account_required_check
raise ArgumentError, 'Account must be present'
end
end
end