nanook 2.5.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'util'
4
+
5
+ class Nanook
6
+ # The <tt>Nanook::PublicKey</tt> class lets you manage your node's keys.
7
+ class PublicKey
8
+ include Nanook::Util
9
+
10
+ def initialize(rpc, key)
11
+ @rpc = rpc
12
+ @key = key.to_s
13
+ end
14
+
15
+ def id
16
+ @key
17
+ end
18
+
19
+ # @param other [Nanook::PublicKey] public key to compare
20
+ # @return [Boolean] true if keys are equal
21
+ def ==(other)
22
+ other.class == self.class &&
23
+ other.id == id
24
+ end
25
+ alias eql? ==
26
+
27
+ # The hash value is used along with #eql? by the Hash class to determine if two objects
28
+ # reference the same hash key.
29
+ #
30
+ # @return [Integer]
31
+ def hash
32
+ id.hash
33
+ end
34
+
35
+ # Returns the account for a public key
36
+ #
37
+ # @return [Nanook::Account] account for the public key
38
+ def account
39
+ account = rpc(:account_get, _access: :account)
40
+ as_account(account)
41
+ end
42
+
43
+ # @return [String]
44
+ def to_s
45
+ "#{self.class.name}(id: \"#{short_id}\")"
46
+ end
47
+ alias inspect to_s
48
+
49
+ private
50
+
51
+ def rpc(action, params = {})
52
+ @rpc.call(action, { key: @key }.merge(params))
53
+ end
54
+ end
55
+ end
data/lib/nanook/rpc.rb CHANGED
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'symbolized'
3
5
 
4
6
  class Nanook
5
-
6
7
  # The <tt>Nanook::Rpc</tt> class is responsible for maintaining the
7
8
  # connection to the RPC server, calling the RPC and parsing its response
8
9
  # into Ruby primitives.
@@ -14,23 +15,35 @@ class Nanook
14
15
  # nanook = Nanook.new
15
16
  # nanook.rpc(:accounts_create, wallet: wallet_id, count: 2)
16
17
  class Rpc
17
-
18
- # Default RPC server and port to connect to
19
- DEFAULT_URI = "http://localhost:7076"
20
- # Default request timeout in seconds
18
+ # Default RPC server and port to connect to.
19
+ DEFAULT_URI = 'http://[::1]:7076'
20
+ # Default request timeout in seconds.
21
21
  DEFAULT_TIMEOUT = 60
22
+ # Error expected to be returned when the RPC makes a call that requires the
23
+ # `enable_control` setting to be enabled when it is disabled.
24
+ RPC_CONTROL_DISABLED_ERROR = 'RPC control is disabled'
22
25
 
23
- def initialize(uri=DEFAULT_URI, timeout:DEFAULT_TIMEOUT)
26
+ def initialize(uri = DEFAULT_URI, timeout: DEFAULT_TIMEOUT)
24
27
  @rpc_server = URI(uri)
25
28
 
26
- unless ['http', 'https'].include?(@rpc_server.scheme)
27
- raise ArgumentError.new("URI must have http or https in it. Was given: #{uri}")
29
+ unless %w[http https].include?(@rpc_server.scheme)
30
+ raise ArgumentError, "URI must have http or https in it. Was given: #{uri}"
28
31
  end
29
32
 
30
- @http = Net::HTTP.new(@rpc_server.host, @rpc_server.port)
33
+ @http = Net::HTTP.new(@rpc_server.hostname, @rpc_server.port)
31
34
  @http.read_timeout = timeout
32
- @request = Net::HTTP::Post.new(@rpc_server.request_uri, {"user-agent" => "Ruby nanook gem"})
33
- @request.content_type = "application/json"
35
+ @request = Net::HTTP::Post.new(@rpc_server.request_uri, { 'user-agent' => "Ruby nanook gem v#{Nanook::VERSION}" })
36
+ @request.content_type = 'application/json'
37
+ end
38
+
39
+ # Tests the RPC connection. Returns +true+ if connection is successful,
40
+ # otherwise raises an exception.
41
+ #
42
+ # @raise [Errno::ECONNREFUSED] if connection is unsuccessful
43
+ # @return [Boolean] true if connection is successful
44
+ def test
45
+ call(:telemetry)
46
+ true
34
47
  end
35
48
 
36
49
  # Calls the RPC server and returns the response.
@@ -39,57 +52,104 @@ class Nanook
39
52
  # expects an "action" param to identify what RPC action is being called.
40
53
  # @param params [Hash] all other params to pass to the RPC
41
54
  # @return [Hash] the response from the RPC
42
- def call(action, params={})
43
- # Stringify param values
44
- params = Hash[params.map {|k, v| [k, v.to_s] }]
55
+ def call(action, params = {})
56
+ coerce_to = params.delete(:_coerce)
57
+ access_as = params.delete(:_access)
45
58
 
46
- @request.body = { action: action }.merge(params).to_json
59
+ raw_hash = make_call(action, params)
47
60
 
48
- response = @http.request(@request)
61
+ check_for_errors!(raw_hash)
49
62
 
50
- if response.is_a?(Net::HTTPSuccess)
51
- hash = JSON.parse(response.body)
52
- process_hash(hash)
53
- else
54
- raise Nanook::Error.new("Encountered net/http error #{response.code}: #{response.class.name}")
55
- end
63
+ hash = parse_values(raw_hash)
64
+
65
+ hash = hash[access_as] if access_as
66
+ hash = coerce_empty_string_to_type(hash, coerce_to) if coerce_to
67
+
68
+ hash
56
69
  end
57
70
 
58
71
  # @return [String]
