nanook 2.2.0 → 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,44 +1,93 @@
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.new("10")**BigDecimal.new("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)
16
- (BigDecimal.new(nano.to_s) * STEP).to_i
17
+ def NANO_to_raw(nano)
18
+ return if nano.nil?
19
+
20
+ (BigDecimal(nano.to_s) * STEP).to_i
17
21
  end
18
22
 
19
23
  # Converts an amount of raw to an amount of NANO.
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.2.0"
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
  #
@@ -18,7 +21,7 @@ class Nanook
18
21
  # person needs to know your wallet id as well as have access to run
19
22
  # RPC commands against your nano node to be able to control your accounts.
20
23
  #
21
- # A _seed_ on the otherhand can be used to link any wallet to another
24
+ # A _seed_ on the other hand can be used to link any wallet to another
22
25
  # wallet's accounts, from anywhere in the nano network. This happens
23
26
  # by setting a wallet's seed to be the same as a previous wallet's seed.
24
27
  # When a wallet has the same seed as another wallet, any accounts
@@ -33,8 +36,7 @@ class Nanook
33
36
  # want to restore the wallet anywhere else on the nano network besides
34
37
  # the node you originally created it on. The nano command line interface
35
38
  # (CLI) is the only method for discovering a wallet's seed. See the
36
- # {https://github.com/nanocurrency/raiblocks/wiki/Command-line-interface
37
- # --wallet_decrypt_unsafe CLI command}.
39
+ # {https://docs.nano.org/commands/command-line-interface/#-wallet_decrypt_unsafe-walletwallet-passwordpassword}.
38
40
  #
39
41
  # === Initializing
40
42
  #
@@ -48,10 +50,32 @@ class Nanook
48
50
  # rpc_conn = Nanook::Rpc.new
49
51
  # wallet = Nanook::Wallet.new(rpc_conn, wallet_id)
50
52
  class Wallet
53
+ include Nanook::Util
51
54
 
52
- def initialize(rpc, wallet)
55
+ def initialize(rpc, wallet = nil)
53
56
  @rpc = rpc
54
- @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
55
79
  end
56
80
 
57
81
  # Returns the given account in the wallet as a {Nanook::WalletAccount} instance
@@ -67,17 +91,18 @@ class Nanook
67
91
  #
68
92
  # ==== Examples:
69
93
  #
70
- # wallet.account("xrb_...") # => Nanook::WalletAccount
94
+ # wallet.account("nano_...") # => Nanook::WalletAccount
71
95
  # wallet.account.create # => Nanook::WalletAccount
72
96
  #
73
- # @param [String] account optional String of an account (starting with
97
+ # @param account [String] optional String of an account (starting with
74
98
  # <tt>"xrb..."</tt>) to start working with. Must be an account within
75
99
  # the wallet. When no account is given, the instance returned only
76
100
  # allows you to call +create+ on it, to create a new account.
77
- # @raise [ArgumentError] if the wallet does no contain the account
101
+ # @raise [ArgumentError] if the wallet does not contain the account
78
102
  # @return [Nanook::WalletAccount]
79
- def account(account=nil)
80
- Nanook::WalletAccount.new(@rpc, @wallet, account)
103
+ def account(account = nil)
104
+ check_wallet_required!
105
+ as_wallet_account(account)
81
106
  end
82
107
 
83
108
  # Array of {Nanook::WalletAccount} instances of accounts in the wallet.
@@ -91,13 +116,33 @@ class Nanook
91
116
  #
92
117
  # @return [Array<Nanook::WalletAccount>] all accounts in the wallet
93
118
  def accounts
94
- wallet_required!
95
- response = rpc(:account_list)[:accounts]
96
- Nanook::Util.coerce_empty_string_to_type(response, Array).map do |account|
97
- Nanook::WalletAccount.new(@rpc, @wallet, account)
119
+ rpc(:account_list, _access: :accounts, _coerce: Array).map do |account|
120
+ as_wallet_account(account)
98
121
  end
99
122
  end
100
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
+
101
146
  # Balance of all accounts in the wallet, optionally breaking the balances down by account.
102
147
  #
103
148
  # ==== Examples:
@@ -128,58 +173,60 @@ class Nanook
128
173
  # Example response:
129
174
  #
130
175
  # {
131
- # "xrb_3e3j5tkog48pnny9dmfzj1r16pg8t1e76dz5tmac6iq689wyjfpi00000000"=>{
176
+ # "nano_3e3j5tkog48pnny9dmfzj1r16pg8t1e76dz5tmac6iq689wyjfpi00000000"=>{
132
177
  # "balance"=>2.5,
133
178
  # "pending"=>1
134
179
  # },
135
- # "xrb_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx"=>{
180
+ # "nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx"=>{
136
181
  # "balance"=>51.4,
137
182
  # "pending"=>0
138
183
  # },
139
184
  # }
140
185
  #
141
- # @param [Boolean] account_break_down (default is +false+). When +true+
186
+ # @param account_break_down [Boolean] (default is +false+). When +true+
142
187
  # the response will contain balances per account.
143
188
  # @param unit (see Nanook::Account#balance)
144
189
  #
145
190
  # @return [Hash{Symbol=>Integer|Float|Hash}]
191
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
146
192
  def balance(account_break_down: false, unit: Nanook.default_unit)
147
- wallet_required!
148
-
149
- unless Nanook::UNITS.include?(unit)
150
- raise ArgumentError.new("Unsupported unit: #{unit}")
151
- end
193
+ validate_unit!(unit)
152
194
 
153
195
  if account_break_down
154
- 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|
155
197
  if unit == :nano
156
- r.each do |account, balances|
157
- r[account][:balance] = Nanook::Util.raw_to_NANO(r[account][:balance])
158
- 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])
159
201
  end
