class Nanook
# The Nanook::Wallet class lets you manage your nano wallets,
# as well as some account-specific things like making and receiving payments.
#
# Your wallets each have a seed, which is a 32-byte uppercase hex
# string that looks like this:
#
# 000D1BAEC8EC208142C99059B393051BAC8380F9B5A2E6B2489A277D81789F3F
#
# You can think of this string as your API key to the nano network.
# The person who knows it can do all read and write actions against
# the wallet and all accounts inside the wallet from anywhere on the
# nano network, not just on the node you created the wallet on.
# Make sure this key is always secret and safe. Do not commit
# your seed into source control.
#
# === Initializing
#
# Initialize this class through the convenient Nanook#wallet method:
#
# nanook = Nanook.new
# wallet = nanook.wallet(wallet_seed)
#
# Or compose the longhand way like this:
#
# rpc_conn = Nanook::Rpc.new
# wallet = Nanook::Wallet.new(rpc_conn, wallet_seed)
class Wallet
def initialize(rpc, wallet)
@rpc = rpc
@wallet = wallet
end
# A convenient method that returns an account in your wallet, allowing
# you to perform all the actions in Nanook::WalletAccount on it.
#
# wallet.account("xrb_...") #=> Nanook::WalletAccount instance
#
# See Nanook::WalletAccount.
#
# Will throw an ArgumentError if the wallet does not contain the account.
#
# ==== Arguments
# [+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. Otherwise, you
# must pass an account string for all other methods.
#
# ==== Examples
#
# wallet.account.create # Creates an account in the wallet and returns a Nanook::WalletAccount
# wallet.account(account_id) # Returns a Nanook::WalletAccount for the account
def account(account=nil)
Nanook::WalletAccount.new(@rpc, @wallet, account)
end
# Returns an Array with Strings of all account ids in the wallet.
#
# ==== Example response
#
# [
# "xrb_3e3j5tkog48pnny9dmfzj1r16pg8t1e76dz5tmac6iq689wyjfpi00000000",
# "xrb_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx"
# ]
def accounts
wallet_required!
response = rpc(:account_list)[:accounts]
Nanook::Util.coerce_empty_string_to_type(response, Array)
end
# Returns a Hash containing the balance of all accounts in the
# wallet, optionally breaking the balances down by account.
#
# ==== Arguments
#
# [+account_break_down:+] Boolean (default is +false+). When +true+
# the response will contain balances per
# account.
# [+unit:+] Symbol (default is +:nano+) Represents the unit that
# the balances will be returned in.
# Must be either +:nano+ or +:raw+. (Note: this method
# interprets +:nano+ as NANO, which is technically Mnano
# See {What are Nano's Units}[https://nano.org/en/faq#what-are-nano-units-])
#
# ==== 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"=>2500000000000000,
"pending"=>10000000000000
},
"xrb_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx"=>{
"balance"=>2500000000000000,
"pending"=>0
},
}
def balance(account_break_down: false, unit: Nanook::WalletAccount::DEFAULT_UNIT)
wallet_required!
unless Nanook::WalletAccount::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][:balapendingnce])
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
# Creates a new wallet.
#
# Nanook.new.wallet.create
#
# ==== Very important
#
# Please read this. The response of this method is a wallet seed. A seed is
# a 32-byte uppercase hex string. You can think of this string as your
# API key to the nano network. The person who knows it can do all read and write
# actions against the wallet and all accounts inside the wallet from
# anywhere on the nano network, not just on the node you created the
# wallet on.
#
# If you intend for your wallet to contain funds, then make sure that
# you consider the seed that is returned as the key to your funds
# and store it somewhere secret and safe. Only transmit
# the seed over secure (SSH or SSL) networks and do not store it where
# it is able to be easily comprised by a hacker, which includes your
# personal computer.
#
# ==== Example response:
#
# "CC2C9846A44DB6F0363F647D12B957794AD937F59498D4E35C172C81E2888650"
def create
rpc(:wallet_create)[:wallet]
end
# Destroy the wallet. Returns a boolean indicating whether the action
# was successful or not.
#
# ==== Example Response
# true
def destroy
wallet_required!
rpc(:wallet_destroy)
true
end
# Generates a String containing a JSON representation of your wallet.
#
# ==== Example response
#
# "{\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
# Returns boolean indicating if the wallet contains an account.
#
# ==== Arguments
#
# [+account+] String account id (will start with "xrb_...")
#
# ==== Example response
# true
def contains?(account)
wallet_required!
response = rpc(:wallet_contains, account: account)
!response.empty? && response[:exists] == 1
end
def id
@wallet
end
def inspect # :nodoc:
"#{self.class.name}(id: \"#{id}\", object_id: \"#{"0x00%x" % (object_id << 1)}\")"
end
# Make a payment from an account in your wallet to another account
# on the nano network. Returns a send block hash if successful,
# or an error String if unsuccessful.
#
# ==== Arguments
#
# [+from:+] String account id of an account in your wallet
# [+to:+] String account id of the recipient of your payment
# [+amount:+] Can be either an Integer or Float.
# [+unit:+] Symbol (default is +:nano+). Represents the unit that +amount+ is in.
# Must be either +:nano+ or +:raw+. (Note: this method
# interprets +:nano+ as NANO, which is technically Mnano
# See {What are Nano's Units}[https://nano.org/en/faq#what-are-nano-units-])
# [+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 that nano payment once.
#
# 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")
# wallet.pay(from: "xrb_...", to: "xrb_...", amount: 54000000000000, unit: :raw, id: "myUniqueId123")
#
# ==== Example responses
# "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2"
#
# Or:
#
# "Account not found"
def pay(from:, to:, amount:, unit: Nanook::WalletAccount::DEFAULT_UNIT, id:)
wallet_required!
validate_wallet_contains_account!(from)
account(from).pay(to: to, amount: amount, unit: unit, id: id)
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 if a receive was successful,
# or +false+ if there were no pending payments to receive.
#
# You can also receive a specific pending block if you know it by
# passing the block has in as an argument.
#
# ==== Arguments
#
# [+block+] Optional block hash of pending payment. If not provided,
# the latest pending payment will be received
# [+into:+] String account id of account in your wallet to receive the
# payment into
#
# ==== Examples
#
# wallet.receive(into: "xrb...")
# wallet.receive("718CC21...", into: "xrb...")
#
# ==== Example responses
#
# "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2"
#
# Or:
#
# false
def receive(block=nil, into:)
wallet_required!
validate_wallet_contains_account!(into)
account(into).receive(block)
end
# Returns a boolean to indicate if the wallet is locked.
#
# ==== Example response
#
# true
def locked?
wallet_required!
response = rpc(:wallet_locked)
!response.empty? && response[:locked] != 0
end
# Unlocks a previously locked wallet. Returns a boolean to indicate
# if the action was successful.
#
# ==== Example response
#
# true
def unlock(password)
wallet_required!
rpc(:password_enter, password: password)[:valid] == 1
end
# Changes the password for a wallet. Returns a boolean to indicate
# if the action was successful.
#
# ==== Example response
#
# true
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