59
- def inspect
60
- "#{self.class.name}(host: \"#{@rpc_server}\", timeout: #{@http.read_timeout} object_id: \"#{"0x00%x" % (object_id << 1)}\")"
72
+ def to_s
73
+ "#{self.class.name}(host: \"#{@rpc_server}\", timeout: #{@http.read_timeout})"
61
74
  end
75
+ alias inspect to_s
62
76
 
63
77
  private
64
78
 
79
+ def make_call(action, params)
80
+ # Stringify param values
81
+ params = params.dup.transform_values do |v|
82
+ next v if v.is_a?(Array)
83
+
84
+ v.to_s
85
+ end
86
+
87
+ @request.body = { action: action }.merge(params).to_json
88
+
89
+ response = @http.request(@request)
90
+
91
+ raise Nanook::ConnectionError, "Encountered net/http error #{response.code}: #{response.class.name}" \
92
+ unless response.is_a?(Net::HTTPSuccess)
93
+
94
+ JSON.parse(response.body)
95
+ end
96
+
97
+ # Raises a {Nanook::NodeRpcConfigurationError} or {Nanook::NodeRpcError} if the RPC
98
+ # response contains an `:error` key.
99
+ def check_for_errors!(response)
100
+ # Raise a special error for when `enable_control` should be enabled.
101
+ if response['error'] == RPC_CONTROL_DISABLED_ERROR
102
+ raise Nanook::NodeRpcConfigurationError,
103
+ 'RPC must have the `enable_control` setting enabled to perform this action.'
104
+ end
105
+
106
+ # Raise any other error.
107
+ raise Nanook::NodeRpcError, "An error was returned from the RPC: #{response['error']}" if response.key?('error')
108
+ end
109
+
65
110
  # Recursively parses the RPC response, sending values to #parse_value
66
- def process_hash(h)
67
- new_hash = h.map do |k,v|
68
- v = if v.is_a?(Array)
69
- if v[0].is_a?(Hash)
70
- v.map{|v| process_hash(v)}
71
- else
72
- v.map{|v| parse_value(v)}
73
- end
74
- elsif v.is_a?(Hash)
75
- process_hash(v)
76
- else
77
- parse_value(v)
78
- end
79
-
80
- [k, v]
111
+ def parse_values(hash)
112
+ new_hash = hash.map do |k, val|
113
+ new_val = case val
114
+ when Array
115
+ if val[0].is_a?(Hash)
116
+ val.map { |v| parse_values(v) }
117
+ else
118
+ val.map { |v| parse_value(v) }
119
+ end
120
+ when Hash
121
+ parse_values(val)
122
+ else
123
+ parse_value(val)
124
+ end
125
+
126
+ [k, new_val]
81
127
  end
82
128
 
83
129
  Hash[new_hash.sort].to_symbolized_hash
84
130
  end
85
131
 
86
132
  # Converts Strings to primitives
87
- def parse_value(v)
88
- return v.to_i if v.match(/^\d+\Z/)
89
- return true if v == "true"
90
- return false if v == "false"
91
- v
133
+ def parse_value(value)
134
+ return value.to_i if value.match(/^\d+\Z/)
135
+ return true if value == 'true'
136
+ return false if value == 'false'
137
+
138
+ value
92
139
  end
93
140
 
141
+ # Converts an empty String value into an empty version of another type.
142
+ #
143
+ # The RPC often returns an empty String as a value to signal
144
+ # emptiness, rather than consistent types like an empty Array,
145
+ # or empty Hash.
146
+ #
147
+ # @param response the value returned from the RPC server
148
+ # @param type the type to return an empty of
149
+ def coerce_empty_string_to_type(response, type)
150
+ return type.new if response == '' || response.nil?
151
+
152
+ response
153
+ end
94
154
  end
95
155
  end
data/lib/nanook/util.rb CHANGED
@@ -1,18 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bigdecimal'
2
4
 
3
5
  class Nanook
4
-
5
- # Set of class utility methods.
6
- class Util
7
-
6
+ # Set of utility methods.
7
+ module Util
8
8
  # Constant used to convert back and forth between raw and NANO.
9
- STEP = BigDecimal("10")**BigDecimal("30")
9
+ STEP = BigDecimal('10')**BigDecimal('30')
10
+
11
+ private
10
12
 
11
13
  # Converts an amount of NANO to an amount of raw.
12
14
  #
13
15
  # @param nano [Float|Integer] amount in nano
14
16
  # @return [Integer] amount in raw
15
- def self.NANO_to_raw(nano)
17
+ def NANO_to_raw(nano)
18
+ return if nano.nil?
19
+
16
20
  (BigDecimal(nano.to_s) * STEP).to_i
17
21
  end
18
22
 
@@ -20,25 +24,70 @@ class Nanook
20
24
  #
21
25
  # @param raw [Integer] amount in raw
22
26
  # @return [Float|Integer] amount in NANO
23
- def self.raw_to_NANO(raw)
27
+ def raw_to_NANO(raw)
28
+ return if raw.nil?
29
+
24
30
  (raw.to_f / STEP).to_f
25
31
  end
26
32
 
27
- # Converts an empty String value into an empty version of another type.
33
+ # @return [TrueClass] if unit is valid.
34
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid.
35
+ def validate_unit!(unit)
36
+ unless Nanook::UNITS.include?(unit.to_sym)
37
+ raise Nanook::NanoUnitError, "Unit #{unit} must be one of #{Nanook::UNITS}"
38
+ end
39
+
40
+ true
41
+ end
42
+
43
+ # Returns the +id+ of the object as a short id.
44
+ # See #shorten_id.
28
45
  #
29
- # The RPC often returns an empty String (<tt>""</tt>) as a value, when a
30
- # +nil+, or empty Array (<tt>[]</tt>), or empty Hash (<tt>{}</tt>) would be better.
31
- # If the response might be
46
+ # @return [String]
47
+ def short_id
48
+ shorten_id(id)
49
+ end
50
+
51
+ # Returns an id string (hash or nano account) truncated with an ellipsis.
52
+ # The first 7 and last 4 characters are retained for easy identification.
32
53
  #