160
202
  end
161
203
  end
162
204
  end
163
205
 
164
- rpc(:wallet_balance_total).tap do |r|
165
- if unit == :nano
166
- r[:balance] = Nanook::Util.raw_to_NANO(r[:balance])
167
- r[:pending] = Nanook::Util.raw_to_NANO(r[:pending])
168
- end
169
- 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
+ }
170
213
  end
171
214
 
172
215
  # Changes a wallet's seed.
173
216
  #
217
+ # It's recommended to only change the seed of a wallet that contains
218
+ # no accounts. This will clear all deterministic accounts in the wallet.
219
+ # To restore accounts after changing the seed, see Nanook::WalletAccount#create.
220
+ #
174
221
  # ==== Example:
175
222
  #
176
223
  # wallet.change_seed("000D1BA...") # => true
224
+ # wallet.account.create(5) # Restores first 5 accounts for wallet with new seed
177
225
  #
178
226
  # @param seed [String] the seed to change to.
179
227
  # @return [Boolean] indicating whether the change was successful.
180
228
  def change_seed(seed)
181
- wallet_required!
182
- rpc(:wallet_change_seed, seed: seed).has_key?(:success)
229
+ rpc(:wallet_change_seed, seed: seed).key?(:success)
183
230
  end
184
231
 
185
232
  # Creates a new wallet.
@@ -197,7 +244,8 @@ class Nanook
197
244
  #
198
245
  # @return [Nanook::Wallet]
199
246
  def create
200
- @wallet = rpc(:wallet_create)[:wallet]
247
+ skip_wallet_required!
248
+ @wallet = rpc(:wallet_create, _access: :wallet)
201
249
  self
202
250
  end
203
251
 
@@ -209,49 +257,57 @@ class Nanook
209
257
  #
210
258
  # @return [Boolean] indicating success of the action
211
259
  def destroy
212
- wallet_required!
213
- rpc(:wallet_destroy)
214
- true
260
+ rpc(:wallet_destroy, _access: :destroyed) == 1
215
261
  end
216
262
 
217
263
  # Generates a String containing a JSON representation of your wallet.
218
264
  #
219
265
  # ==== Example:
220
266
  #
221
- # 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]
222
271
  def export
223
- wallet_required!
224
- 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
225
287
  end
226
288
 
227
289
  # Will return +true+ if the account exists in the wallet.
228
290
  #
229
291
  # ==== Example:
230
- # wallet.contains?("xrb_...") # => true
292
+ # wallet.contains?("nano_...") # => true
231
293
  #
232
- # @param account [String] id (will start with <tt>"xrb_..."</tt>)
294
+ # @param account [String] id (will start with <tt>"nano_..."</tt>)
233
295
  # @return [Boolean] indicating if the wallet contains the given account
234
296
  def contains?(account)
235
- wallet_required!
236
- response = rpc(:wallet_contains, account: account)
237
- !response.empty? && response[:exists] == 1
238
- end
239
-
240
- # @return [String] the wallet id
241
- def id
242
- @wallet
297
+ rpc(:wallet_contains, account: account, _access: :exists) == 1
243
298
  end
