class Nanook
# The Nanook::Wallet class lets you manage your nano wallets,
# as well as some account-specific things like making and receiving payments.
#
# === Wallet seeds vs ids
#
# Your wallets each have an id as well as a seed. Both are 32-byte uppercase hex
# strings that look like this:
#
# 000D1BAEC8EC208142C99059B393051BAC8380F9B5A2E6B2489A277D81789F3F
#
# This class uses wallet _ids_ to identify your wallet. A wallet id only
# exists locally on the nano node that it was created on. The person
# who knows this id can only perform all read and write actions against
# the wallet and all accounts inside the wallet from the same nano node
# that it was created on. This makes wallet ids fairly safe to use as a
# person needs to know your wallet id as well as have access to run
# RPC commands against your nano node to be able to control your accounts.
#
# A _seed_ on the other hand can be used to link any wallet to another
# wallet's accounts, from anywhere in the nano network. This happens
# by setting a wallet's seed to be the same as a previous wallet's seed.
# When a wallet has the same seed as another wallet, any accounts
# created in the second wallet will be the same accounts as those that were
# created in the previous wallet, and the new wallet's owner will
# also gain ownership of the previous wallet's accounts. Note, that the
# two wallets will have different ids, but the same seed.
#
# Nanook is based on the Nano RPC, which uses wallet ids and not seeds.
# The RPC and therefore Nanook cannot tell you what a wallet's seed is,
# only its id. Knowing a wallet's seed is very useful for if you ever
# want to restore the wallet anywhere else on the nano network besides
# the node you originally created it on. The nano command line interface
# (CLI) is the only method for discovering a wallet's seed. See the
# {https://github.com/nanocurrency/raiblocks/wiki/Command-line-interface
# --wallet_decrypt_unsafe CLI command}.
#
# === Initializing
#
# Initialize this class through the convenient {Nanook#wallet} method:
#
# nanook = Nanook.new
# wallet = nanook.wallet(wallet_id)
#
# Or compose the longhand way like this:
#
# rpc_conn = Nanook::Rpc.new
# wallet = Nanook::Wallet.new(rpc_conn, wallet_id)
class Wallet
def initialize(rpc, wallet)
@rpc = rpc
@wallet = wallet
end
# Returns the given account in the wallet as a {Nanook::WalletAccount} instance
# to let you start working with it.
#
# Call with no +account+ argument if you wish to create a new account
# in the wallet, like this:
#
# wallet.account.create # => Nanook::WalletAccount
#
# See {Nanook::WalletAccount} for all the methods you can call on the
# account object returned.
#
# ==== Examples:
#
# wallet.account("xrb_...") # => Nanook::WalletAccount
# wallet.account.create # => Nanook::WalletAccount
#
# @param [String] account optional String of an account (starting with
# "xrb...") to start working with. Must be an account within
# the wallet. When no account is given, the instance returned only
# allows you to call +create+ on it, to create a new account.
# @raise [ArgumentError] if the wallet does no contain the account
# @return [Nanook::WalletAccount]
def account(account=nil)
Nanook::WalletAccount.new(@rpc, @wallet, account)
end
# Array of {Nanook::WalletAccount} instances of accounts in the wallet.
#
# See {Nanook::WalletAccount} for all the methods you can call on the
# account objects returned.
#
# ==== Example:
#
# wallet.accounts # => [Nanook::WalletAccount, Nanook::WalletAccount...]
#
# @return [Array] all accounts in the wallet
def accounts
wallet_required!
response = rpc(:account_list)[:accounts]
Nanook::Util.coerce_empty_string_to_type(response, Array).map do |account|
Nanook::WalletAccount.new(@rpc, @wallet, account)
end
end
# Balance of all accounts in the wallet, optionally breaking the balances down by account.
#
# ==== Examples:
# wallet.balance
#
# Example response:
#
# {
# "balance"=>5,
# "pending"=>0.001
# }
#
# Asking for the balances to be returned in raw instead of NANO.
#
# wallet.balance(unit: :raw)
#
# Example response:
#
# {
# "balance"=>5000000000000000000000000000000,
# "pending"=>1000000000000000000000000000
# }
#
# Asking for totals to be broken down by account:
#
# wallet.balance(account_break_down: true)
#
# Example response:
#
# {
# "xrb_3e3j5tkog48pnny9dmfzj1r16pg8t1e76dz5tmac6iq689wyjfpi00000000"=>{
# "balance"=>2.5,
# "pending"=>1
# },
# "xrb_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx"=>{
# "balance"=>51.4,
# "pending"=>0
# },
# }
#
# @param [Boolean] account_break_down (default is +false+). When +true+
# the response will contain balances per account.
# @param unit (see Nanook::Account#balance)
#
# @return [Hash{Symbol=>Integer|Float|Hash}]
def balance(account_break_down: false, unit: Nanook.default_unit)
wallet_required!
unless Nanook::UNITS.include?(unit)
raise ArgumentError.new("Unsupported unit: #{unit}")
end
if account_break_down
return Nanook::Util.coerce_empty_string_to_type(rpc(:wallet_balances)[:balances], Hash).tap do |r|
if unit == :nano
r.each do |account, balances|
r[account][:balance] = Nanook::Util.raw_to_NANO(r[account][:balance])
r[account][:pending] = Nanook::Util.raw_to_NANO(r[account][:pending])
end
end
end
end
rpc(:wallet_balance_total).tap do |r|
if unit == :nano
r[:balance] = Nanook::Util.raw_to_NANO(r[:balance])
r[:pending] = Nanook::Util.raw_to_NANO(r[:pending])
end
end
end
# Changes a wallet's seed.
#
# ==== Example:
#
# wallet.change_seed("000D1BA...") # => true
#
# @param seed [String] the seed to change to.
# @return [Boolean] indicating whether the change was successful.
def change_seed(seed)
wallet_required!
rpc(:wallet_change_seed, seed: seed).has_key?(:success)
end
# Creates a new wallet.
#
# The wallet will be created only on this node. It's important that
# if you intend to add funds to accounts in this wallet that you
# backup the wallet *seed* in order to restore the wallet in future.
# The nano command line interface (CLI) is the only method for
# backing up a wallet's seed. See the
# {https://github.com/nanocurrency/raiblocks/wiki/Command-line-interface
# --wallet_decrypt_unsafe CLI command}.
#
# ==== Example:
# Nanook.new.wallet.create # => Nanook::Wallet
#
# @return [Nanook::Wallet]
def create
@wallet = rpc(:wallet_create)[:wallet]
self
end
# Destroys the wallet.
#
# ==== Example:
#
# wallet.destroy # => true
#
# @return [Boolean] indicating success of the action
def destroy
wallet_required!
rpc(:wallet_destroy)
true
end
# Generates a String containing a JSON representation of your wallet.
#
# ==== Example:
#
# wallet.export # => "{\n \"0000000000000000000000000000000000000000000000000000000000000000\": \"0000000000000000000000000000000000000000000000000000000000000003\",\n \"0000000000000000000000000000000000000000000000000000000000000001\": \"C3A176FC3B90113277BFC91F55128FC9A1F1B6166A73E7446927CFFCA4C2C9D9\",\n \"0000000000000000000000000000000000000000000000000000000000000002\": \"3E58EC805B99C52B4715598BD332C234A1FBF1780577137E18F53B9B7F85F04B\",\n \"0000000000000000000000000000000000000000000000000000000000000003\": \"5FF8021122F3DEE0E4EC4241D35A3F41DEF63CCF6ADA66AF235DE857718498CD\",\n \"0000000000000000000000000000000000000000000000000000000000000004\": \"A30E0A32ED41C8607AA9212843392E853FCBCB4E7CB194E35C94F07F91DE59EF\",\n \"0000000000000000000000000000000000000000000000000000000000000005\": \"E707002E84143AA5F030A6DB8DD0C0480F2FFA75AB1FFD657EC22B5AA8E395D5\",\n \"0000000000000000000000000000000000000000000000000000000000000006\": \"0000000000000000000000000000000000000000000000000000000000000001\",\n \"8646C0423160DEAEAA64034F9C6858F7A5C8A329E73E825A5B16814F6CCAFFE3\": \"0000000000000000000000000000000000000000000000000000000100000000\"\n}\n"
def export
wallet_required!
rpc(:wallet_export)[:json]
end
# Will return +true+ if the account exists in the wallet.
#
# ==== Example:
# wallet.contains?("xrb_...") # => true
#
# @param account [String] id (will start with "xrb_...")
# @return [Boolean] indicating if the wallet contains the given account
def contains?(account)
wallet_required!
response = rpc(:wallet_contains, account: account)
!response.empty? && response[:exists] == 1
end
# @return [String] the wallet id
def id
@wallet
end
# @return [String]
def inspect
"#{self.class.name}(id: \"#{id}\", object_id: \"#{"0x00%x" % (object_id << 1)}\")"
end
# Makes a payment from an account in your wallet to another account
# on the nano network.
#
# Note, there may be a delay in receiving a response due to Proof of
# Work being done. From the {Nano RPC}[https://github.com/nanocurrency/raiblocks/wiki/RPC-protocol#account-create]:
#
# 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:
#
# wallet.pay(from: "xrb_...", to: "xrb_...", amount: 1.1, id: "myUniqueId123") # => "9AE2311..."
# wallet.pay(from: "xrb_...", to: "xrb_...", amount: 54000000000000, unit: :raw, id: "myUniqueId123") # => "9AE2311..."
#
# @param from [String] account id of an account in your wallet
# @param to (see Nanook::WalletAccount#pay)
# @param amount (see Nanook::WalletAccount#pay)
# @param unit (see Nanook::Account#balance)
# @params id (see Nanook::WalletAccount#pay)
# @return (see Nanook::WalletAccount#pay)
# @raise [Nanook::Error] if unsuccessful
def pay(from:, to:, amount:, unit: Nanook.default_unit, id:)
wallet_required!
validate_wallet_contains_account!(from)
account(from).pay(to: to, amount: amount, unit: unit, id: id)
end
# Information about pending blocks (payments) that are waiting
# to be received by accounts in this wallet.
#
# See also the {#receive} method of this class for how to receive a pending payment.
#
# @param limit [Integer] number of accounts with pending payments to return (default is 1000)
# @param detailed [Boolean]return a more complex Hash of pending block information (default is +false+)
# @param unit (see Nanook::Account#balance)
#
# ==== Examples:
#
# wallet.pending
#
# Example response:
#
# {
# :xrb_1111111111111111111111111111111111111111111111111117353trpda=>[
# "142A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D",
# "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2"
# ],
# :xrb_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3=>[
# "4C1FEEF0BEA7F50BE35489A1233FE002B212DEA554B55B1B470D78BD8F210C74"
# ]
# }
#
# Asking for more information:
#
# wallet.pending(detailed: true)
#
# Example response:
#
# {
# :xrb_1111111111111111111111111111111111111111111111111117353trpda=>[
# {
# :amount=>6.0,
# :source=>"xrb_3dcfozsmekr1tr9skf1oa5wbgmxt81qepfdnt7zicq5x3hk65fg4fqj58mbr",
# :block=>:"142A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D"
# },
# {
# :amount=>12.0,
# :source=>"xrb_3dcfozsmekr1tr9skf1oa5wbgmxt81qepfdnt7zicq5x3hk65fg4fqj58mbr",
# :block=>:"242A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D"
# }
# ],
# :xrb_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3=>[
# {
# :amount=>106.370018,
# :source=>"xrb_13ezf4od79h1tgj9aiu4djzcmmguendtjfuhwfukhuucboua8cpoihmh8byo",
# :block=>:"4C1FEEF0BEA7F50BE35489A1233FE002B212DEA554B55B1B470D78BD8F210C74"
# }
# ]
# }
def pending(limit:1000, detailed:false, unit:Nanook.default_unit)
wallet_required!
unless Nanook::UNITS.include?(unit)
raise ArgumentError.new("Unsupported unit: #{unit}")
end
params = { count: limit }
params[:source] = true if detailed
response = rpc(:wallet_pending, params)[:blocks]
response = Nanook::Util.coerce_empty_string_to_type(response, Hash)
return response unless detailed
# Map the RPC response, which is:
# account=>block=>[amount|source] into
# account=>[block|amount|source]
x = response.map do |account, data|
new_data = data.map do |block, amount_and_source|
d = amount_and_source.merge(block: block.to_s)
if unit == :nano
d[:amount] = Nanook::Util.raw_to_NANO(d[:amount])
end
d
end
[account, new_data]
end
Hash[x].to_symbolized_hash
end
# Receives a pending payment into an account in the wallet.
#
# When called with no +block+ argument, the latest pending payment
# for the account will be received.
#
# Returns a receive block hash 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 has in as an argument.
#
# ==== Examples:
#
# wallet.receive(into: "xrb...") # => "9AE2311..."
# wallet.receive("718CC21...", into: "xrb...") # => "9AE2311..."
#
# @param block (see Nanook::WalletAccount#receive)
# @param into [String] account id of account in your wallet to receive the
# payment into
# @return (see Nanook::WalletAccount#receive)
def receive(block=nil, into:)
wallet_required!
validate_wallet_contains_account!(into)
account(into).receive(block)
end
# The default representative account id for the wallet. This is the
# representative that all new accounts created in this wallet will have.
#
# Changing the default representative for a wallet does not change
# the representatives for any accounts that have been created.
#
# ==== Example:
#
# wallet.default_representative # => "xrb_3pc..."
#
# @return [String] Representative account of the account
def default_representative
rpc(:wallet_representative)[:representative]
end
alias_method :representative, :default_representative
# Sets the default representative for the wallet. A wallet's default
# representative is the representative all new accounts created in
# the wallet will have. Changing the default representative for a
# wallet does not change the representatives for existing accounts
# in the wallet.
#
# ==== Example:
#
# wallet.change_default_representative("xrb_...") # => "xrb_..."
#
# @param [String] representative the id of the representative account
# to set as this account's representative
# @return [String] the representative account id
# @raise [ArgumentError] if the representative account does not exist
# @raise [Nanook::Error] if setting the representative fails
def change_default_representative(representative)
unless Nanook::Account.new(@rpc, representative).exists?
raise ArgumentError.new("Representative account does not exist: #{representative}")
end
if rpc(:wallet_representative_set, representative: representative)[:set] == 1
representative
else
raise Nanook::Error.new("Setting the representative failed")
end
end
alias_method :change_representative, :change_default_representative
# Restores a previously created wallet by its seed.
# A new wallet will be created on your node (with a new wallet id)
# and will have its seed set to the given seed.
#
# ==== Example:
#
# Nanook.new.wallet.restore(seed) # => Nanook::Wallet
#
# @param seed [String] the wallet seed to restore.
# @param accounts [Integer] optionally restore the given number of accounts for the wallet.
#
# @return [Nanook::Wallet] a new wallet
# @raise [Nanook::Error] if unsuccessful
def restore(seed, accounts:0)
create
unless change_seed(seed)
raise Nanook::Error.new("Unable to set seed for wallet")
end
if accounts > 0
account.create(accounts)
end
self
end
# Information about this wallet and all of its accounts.
#
# ==== Examples:
#
# wallet.info
#
# Example response:
#
# {
# id: "2C3C570EA8898443C0FD04A1C385A3E3A8C985AD792635FCDCEBB30ADF6A0570",
# accounts: [
# {
# id: "xrb_11119gbh8hb4hj1duf7fdtfyf5s75okzxdgupgpgm1bj78ex3kgy7frt3s9n"
# frontier: "E71AF3E9DD86BBD8B4620EFA63E065B34D358CFC091ACB4E103B965F95783321",
# open_block: "643B77F1ECEFBDBE1CC909872964C1DBBE23A6149BD3CEF2B50B76044659B60F",
# representative_block: "643B77F1ECEFBDBE1CC909872964C1DBBE23A6149BD3CEF2B50B76044659B60F",
# balance: 1.45,
# modified_timestamp: 1511476234,
# block_count: 2
# },
# { ... }
# ]
# }
#
# @param unit (see #balance)
# @return [Hash{Symbol=>String|ArrayString|Integer|Float}>}] information about the wallet.
# See {Nanook::Account#info} for details of what is returned for each account.
def info(unit: Nanook.default_unit)
unless Nanook::UNITS.include?(unit)
raise ArgumentError.new("Unsupported unit: #{unit}")
end
wallet_required!
accounts = rpc(:wallet_ledger)[:accounts].map do |account_id, payload|
payload[:id] = account_id
if unit == :nano
payload[:balance] = Nanook::Util.raw_to_NANO(payload[:balance])
end
payload
end
{
id: @wallet,
accounts: accounts
}.to_symbolized_hash
end
# Returns +true+ if the wallet is locked.
#
# ==== Example:
#
# wallet.locked? #=> false
#
# @return [Boolean] indicates if the wallet is locked
def locked?
wallet_required!
response = rpc(:wallet_locked)
!response.empty? && response[:locked] != 0
end
# Unlocks a previously locked wallet.
#
# ==== Example:
#
# wallet.unlock("new_pass") #=> true
#
# @return [Boolean] indicates if the unlocking action was successful
def unlock(password)
wallet_required!
rpc(:password_enter, password: password)[:valid] == 1
end
# Changes the password for a wallet.
#
# ==== Example:
#
# wallet.change_password("new_pass") #=> true
# @return [Boolean] indicates if the action was successful
def change_password(password)
wallet_required!
rpc(:password_change, password: password)[:changed] == 1
end
private
def rpc(action, params={})
p = @wallet.nil? ? {} : { wallet: @wallet }
@rpc.call(action, p.merge(params))
end
def wallet_required!
if @wallet.nil?
raise ArgumentError.new("Wallet must be present")
end
end
def validate_wallet_contains_account!(account)
@known_valid_accounts ||= []
return if @known_valid_accounts.include?(account)
if contains?(account)
@known_valid_accounts << account
else
raise ArgumentError.new("Account does not exist in wallet. Account: #{account}, wallet: #{@wallet}")
end
end
end
end