33
- # @param response the value returned from the RPC server
34
- # @param type the type to return an empty of
35
- def self.coerce_empty_string_to_type(response, type)
36
- if response == "" || response.nil?
37
- return type.new
38
- end
54
+ # ==== Examples:
55
+ #
56
+ # shorten_id('nano_16u1uufyoig8777y6r8iqjtrw8sg8maqrm36zzcm95jmbd9i9aj5i8abr8u5')
57
+ # # => "16u1uuf...r8u5"
58
+ #
59
+ # shorten_id('A170D51B94E00371ACE76E35AC81DC9405D5D04D4CEBC399AEACE07AE05DD293')
60
+ # # => "A170D51...D293"
61
+ #
62
+ # @return [String]
63
+ def shorten_id(long_id)
64
+ return unless long_id
65
+
66
+ [long_id.sub('nano_', '')[0..6], long_id[-4, 4]].join('...')
67
+ end
39
68
 
40
- response
69
+ def as_account(account_id)
70
+ Nanook::Account.new(@rpc, account_id)
41
71
  end
42
72
 
73
+ def as_wallet_account(account_id)
74
+ Nanook::WalletAccount.new(@rpc, @wallet, account_id)
75
+ end
76
+
77
+ def as_block(block_id)
78
+ Nanook::Block.new(@rpc, block_id)
79
+ end
80
+
81
+ def as_private_key(key)
82
+ Nanook::PrivateKey.new(@rpc, key)
83
+ end
84
+
85
+ def as_public_key(key)
86
+ Nanook::PublicKey.new(@rpc, key)
87
+ end
88
+
89
+ def as_time(time)
90
+ Time.at(time).utc if time
91
+ end
43
92
  end
44
93
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Nanook
2
- VERSION = "2.5.1"
4
+ VERSION = '3.0.0'
3
5
  end
data/lib/nanook/wallet.rb CHANGED
@@ -1,7 +1,10 @@
1
- class Nanook
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'util'
2
4
 
3
- # The <tt>Nanook::Wallet</tt> class lets you manage your nano wallets,
4
- # as well as some account-specific things like making and receiving payments.
5
+ class Nanook
6
+ # The <tt>Nanook::Wallet</tt> class lets you manage your nano wallets.
7
+ # Your node will need the <tt>enable_control</tt> setting enabled.
5
8
  #
6
9
  # === Wallet seeds vs ids
7
10
  #
@@ -47,10 +50,32 @@ class Nanook
47
50
  # rpc_conn = Nanook::Rpc.new
48
51
  # wallet = Nanook::Wallet.new(rpc_conn, wallet_id)
49
52
  class Wallet
53
+ include Nanook::Util
50
54
 
51
- def initialize(rpc, wallet)
55
+ def initialize(rpc, wallet = nil)
52
56
  @rpc = rpc
53
- @wallet = wallet
57
+ @wallet = wallet.to_s if wallet
58
+ end
59
+
60
+ # @return [String] the wallet id
61
+ def id
62
+ @wallet
63
+ end
64
+
65
+ # @param other [Nanook::Wallet] wallet to compare
66
+ # @return [Boolean] true if wallets are equal
67
+ def ==(other)
68
+ other.class == self.class &&
69
+ other.id == id
70
+ end
71
+ alias eql? ==
72
+
73
+ # The hash value is used along with #eql? by the Hash class to determine if two objects
74
+ # reference the same hash key.
75
+ #
76
+ # @return [Integer]
77
+ def hash
78
+ id.hash
54
79
  end
55
80
 
56
81
  # Returns the given account in the wallet as a {Nanook::WalletAccount} instance
@@ -69,14 +94,15 @@ class Nanook
69
94
  # wallet.account("nano_...") # => Nanook::WalletAccount
70
95
  # wallet.account.create # => Nanook::WalletAccount
71
96
  #
72
- # @param [String] account optional String of an account (starting with
97
+ # @param account [String] optional String of an account (starting with
73
98
  # <tt>"xrb..."</tt>) to start working with. Must be an account within
74
99
  # the wallet. When no account is given, the instance returned only
75
100
  # allows you to call +create+ on it, to create a new account.
76
- # @raise [ArgumentError] if the wallet does no contain the account
101
+ # @raise [ArgumentError] if the wallet does not contain the account
77
102
  # @return [Nanook::WalletAccount]
78
- def account(account=nil)
79
- Nanook::WalletAccount.new(@rpc, @wallet, account)
103
+ def account(account = nil)
104
+ check_wallet_required!
105
+ as_wallet_account(account)
80
106
  end
81
107
 
82
108
  # Array of {Nanook::WalletAccount} instances of accounts in the wallet.
@@ -90,13 +116,33 @@ class Nanook
90
116
  #
91
117
  # @return [Array<Nanook::WalletAccount>] all accounts in the wallet
92
118
  def accounts
93
- wallet_required!
94
- response = rpc(:account_list)[:accounts]
95
- Nanook::Util.coerce_empty_string_to_type(response, Array).map do |account|
96
- Nanook::WalletAccount.new(@rpc, @wallet, account)
119
+ rpc(:account_list, _access: :accounts, _coerce: Array).map do |account|
120
+ as_wallet_account(account)
97
121
  end
98
122
  end
99
123
 