244
299
 
245
300
  # @return [String]
246
- def inspect
247
- "#{self.class.name}(id: \"#{id}\", object_id: \"#{"0x00%x" % (object_id << 1)}\")"
301
+ def to_s
302
+ "#{self.class.name}(id: \"#{short_id}\")"
248
303
  end
304
+ alias inspect to_s
249
305
 
250
306
  # Makes a payment from an account in your wallet to another account
251
307
  # on the nano network.
252
308
  #
253
309
  # Note, there may be a delay in receiving a response due to Proof of
254
- # Work being done. From the {Nano RPC}[https://github.com/nanocurrency/raiblocks/wiki/RPC-protocol#account-create]:
310
+ # Work being done. From the {Nano RPC}[https://docs.nano.org/commands/rpc-protocol/#send]:
255
311
  #
256
312
  # <i>Proof of Work is precomputed for one transaction in the
257
313
  # background. If it has been a while since your last transaction it
@@ -260,8 +316,9 @@ class Nanook
260
316
  #
261
317
  # ==== Examples:
262
318
  #
263
- # wallet.pay(from: "xrb_...", to: "xrb_...", amount: 1.1, id: "myUniqueId123") # => "9AE2311..."
264
- # wallet.pay(from: "xrb_...", to: "xrb_...", amount: 54000000000000, unit: :raw, id: "myUniqueId123") # => "9AE2311..."
319
+ # wallet.pay(from: "nano_...", to: "nano_...", amount: 1.1, id: "myUniqueId123") # => "9AE2311..."
320
+ # wallet.pay(from: "nano_...", to: "nano_...", amount: 54000000000000, unit: :raw, id: "myUniqueId123")
321
+ # # => "9AE2311..."
265
322
  #
266
323
  # @param from [String] account id of an account in your wallet
267
324
  # @param to (see Nanook::WalletAccount#pay)
@@ -270,8 +327,7 @@ class Nanook
270
327
  # @params id (see Nanook::WalletAccount#pay)
271
328
  # @return (see Nanook::WalletAccount#pay)
272
329
  # @raise [Nanook::Error] if unsuccessful
273
- def pay(from:, to:, amount:, unit: Nanook.default_unit, id:)
274
- wallet_required!
330
+ def pay(from:, to:, amount:, id:, unit: Nanook.default_unit)
275
331
  validate_wallet_contains_account!(from)
276
332
  account(from).pay(to: to, amount: amount, unit: unit, id: id)
277
333
  end
@@ -292,12 +348,12 @@ class Nanook
292
348
  # Example response:
293
349
  #
294
350
  # {
295
- # :xrb_1111111111111111111111111111111111111111111111111117353trpda=>[
296
- # "142A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D",
297
- # "718CC2121C3E641059BC1C2CFC45666C99E8AE922F7A807B7D07B62C995D79E2"
351
+ # Nanook::Account=>[
352
+ # Nanook::Block,
353
+ # Nanook::Block"
298
354
  # ],
299
- # :xrb_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3=>[
300
- # "4C1FEEF0BEA7F50BE35489A1233FE002B212DEA554B55B1B470D78BD8F210C74"
355
+ # Nanook::Account=>[
356
+ # Nanook::Block
301
357
  # ]
302
358
  # }
303
359
  #
@@ -308,57 +364,69 @@ class Nanook
308
364
  # Example response:
309
365
  #
310
366
  # {
311
- # :xrb_1111111111111111111111111111111111111111111111111117353trpda=>[
367
+ # Nanook::Account=>[
312
368
  # {
313
369
  # :amount=>6.0,
314
- # :source=>"xrb_3dcfozsmekr1tr9skf1oa5wbgmxt81qepfdnt7zicq5x3hk65fg4fqj58mbr",
315
- # :block=>:"142A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D"
370
+ # :source=>Nanook::Account,
371
+ # :block=>Nanook::Block
316
372
  # },
317
373
  # {
318
374
  # :amount=>12.0,
319
- # :source=>"xrb_3dcfozsmekr1tr9skf1oa5wbgmxt81qepfdnt7zicq5x3hk65fg4fqj58mbr",
320
- # :block=>:"242A538F36833D1CC78B94E11C766F75818F8B940771335C6C1B8AB880C5BB1D"
375
+ # :source=>Nanook::Account,
376
+ # :block=>Nanook::Block
321
377
  # }
322
378
  # ],
