lib/nanook/wallet.rb in nanook-0.7.0 vs lib/nanook/wallet.rb in nanook-1.0.0
- old
+ new
@@ -1,93 +1,354 @@
class Nanook
+
+ # The <tt>Nanook::Wallet</tt> 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.
+ # <b>Make sure this key is always secret and safe</b>. 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(wallet, rpc)
- @wallet = 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
+ # <tt>"xrb..."</tt>) 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(@wallet, account, @rpc)
+ 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
- def balance(account_break_down: false)
+ # 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
- rpc(:wallet_balances)[:balances]
- else
- rpc(:wallet_balance_total)
+ 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
+ #
+ # <b>Please read this.</b> 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 <tt>"xrb_..."</tt>)
+ #
+ # ==== Example response
+ # true
def contains?(account)
wallet_required!
response = rpc(:wallet_contains, account: account)
!response.empty? && response[:exists] == 1
end
- def pay(from:, to:, amount:, id:)
+ 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 <i>send</i> 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]:
+ #
+ # <i>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.</i>
+ #
+ # ==== 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!
- account(from).pay(to: to, amount: amount, id: id)
+ 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 <i>receive</i> 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
- def all
- wallet_required!
- rpc(:account_list)[:accounts]
- 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