124
+ # Move accounts from another {Nanook::Wallet} on the node to this {Nanook::Wallet}.
125
+ #
126
+ # ==== Example:
127
+ #
128
+ # wallet.move_accounts("0023200...", ["nano_3e3j5...", "nano_5f2a1..."]) # => true
129
+ #
130
+ # @return [Boolean] true when the move was successful
131
+ def move_accounts(wallet, accounts)
132
+ rpc(:account_move, source: wallet, accounts: accounts, _access: :moved) == 1
133
+ end
134
+
135
+ # Remove an {Nanook::Account} from this {Nanook::Wallet}.
136
+ #
137
+ # ==== Example:
138
+ #
139
+ # wallet.remove_account("nano_3e3j5...") # => true
140
+ #
141
+ # @return [Boolean] true when the remove was successful
142
+ def remove_account(account)
143
+ rpc(:account_remove, account: account, _access: :removed) == 1
144
+ end
145
+
100
146
  # Balance of all accounts in the wallet, optionally breaking the balances down by account.
101
147
  #
102
148
  # ==== Examples:
@@ -137,51 +183,50 @@ class Nanook
137
183
  # },
138
184
  # }
139
185
  #
140
- # @param [Boolean] account_break_down (default is +false+). When +true+
186
+ # @param account_break_down [Boolean] (default is +false+). When +true+
141
187
  # the response will contain balances per account.
142
188
  # @param unit (see Nanook::Account#balance)
143
189
  #
144
190
  # @return [Hash{Symbol=>Integer|Float|Hash}]
191
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
145
192
  def balance(account_break_down: false, unit: Nanook.default_unit)
146
- wallet_required!
147
-
148
- unless Nanook::UNITS.include?(unit)
149
- raise ArgumentError.new("Unsupported unit: #{unit}")
150
- end
193
+ validate_unit!(unit)
151
194
 
152
195
  if account_break_down
153
- return Nanook::Util.coerce_empty_string_to_type(rpc(:wallet_balances)[:balances], Hash).tap do |r|
196
+ return rpc(:wallet_balances, _access: :balances, _coerce: Hash).tap do |r|
154
197
  if unit == :nano
155
- r.each do |account, balances|
156
- r[account][:balance] = Nanook::Util.raw_to_NANO(r[account][:balance])
157
- r[account][:pending] = Nanook::Util.raw_to_NANO(r[account][:pending])
198
+ r.each do |account, _balances|
199
+ r[account][:balance] = raw_to_NANO(r[account][:balance])
200
+ r[account][:pending] = raw_to_NANO(r[account][:pending])
158
201
  end
159
202
  end
160
203
  end
161
204
  end
162
205
 
163
- rpc(:wallet_balance_total).tap do |r|
164
- if unit == :nano
165
- r[:balance] = Nanook::Util.raw_to_NANO(r[:balance])
166
- r[:pending] = Nanook::Util.raw_to_NANO(r[:pending])
167
- end
168
- end
206
+ response = rpc(:wallet_info, _coerce: Hash).slice(:balance, :pending)
207
+ return response unless unit == :nano
208
+
209
+ {
210
+ balance: raw_to_NANO(response[:balance]),
211
+ pending: raw_to_NANO(response[:pending])
212
+ }
169
213
  end
170
214
 
171
215
  # Changes a wallet's seed.
172
216
  #
173
217
  # It's recommended to only change the seed of a wallet that contains
174
- # no accounts.
218
+ # no accounts. This will clear all deterministic accounts in the wallet.
219
+ # To restore accounts after changing the seed, see Nanook::WalletAccount#create.
175
220
  #
176
221
  # ==== Example:
177
222
  #
178
223
  # wallet.change_seed("000D1BA...") # => true
224
+ # wallet.account.create(5) # Restores first 5 accounts for wallet with new seed
179
225
  #
180
226
  # @param seed [String] the seed to change to.
181
227
  # @return [Boolean] indicating whether the change was successful.
182
228
  def change_seed(seed)
183
- wallet_required!
184
- rpc(:wallet_change_seed, seed: seed).has_key?(:success)
229
+ rpc(:wallet_change_seed, seed: seed).key?(:success)
185
230
  end
186
231
 
187
232
  # Creates a new wallet.
@@ -199,7 +244,8 @@ class Nanook
199
244
  #
200
245
  # @return [Nanook::Wallet]
201
246
  def create
202
- @wallet = rpc(:wallet_create)[:wallet]
247
+ skip_wallet_required!
248
+ @wallet = rpc(:wallet_create, _access: :wallet)
203
249
  self
204
250
  end
205
251
 
@@ -211,19 +257,33 @@ class Nanook
211
257
  #
212
258
  # @return [Boolean] indicating success of the action
213
259
  def destroy
214
- wallet_required!
215
- rpc(:wallet_destroy)
216
- true
260
+ rpc(:wallet_destroy, _access: :destroyed) == 1
217
261
  end
218
262
 
219
263
  # Generates a String containing a JSON representation of your wallet.
220
264
  #
221
265
  # ==== Example:
222
266
  #
223
- # 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"
267
+ # wallet.export
268
+ # # => "{\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"
269
+ #
270
+ # @return [String]
224
271
  def export
225
- wallet_required!
226
- rpc(:wallet_export)[:json]
272
+ rpc(:wallet_export, _access: :json)
273
+ end
274
+
275
+ # Returns true if wallet exists on the node.
276
+ #
277
+ # ==== Example:
278
+ #
279
+ # wallet.exists? # => true
280
+ #
281
+ # @return [Boolean] true if wallet exists on the node
282
+ def exists?
283
+ export
284
+ true
285
+ rescue Nanook::NodeRpcError
286
+ false
227
287
  end
228
288
 
229
289
  # Will return +true+ if the account exists in the wallet.
@@ -234,20 +294,14 @@ class Nanook
234
294
  # @param account [String] id (will start with <tt>"nano_..."</tt>)
235
295
  # @return [Boolean] indicating if the wallet contains the given account
236
296
  def contains?(account)