323
- # :xrb_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3=>[
379
+ # Nanook::Account=>[
324
380
  # {
325
381
  # :amount=>106.370018,
326
- # :source=>"xrb_13ezf4od79h1tgj9aiu4djzcmmguendtjfuhwfukhuucboua8cpoihmh8byo",
327
- # :block=>:"4C1FEEF0BEA7F50BE35489A1233FE002B212DEA554B55B1B470D78BD8F210C74"
382
+ # :source=>Nanook::Account,
383
+ # :block=>Nanook::Block
328
384
  # }
329
385
  # ]
330
386
  # }
331
- def pending(limit:1000, detailed:false, unit:Nanook.default_unit)
332
- 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)
333
391
 
334
- unless Nanook::UNITS.include?(unit)
335
- raise ArgumentError.new("Unsupported unit: #{unit}")
336
- end
392
+ params = {
393
+ count: limit,
394
+ _access: :blocks,
395
+ _coerce: Hash
396
+ }
337
397
 
338
- params = { count: limit }
339
398
  params[:source] = true if detailed
340
399
 
341
- response = rpc(:wallet_pending, params)[:blocks]
342
- response = Nanook::Util.coerce_empty_string_to_type(response, Hash)
400
+ response = rpc(:wallet_pending, params)
343
401
 
344
- 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
345
411
 
346
412
  # Map the RPC response, which is:
347
413
  # account=>block=>[amount|source] into
348
414
  # account=>[block|amount|source]
349
415
  x = response.map do |account, data|
350
416
  new_data = data.map do |block, amount_and_source|
351
- d = amount_and_source.merge(block: block.to_s)
352
- if unit == :nano
353
- d[:amount] = Nanook::Util.raw_to_NANO(d[:amount])
354
- 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
355
423
  d
356
424
  end
357
425
 
358
- [account, new_data]
426
+ [as_account(account), new_data]
359
427
  end
360
428
 
361
- Hash[x].to_symbolized_hash
429
+ Hash[x]
362
430
  end
363
431
 
364
432
  # Receives a pending payment into an account in the wallet.
@@ -366,7 +434,7 @@ class Nanook
366
434
  # When called with no +block+ argument, the latest pending payment
367
435
  # for the account will be received.
368
436
  #
369
- # 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,
370
438
  # or +false+ if there were no pending payments to receive.
371
439
  #
372
440
  # You can receive a specific pending block if you know it by
@@ -374,19 +442,76 @@ class Nanook
374
442
  #
375
443
  # ==== Examples:
376
444
  #
377
- # wallet.receive(into: "xrb...") # => "9AE2311..."
378
- # wallet.receive("718CC21...", into: "xrb...") # => "9AE2311..."
445
+ # wallet.receive(into: "xrb...") # => Nanook::Block
446
+ # wallet.receive("718CC21...", into: "xrb...") # => Nanook::Block
379
447
  #
380
448
  # @param block (see Nanook::WalletAccount#receive)
381
449
  # @param into [String] account id of account in your wallet to receive the
382
450
  # payment into
383
451
  # @return (see Nanook::WalletAccount#receive)
384
- def receive(block=nil, into:)
385
- wallet_required!
452
+ def receive(block = nil, into:)
386
453
  validate_wallet_contains_account!(into)
387
454
  account(into).receive(block)
388
455
  end
389
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
+
472
+ # The default representative account id for the wallet. This is the
473
+ # representative that all new accounts created in this wallet will have.
474
+ #
475
+ # Changing the default representative for a wallet does not change
476
+ # the representatives for any accounts that have been created.
477
+ #
478
+ # ==== Example:
479
+ #
480
+ # wallet.default_representative # => "nano_3pc..."
481
+ #
482
+ # @return [Nanook::Account] Representative account. Can be nil.
483
+ def default_representative
484
+ representative = rpc(:wallet_representative, _access: :representative)
485
+ as_account(representative) if representative
486
+ end
487
+ alias representative default_representative
488
+
489
+ # Sets the default representative for the wallet. A wallet's default
490
+ # representative is the representative all new accounts created in
491
+ # the wallet will have. Changing the default representative for a
492
+ # wallet does not change the representatives for existing accounts
493
+ # in the wallet.
494
+ #
495
+ # ==== Example:
496
+ #
497
+ # wallet.change_default_representative("nano_...") # => "nano_..."
498
+ #
499
+ # @param representative [String] id of the representative account
500
+ # to set as this account's representative
501
+ # @return [Nanook::Account] the representative account
502
+ # @raise [Nanook::Error] if setting the representative fails
503
+ def change_default_representative(representative)
504
+ unless as_account(representative).exists?
505
+ raise Nanook::Error, "Representative account does not exist: #{representative}"
506
+ end
507
+
508
+ raise Nanook::Error, 'Setting the representative failed' \
509
+ unless rpc(:wallet_representative_set, representative: representative, _access: :set) == 1
510
+
511
+ as_account(representative)
512
+ end
513
+ alias change_representative change_default_representative
514
+
390
515
  # Restores a previously created wallet by its seed.
