nanook 2.5.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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