237
- wallet_required!
238
- response = rpc(:wallet_contains, account: account)
239
- !response.empty? && response[:exists] == 1
240
- end
241
-
242
- # @return [String] the wallet id
243
- def id
244
- @wallet
297
+ rpc(:wallet_contains, account: account, _access: :exists) == 1
245
298
  end
246
299
 
247
300
  # @return [String]
248
- def inspect
249
- "#{self.class.name}(id: \"#{id}\", object_id: \"#{"0x00%x" % (object_id << 1)}\")"
301
+ def to_s
302
+ "#{self.class.name}(id: \"#{short_id}\")"
250
303
  end
304
+ alias inspect to_s
251
305
 
252
306
  # Makes a payment from an account in your wallet to another account
253
307
  # on the nano network.
@@ -263,7 +317,8 @@ class Nanook
263
317
  # ==== Examples:
264
318
  #
265
319
  # wallet.pay(from: "nano_...", to: "nano_...", amount: 1.1, id: "myUniqueId123") # => "9AE2311..."
266
- # wallet.pay(from: "nano_...", to: "nano_...", amount: 54000000000000, unit: :raw, id: "myUniqueId123") # => "9AE2311..."
320
+ # wallet.pay(from: "nano_...", to: "nano_...", amount: 54000000000000, unit: :raw, id: "myUniqueId123")
321
+ # # => "9AE2311..."
267
322
  #
268
323
  # @param from [String] account id of an account in your wallet
269
324
  # @param to (see Nanook::WalletAccount#pay)
@@ -272,8 +327,7 @@ class Nanook
272
327
  # @params id (see Nanook::WalletAccount#pay)
273
328
  # @return (see Nanook::WalletAccount#pay)
274
329
  # @raise [Nanook::Error] if unsuccessful
275
- def pay(from:, to:, amount:, unit: Nanook.default_unit, id:)
276
- wallet_required!
330
+ def pay(from:, to:, amount:, id:, unit: Nanook.default_unit)
277
331
  validate_wallet_contains_account!(from)
278
332
  account(from).pay(to: to, amount: amount, unit: unit, id: id)
279
333
  end
@@ -294,12 +348,12 @@ class Nanook
294
348
  # Example response:
295
349
  #
296
350
  # {
297
- # :nano_1111111111111111111111111111111111111111111111111117353trpda=>[
298
- # "142A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D",
299
- # "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2"
351
+ # Nanook::Account=>[
352
+ # Nanook::Block,
353
+ # Nanook::Block"
300
354
  # ],
301
- # :nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3=>[
302
- # "4C1FEEF0BEA7F50BE35489A1233FE002B212DEA554B55B1B470D78BD8F210C74"
355
+ # Nanook::Account=>[
356
+ # Nanook::Block
303
357
  # ]
304
358
  # }
305
359
  #
@@ -310,57 +364,69 @@ class Nanook
310
364
  # Example response:
311
365
  #
312
366
  # {
313
- # :nano_1111111111111111111111111111111111111111111111111117353trpda=>[
367
+ # Nanook::Account=>[
314
368
  # {
315
369
  # :amount=>6.0,
316
- # :source=>"nano_3dcfozsmekr1tr9skf1oa5wbgmxt81qepfdnt7zicq5x3hk65fg4fqj58mbr",
317
- # :block=>:"142A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D"
370
+ # :source=>Nanook::Account,
371
+ # :block=>Nanook::Block
318
372
  # },
319
373
  # {
320
374
  # :amount=>12.0,
321
- # :source=>"nano_3dcfozsmekr1tr9skf1oa5wbgmxt81qepfdnt7zicq5x3hk65fg4fqj58mbr",
322
- # :block=>:"242A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D"
375
+ # :source=>Nanook::Account,
376
+ # :block=>Nanook::Block
323
377
  # }
324
378
  # ],
325
- # :nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3=>[
379
+ # Nanook::Account=>[
326
380
  # {
327
381
  # :amount=>106.370018,
328
- # :source=>"nano_13ezf4od79h1tgj9aiu4djzcmmguendtjfuhwfukhuucboua8cpoihmh8byo",
329
- # :block=>:"4C1FEEF0BEA7F50BE35489A1233FE002B212DEA554B55B1B470D78BD8F210C74"
382
+ # :source=>Nanook::Account,
383
+ # :block=>Nanook::Block
330
384
  # }
331
385
  # ]
332
386
  # }
333
- def pending(limit:1000, detailed:false, unit:Nanook.default_unit)
334
- wallet_required!
387
+ #
388
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
389
+ def pending(limit: 1000, detailed: false, unit: Nanook.default_unit)
390
+ validate_unit!(unit)
335
391
 
336
- unless Nanook::UNITS.include?(unit)
337
- raise ArgumentError.new("Unsupported unit: #{unit}")
338
- end
392
+ params = {
393
+ count: limit,
394
+ _access: :blocks,
395
+ _coerce: Hash
396
+ }
339
397
 
340
- params = { count: limit }
341
398
  params[:source] = true if detailed
342
399
 
343
- response = rpc(:wallet_pending, params)[:blocks]
344
- response = Nanook::Util.coerce_empty_string_to_type(response, Hash)
400
+ response = rpc(:wallet_pending, params)
345
401
 
346
- return response unless detailed
402
+ unless detailed
403
+
404
+ x = response.map do |account, block_ids|
405
+ blocks = block_ids.map { |block_id| as_block(block_id) }
406
+ [as_account(account), blocks]
407
+ end
408
+
409
+ return Hash[x]
410
+ end
347
411
 
348
412
  # Map the RPC response, which is:
349
413
  # account=>block=>[amount|source] into
350
414
  # account=>[block|amount|source]
351
415
  x = response.map do |account, data|
