nanook 2.2.0 → 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,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