391
516
  # A new wallet will be created on your node (with a new wallet id)
392
517
  # and will have its seed set to the given seed.
@@ -400,18 +525,149 @@ class Nanook
400
525
  #
401
526
  # @return [Nanook::Wallet] a new wallet
402
527
  # @raise [Nanook::Error] if unsuccessful
403
- def restore(seed, accounts:0)
528
+ def restore(seed, accounts: 0)
529
+ skip_wallet_required!
530
+
404
531
  create
405
532
 
406
- unless change_seed(seed)
407
- raise Nanook::Error.new("Unable to set seed for wallet")
533
+ raise Nanook::Error, 'Unable to set seed for wallet' unless change_seed(seed)
534
+
535
+ account.create(accounts) if accounts.positive?
536
+
537
+ self
538
+ end
539
+
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]
408
580
  end
409
581
 
410
- if accounts > 0
411
- account.create(accounts)
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.
590
+ #
591
+ # ==== Examples:
592
+ #
593
+ # wallet.info
594
+ #
595
+ # Example response:
596
+ #
597
+ # {
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
+ # }
605
+ #
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
609
+ def info(unit: Nanook.default_unit)
610
+ validate_unit!(unit)
611
+
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])
412
617
  end
413
618
 
414
- self
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
660
+ end
661
+
662
+ # Locks the wallet. A locked wallet cannot pocket pending transactions or make payments. See {#unlock}.
663
+ #
664
+ # ==== Example:
665
+ #
666
+ # wallet.lock #=> true
667
+ #
668
+ # @return [Boolean] indicates if the wallet was successfully locked
669
+ def lock
670
+ rpc(:wallet_lock, _access: :locked) == 1
415
671
  end
416
672
 
417
673
  # Returns +true+ if the wallet is locked.
@@ -422,9 +678,7 @@ class Nanook
422
678
  #
423
679
  # @return [Boolean] indicates if the wallet is locked
424
680
  def locked?
425
- wallet_required!
426
- response = rpc(:wallet_locked)
427
- !response.empty? && response[:locked] != 0
681
+ rpc(:wallet_locked, _access: :locked) == 1
428
682
  end
429
683
 
430
684
  # Unlocks a previously locked wallet.
@@ -434,9 +688,8 @@ class Nanook
434
688
  # wallet.unlock("new_pass") #=> true
435
689
  #
436
690
  # @return [Boolean] indicates if the unlocking action was successful
437
- def unlock(password)
438
- wallet_required!
439
- rpc(:password_enter, password: password)[:valid] == 1
691
+ def unlock(password = nil)
692
+ rpc(:password_enter, password: password, _access: :valid) == 1
440
693
  end
441
694
 
442
695
  # Changes the password for a wallet.
@@ -446,33 +699,73 @@ class Nanook
446
699
  # wallet.change_password("new_pass") #=> true
447
700
  # @return [Boolean] indicates if the action was successful
448
701
  def change_password(password)
449
- wallet_required!
450
- 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]
451
734
  end
452
735
 
453
736
  private
454
737
 
455
- def rpc(action, params={})
456
- p = @wallet.nil? ? {} : { wallet: @wallet }
457
- @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! }
458
743
  end
459
744
 
460
- def wallet_required!
461
- if @wallet.nil?
462
- raise ArgumentError.new("Wallet must be present")
463
- 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'
464
757
  end
465
758
 
466
759
  def validate_wallet_contains_account!(account)
467
760
  @known_valid_accounts ||= []
468
761
  return if @known_valid_accounts.include?(account)
469
762
 
470
- if contains?(account)
471
- @known_valid_accounts << account
472
- else
473
- 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}"
474
766
  end
475
- end
476
767
 
768
+ @known_valid_accounts << account
769
+ end
477
770
  end
478
771
  end