352
416
  new_data = data.map do |block, amount_and_source|
353
- d = amount_and_source.merge(block: block.to_s)
354
- if unit == :nano
355
- d[:amount] = Nanook::Util.raw_to_NANO(d[:amount])
356
- end
417
+ d = {
418
+ block: as_block(block),
419
+ source: as_account(amount_and_source[:source]),
420
+ amount: amount_and_source[:amount]
421
+ }
422
+ d[:amount] = raw_to_NANO(d[:amount]) if unit == :nano
357
423
  d
358
424
  end
359
425
 
360
- [account, new_data]
426
+ [as_account(account), new_data]
361
427
  end
362
428
 
363
- Hash[x].to_symbolized_hash
429
+ Hash[x]
364
430
  end
365
431
 
366
432
  # Receives a pending payment into an account in the wallet.
@@ -368,7 +434,7 @@ class Nanook
368
434
  # When called with no +block+ argument, the latest pending payment
369
435
  # for the account will be received.
370
436
  #
371
- # Returns a <i>receive</i> block hash id if a receive was successful,
437
+ # Returns a <i>receive</i> block if a receive was successful,
372
438
  # or +false+ if there were no pending payments to receive.
373
439
  #
374
440
  # You can receive a specific pending block if you know it by
@@ -376,19 +442,33 @@ class Nanook
376
442
  #
377
443
  # ==== Examples:
378
444
  #
379
- # wallet.receive(into: "xrb...") # => "9AE2311..."
380
- # wallet.receive("718CC21...", into: "xrb...") # => "9AE2311..."
445
+ # wallet.receive(into: "xrb...") # => Nanook::Block
446
+ # wallet.receive("718CC21...", into: "xrb...") # => Nanook::Block
381
447
  #
382
448
  # @param block (see Nanook::WalletAccount#receive)
383
449
  # @param into [String] account id of account in your wallet to receive the
384
450
  # payment into
385
451
  # @return (see Nanook::WalletAccount#receive)
386
- def receive(block=nil, into:)
387
- wallet_required!
452
+ def receive(block = nil, into:)
388
453
  validate_wallet_contains_account!(into)
389
454
  account(into).receive(block)
390
455
  end
391
456
 
457
+ # Rebroadcast blocks for accounts from wallet starting at frontier down to count to the network.
458
+ #
459
+ # ==== Examples:
460
+ #
461
+ # wallet.republish_blocks # => [Nanook::Block, ...]
462
+ # wallet.republish_blocks(limit: 10) # => [Nanook::Block, ...
463
+ #
464
+ # @param limit [Integer] limit of blocks to publish. Default is 1000.
465
+ # @return [Array<Nanook::Block>] republished blocks
466
+ def republish_blocks(limit: 1000)
467
+ rpc(:wallet_republish, count: limit, _access: :blocks, _coerce: Array).map do |block|
468
+ as_block(block)
469
+ end
470
+ end
471
+
392
472
  # The default representative account id for the wallet. This is the
393
473
  # representative that all new accounts created in this wallet will have.
394
474
  #
@@ -399,11 +479,12 @@ class Nanook
399
479
  #
400
480
  # wallet.default_representative # => "nano_3pc..."
401
481
  #
402
- # @return [String] Representative account of the account
482
+ # @return [Nanook::Account] Representative account. Can be nil.
403
483
  def default_representative
404
- rpc(:wallet_representative)[:representative]
484
+ representative = rpc(:wallet_representative, _access: :representative)
485
+ as_account(representative) if representative
405
486
  end
406
- alias_method :representative, :default_representative
487
+ alias representative default_representative
407
488
 
408
489
  # Sets the default representative for the wallet. A wallet's default
409
490
  # representative is the representative all new accounts created in
@@ -415,23 +496,21 @@ class Nanook
415
496
  #
416
497
  # wallet.change_default_representative("nano_...") # => "nano_..."
417
498
  #
418
- # @param [String] representative the id of the representative account
499
+ # @param representative [String] id of the representative account
419
500
  # to set as this account's representative
420
- # @return [String] the representative account id
421
- # @raise [ArgumentError] if the representative account does not exist
501
+ # @return [Nanook::Account] the representative account
422
502
  # @raise [Nanook::Error] if setting the representative fails
423
503
  def change_default_representative(representative)
424
- unless Nanook::Account.new(@rpc, representative).exists?
425
- raise ArgumentError.new("Representative account does not exist: #{representative}")
504
+ unless as_account(representative).exists?
505
+ raise Nanook::Error, "Representative account does not exist: #{representative}"
426
506
  end
427
507
 
428
- if rpc(:wallet_representative_set, representative: representative)[:set] == 1
429
- representative
430
- else
431
- raise Nanook::Error.new("Setting the representative failed")
432
- end
508
+ raise Nanook::Error, 'Setting the representative failed' \
509
+ unless rpc(:wallet_representative_set, representative: representative, _access: :set) == 1
510
+
511
+ as_account(representative)
433
512
  end
434
- alias_method :change_representative, :change_default_representative
513
+ alias change_representative change_default_representative
435
514
 
436
515
  # Restores a previously created wallet by its seed.
437
516
  # A new wallet will be created on your node (with a new wallet id)
@@ -446,21 +525,68 @@ class Nanook
446
525
  #
447
526
  # @return [Nanook::Wallet] a new wallet
448
527
  # @raise [Nanook::Error] if unsuccessful
449
- def restore(seed, accounts:0)
528
+ def restore(seed, accounts: 0)
529
+ skip_wallet_required!
530
+
450
531
  create
451
532
 
452
- unless change_seed(seed)
453
- raise Nanook::Error.new("Unable to set seed for wallet")
454
- end
533
+ raise Nanook::Error, 'Unable to set seed for wallet' unless change_seed(seed)
455
534
 
456
- if accounts > 0
457
- account.create(accounts)
458
- end
535
+ account.create(accounts) if accounts.positive?
459
536
 
460
537
  self
461
538
  end
462
539
 
463
- # Information about this wallet and all of its accounts.
540
+ # Information ledger information about this wallet's accounts.
541
+ #
542
+ # This call may return results that include unconfirmed blocks, so it should not be
543
+ # used in any processes or integrations requiring only details from blocks confirmed
544
+ # by the network.
545
+ #
546
+ # ==== Examples:
547
+ #
548
+ # wallet.ledger
549
+ #
550
+ # Example response:
551
+ #
552
+ # {
553
+ # Nanook::Account => {
554
+ # frontier: "E71AF3E9DD86BBD8B4620EFA63E065B34D358CFC091ACB4E103B965F95783321",
555
+ # open_block: "643B77F1ECEFBDBE1CC909872964C1DBBE23A6149BD3CEF2B50B76044659B60F",
556
+ # representative_block: "643B77F1ECEFBDBE1CC909872964C1DBBE23A6149BD3CEF2B50B76044659B60F",
557
+ # balance: 1.45,
558
+ # modified_timestamp: 1511476234,
559
+ # block_count: 2
560
+ # },
561
+ # Nanook::Account => { ... }
562
+ # }
563
+ #
564
+ # @param unit (see Nanook::Account#balance)
565
+ # @return [Hash{Nanook::Account=>Hash{Symbol=>Nanook::Block|Integer|Float|Time}}] ledger.
566
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
567
+ def ledger(unit: Nanook.default_unit)
568
+ validate_unit!(unit)
569
+
570
+ response = rpc(:wallet_ledger, _access: :accounts, _coerce: Hash)
571
+
572
+ accounts = response.map do |account_id, data|
573
+ data[:frontier] = as_block(data[:frontier]) if data[:frontier]
574
+ data[:open_block] = as_block(data[:open_block]) if data[:open_block]
575
+ data[:representative_block] = as_block(data[:representative_block]) if data[:representative_block]
576
+ data[:balance] = raw_to_NANO(data[:balance]) if unit == :nano && data[:balance]
577
+ data[:last_modified_at] = as_time(data.delete(:modified_timestamp))
578
+
579
+ [as_account(account_id), data]
580
+ end
581
+
582
+ Hash[accounts]
583
+ end
584
+
585
+ # Information about this wallet.
586
+ #
587
+ # This call may return results that include unconfirmed blocks, so it should not be
588
+ # used in any processes or integrations requiring only details from blocks confirmed
589
+ # by the network.
464
590
  #
465
591
  # ==== Examples:
466
592
  #
@@ -469,42 +595,68 @@ class Nanook
469
595
  # Example response:
470
596
  #
471
597
  # {
472
- # id: "2C3C570EA8898443C0FD04A1C385A3E3A8C985AD792635FCDCEBB30ADF6A0570",
473
- # accounts: [
474
- # {
475
- # id: "nano_11119gbh8hb4hj1duf7fdtfyf5s75okzxdgupgpgm1bj78ex3kgy7frt3s9n"
476
- # frontier: "E71AF3E9DD86BBD8B4620EFA63E065B34D358CFC091ACB4E103B965F95783321",
477
- # open_block: "643B77F1ECEFBDBE1CC909872964C1DBBE23A6149BD3CEF2B50B76044659B60F",
478
- # representative_block: "643B77F1ECEFBDBE1CC909872964C1DBBE23A6149BD3CEF2B50B76044659B60F",
479
- # balance: 1.45,
480
- # modified_timestamp: 1511476234,
481
- # block_count: 2
482
- # },
483
- # { ... }
484
- # ]
485
- # }
598
+ # balance: 1.0,
599
+ # pending: 2.3
600
+ # accounts_count: 3,
601
+ # adhoc_count: 1,
602
+ # deterministic_count: 2,
603
+ # deterministic_index: 2
604
+ # }
486
605
  #
487
- # @param unit (see #balance)
488
- # @return [Hash{Symbol=>String|Array<Hash{Symbol=>String|Integer|Float}>}] information about the wallet.
489
- # See {Nanook::Account#info} for details of what is returned for each account.
606
+ # @param unit (see Nanook::Account#balance)
607
+ # @return [Hash{Symbol=>Integer|Float}] information about the wallet.
608
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
490
609
  def info(unit: Nanook.default_unit)
491
- unless Nanook::UNITS.include?(unit)
492
- raise ArgumentError.new("Unsupported unit: #{unit}")
493
- end
610
+ validate_unit!(unit)
494
611
 
495
- wallet_required!
496
- accounts = rpc(:wallet_ledger)[:accounts].map do |account_id, payload|
497
- payload[:id] = account_id
498
- if unit == :nano
499
- payload[:balance] = Nanook::Util.raw_to_NANO(payload[:balance])
500
- end
501
- payload
612
+ response = rpc(:wallet_info, _coerce: Hash)
613
+
614
+ if unit == :nano
615
+ response[:balance] = raw_to_NANO(response[:balance])
616
+ response[:pending] = raw_to_NANO(response[:pending])
502
617
  end
503
618
 
504
- {
505
- id: @wallet,
506
- accounts: accounts
507
- }.to_symbolized_hash
619
+ response
620
+ end
621
+
622
+ # Reports send/receive information for accounts in wallet. Change blocks are skipped,
623
+ # open blocks will appear as receive. Response will start with most recent blocks
624
+ # according to local ledger.
625
+ #
626
+ # ==== Example:
627
+ #
628
+ # wallet.history
629
+ #
630
+ # Example response:
631
+ #
632
+ # [
633
+ # {
634
+ # "type": "send",
635
+ # "account": Nanook::Account,
636
+ # "amount": 3.2,
637
+ # "block_account": Nanook::Account,
638
+ # "hash": Nanook::Block,
639
+ # "local_timestamp": Time
640
+ # },
641
+ # {
642
+ # ...
643
+ # }
644
+ # ]
645
+ #
646
+ # @param unit (see #balance)
647
+ # @return [Array<Hash{Symbol=>String|Nanook::Account|Nanook::WalletAccount|Nanook::Block|Integer|Float|Time}>]
648
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
649
+ def history(unit: Nanook.default_unit)
650
+ validate_unit!(unit)
651
+
652
+ rpc(:wallet_history, _access: :history, _coerce: Array).map do |h|
653
+ h[:account] = account(h[:account])
654
+ h[:block_account] = as_account(h[:block_account])
655
+ h[:amount] = raw_to_NANO(h[:amount]) if unit == :nano
656
+ h[:block] = as_block(h.delete(:hash))
657
+ h[:local_timestamp] = as_time(h[:local_timestamp])
658
+ h
659
+ end
508
660
  end
509
661
 
510
662
  # Locks the wallet. A locked wallet cannot pocket pending transactions or make payments. See {#unlock}.
@@ -515,9 +667,7 @@ class Nanook
515
667
  #
516
668
  # @return [Boolean] indicates if the wallet was successfully locked
517
669
  def lock
518
- wallet_required!
519
- response = rpc(:wallet_lock)
520
- !response.empty? && response[:locked] == 1
670
+ rpc(:wallet_lock, _access: :locked) == 1
521
671
  end
522
672
 
523
673
  # Returns +true+ if the wallet is locked.
@@ -528,9 +678,7 @@ class Nanook
528
678
  #
529
679
  # @return [Boolean] indicates if the wallet is locked
530
680
  def locked?
531
- wallet_required!
532
- response = rpc(:wallet_locked)
533
- !response.empty? && response[:locked] != 0
681
+ rpc(:wallet_locked, _access: :locked) == 1
534
682
  end
535
683
 
536
684
  # Unlocks a previously locked wallet.
@@ -540,9 +688,8 @@ class Nanook
540
688
  # wallet.unlock("new_pass") #=> true
541
689
  #
542
690
  # @return [Boolean] indicates if the unlocking action was successful
543
- def unlock(password)
544
- wallet_required!
545
- rpc(:password_enter, password: password)[:valid] == 1
691
+ def unlock(password = nil)
692
+ rpc(:password_enter, password: password, _access: :valid) == 1
546
693
  end
547
694
 
548
695
  # Changes the password for a wallet.
@@ -552,33 +699,73 @@ class Nanook
552
699
  # wallet.change_password("new_pass") #=> true
553
700
  # @return [Boolean] indicates if the action was successful
554
701
  def change_password(password)
555
- wallet_required!
556
- rpc(:password_change, password: password)[:changed] == 1
702
+ rpc(:password_change, password: password, _access: :changed) == 1
703
+ end
704
+
705
+ # Tells the node to look for pending blocks for any account in the wallet.
706
+ #
707
+ # ==== Example:
708
+ #
709
+ # wallet.search_pending #=> true
710
+ # @return [Boolean] indicates if the action was successful
711
+ def search_pending
712
+ rpc(:search_pending, _access: :started) == 1
713
+ end
714
+
715
+ # Returns a list of pairs of {Nanook::WalletAccount} and work for wallet.
716
+ #
717
+ # ==== Example:
718
+ #
719
+ # wallet.work
720
+ #
721
+ # ==== Example response:
722
+ #
723
+ # {
724
+ # Nanook::WalletAccount: "432e5cf728c90f4f",
725
+ # Nanook::WalletAccount: "4efec5f63fc902cf"
726
+ # }
727
+ # @return [Boolean] indicates if the action was successful
728
+ def work
729
+ hash = rpc(:wallet_work_get, _access: :works, _coerce: Hash).map do |account_id, work|
730
+ [as_wallet_account(account_id), work]
731
+ end
732
+
733
+ Hash[hash]
557
734
  end
558
735
 
559
736
  private
560
737
 
561
- def rpc(action, params={})
562
- p = @wallet.nil? ? {} : { wallet: @wallet }
563
- @rpc.call(action, p.merge(params))
738
+ def rpc(action, params = {})
739
+ check_wallet_required!
740
+
741
+ p = { wallet: @wallet }.compact
742
+ @rpc.call(action, p.merge(params)).tap { reset_skip_wallet_required! }
564
743
  end
565
744
 
566
- def wallet_required!
567
- if @wallet.nil?
568
- raise ArgumentError.new("Wallet must be present")
569
- end
745
+ def skip_wallet_required!
746
+ @skip_wallet_required_check = true
747
+ end
748
+
749
+ def reset_skip_wallet_required!
750
+ @skip_wallet_required_check = false
751
+ end
752
+
753
+ def check_wallet_required!
754
+ return if @wallet || @skip_wallet_required_check
755
+
756
+ raise ArgumentError, 'Wallet must be present'
570
757
  end
571
758
 
572
759
  def validate_wallet_contains_account!(account)
573
760
  @known_valid_accounts ||= []
574
761
  return if @known_valid_accounts.include?(account)
575
762
 
576
- if contains?(account)
577
- @known_valid_accounts << account
578
- else
579
- raise ArgumentError.new("Account does not exist in wallet. Account: #{account}, wallet: #{@wallet}")
763
+ unless contains?(account)
764
+ raise ArgumentError,
765
+ "Account does not exist in wallet. Account: #{account}, wallet: #{@wallet}"
580
766
  end
581
- end
582
767
 
768
+ @known_valid_accounts << account
769
+ end
583
770
  end
584
771
  end