nanook 2.5.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Nanook
4
+ Error = Class.new(StandardError)
5
+
6
+ ConnectionError = Class.new(Error)
7
+ NanoUnitError = Class.new(Error)
8
+ NodeRpcError = Class.new(Error)
9
+ NodeRpcConfigurationError = Class.new(NodeRpcError)
10
+ end
data/lib/nanook/node.rb CHANGED
@@ -1,5 +1,8 @@
1
- class Nanook
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'util'
2
4
 
5
+ class Nanook
3
6
  # The <tt>Nanook::Node</tt> class contains methods to manage your nano
4
7
  # node and query its data of the nano network.
5
8
  #
@@ -25,6 +28,7 @@ class Nanook
25
28
  # rpc_conn = Nanook::Rpc.new
26
29
  # node = Nanook::Node.new(rpc_conn)
27
30
  class Node
31
+ include Nanook::Util
28
32
 
29
33
  def initialize(rpc)
30
34
  @rpc = rpc
@@ -38,56 +42,46 @@ class Nanook
38
42
  #
39
43
  # @return [Integer] number of accounts with _open_ blocks.
40
44
  def account_count
41
- rpc(:frontier_count)[:count]
45
+ rpc(:frontier_count, _access: :count)
42
46
  end
43
- alias_method :frontier_count, :account_count
47
+ alias frontier_count account_count
44
48
 
45
49
  # The count of all blocks downloaded to the node, and
46
50
  # blocks still to be synchronized by the node.
47
51
  #
48
52
  # ==== Example:
49
53
  #
50
- #
54
+ # {
55
+ # count: 100,
56
+ # unchecked: 10,
57
+ # cemented: 25
58
+ # }
51
59
  #
52
60
  # @return [Hash{Symbol=>Integer}] number of blocks and unchecked
53
61
  # synchronizing blocks
54
62
  def block_count
55
- rpc(:block_count)
63
+ rpc(:block_count, _coerce: Hash)
56
64
  end
57
65
 
58
- # The count of all known blocks by their type.
59
- #
60
- # ==== Example:
61
- #
62
- # node.block_count_by_type
66
+ # Tells the node to send a keepalive packet to a specific IP address and port.
63
67
  #
64
- # Example response:
65
- #
66
- # {
67
- # send: 1000,
68
- # receive: 900,
69
- # open: 900,
70
- # change: 50
71
- # }
72
- #
73
- # @return [Hash{Symbol=>Integer}] number of blocks by type
74
- def block_count_by_type
75
- rpc(:block_count_type)
68
+ # @return [Boolean] indicating if the action was successful
69
+ def keepalive(address:, port:)
70
+ rpc(:keepalive, address: address, port: port).key?(:started)
76
71
  end
77
- alias_method :block_count_type, :block_count_by_type
78
72
 
79
73
  # Initialize bootstrap to a specific IP address and port.
80
74
  #
81
75
  # @return [Boolean] indicating if the action was successful
82
76
  def bootstrap(address:, port:)
83
- rpc(:bootstrap, address: address, port: port).has_key?(:success)
77
+ rpc(:bootstrap, address: address, port: port).key?(:success)
84
78
  end
85
79
 
86
80
  # Initialize multi-connection bootstrap to random peers
87
81
  #
88
82
  # @return [Boolean] indicating if the action was successful
89
83
  def bootstrap_any
90
- rpc(:bootstrap_any).has_key?(:success)
84
+ rpc(:bootstrap_any).key?(:success)
91
85
  end
92
86
 
93
87
  # Initialize lazy bootstrap with given block hash
@@ -97,79 +91,49 @@ class Nanook
97
91
  # of all current bootstraps
98
92
  # @return [Boolean] indicating if the action was successful
99
93
  def bootstrap_lazy(hash, force: false)
100
- rpc(:bootstrap_lazy, hash: hash, force: force)[:started] == 1
94
+ rpc(:bootstrap_lazy, hash: hash, force: force, _access: :started) == 1
101
95
  end
102
96
 
103
- # Returning status of current bootstrap attempt for debug purposes only.
104
- # This call is for internal diagnostics/debug purposes only.
105
- # Do not rely on this interface being stable and do not use in a
106
- # production system.
97
+ # Returns information about node elections settings and observed network state:
98
+ #
99
+ # - `quorum_delta`: delta tally required to rollback block
100
+ # - `online_weight_quorum_percent`: percentage of online weight for delta
101
+ # - `online_weight_minimum`: minimum online weight to confirm block
102
+ # - `online_stake_total`: currently observed online total weight
103
+ # - `peers_stake_total`: known peers total weight
104
+ # - `peers_stake_required`: effective stake needed from directly connected peers for quorum
107
105
  #
108
106
  # ==== Example:
109
107
  #
110
- # node.bootstrap_status
108
+ # node.confirmation_quorum
111
109
  #
112
110
  # Example response:
113
111
  #
114
112
  # {
115
- # clients: 5790,
116
- # pulls: 141065,
117
- # pulling: 3,
118
- # connections: 16,
119
- # idle: 0,
120
- # target_connections: 64,
121
- # total_blocks: 536820,
122
- # lazy_mode: true,
123
- # lazy_blocks: 423388,
124
- # lazy_state_unknown: 2,
125
- # lazy_balances: 0,
126
- # lazy_pulls: 0,
127
- # lazy_stopped: 644,
128
- # lazy_keys: 449,
129
- # lazy_key_1: "A86EB2B479AAF3CD531C8356A1FBE3CB500DFBF5BF292E5E6B8D1048DE199C32"
113
+ # "quorum_delta": 43216377.43025059,
114
+ # "online_weight_quorum_percent": 50,
115
+ # "online_weight_minimum": 60000000.0",
116
+ # "online_stake_total": 86432754.86050119,
117
+ # "peers_stake_total": 84672338.52479072,
118
+ # "peers_stake_required": 60000000.0"
130
119
  # }
131
120
  #
132
- # @return [Hash{Symbol=>String|Integer|Boolean}]
133
- def bootstrap_status
134
- rpc(:bootstrap_status)
135
- end
136
-
137
- # This call is for internal diagnostics/debug purposes only.
138
- # Do not rely on this interface being stable and do not use in a
139
- # production system.
140
- #
141
- # Returns block and tally weight (in raw) election duration (in
142
- # milliseconds), election confirmation timestamp for recent elections
143
- # winners.
144
- #
145
- # ==== Example:
146
- #
147
- # node.confirmation_history
148
- #
149
- # Example response:
150
- #
151
- # [
152
- # {
153
- # block: "EA70B32C55C193345D625F766EEA2FCA52D3F2CCE0B3A30838CC543026BB0FEA",
154
- # tally: 80394786589602980996311817874549318248,
155
- # duration: 4000,
156
- # time: 1544819986,
157
- # },
158
- # {
159
- # block: "F2F8DA6D2CA0A4D78EB043A7A29E12BDE5B4CE7DE1B99A93A5210428EE5B8667",
160
- # tally: 68921714529890443063672782079965877749,
161
- # duration: 6000,
162
- # time: 1544819988,
163
- # }
164
- # ]
165
- #
166
121
  # @return [Hash{Symbol=>String|Integer}]
167
- def confirmation_history
168
- rpc(:confirmation_history)[:confirmations].map do |history|
169
- # Rename hash key to block
170
- block = history.delete(:hash)
171
- {block: block}.merge(history)
172
- end
122
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
123
+ def confirmation_quorum(unit: Nanook.default_unit)
124
+ validate_unit!(unit)
125
+
126
+ response = rpc(:confirmation_quorum, _coerce: Hash)
127
+
128
+ return response unless unit == :nano
129
+
130
+ response[:quorum_delta] = raw_to_NANO(response[:quorum_delta])
131
+ response[:online_weight_minimum] = raw_to_NANO(response[:online_weight_minimum])
132
+ response[:online_stake_total] = raw_to_NANO(response[:online_stake_total])
133
+ response[:peers_stake_total] = raw_to_NANO(response[:peers_stake_total])
134
+ response[:peers_stake_required] = raw_to_NANO(response[:peers_stake_required])
135
+
136
+ response.compact
173
137
  end
174
138
 
175
139
  # Returns the difficulty values (16 hexadecimal digits string, 64 bit)
@@ -205,22 +169,35 @@ class Nanook
205
169
  # that the first value is the most recent sample.
206
170
  # @return [Hash{Symbol=>String|Float|Array}]
207
171
  def difficulty(include_trend: false)
208
- rpc(:active_difficulty, include_trend: include_trend).tap do |response|
172
+ rpc(:active_difficulty, include_trend: include_trend, _coerce: Hash).tap do |response|
209
173
  response[:multiplier] = response[:multiplier].to_f
210
174
 
211
- if response.key?(:difficulty_trend)
212
- response[:difficulty_trend].map!(&:to_f)
213
- end
175
+ response[:difficulty_trend].map!(&:to_f) if response.key?(:difficulty_trend)
214
176
  end
215
177
  end
216
178
 
217
179
  # @return [String]
218
- def inspect
219
- "#{self.class.name}(object_id: \"#{"0x00%x" % (object_id << 1)}\")"
180
+ def to_s
181
+ self.class.name
220
182
  end
183
+ alias inspect to_s
221
184
 
185
+ # Returns peers information.
186
+ #
187
+ # Example response:
188
+ #
189
+ # {
190
+ # :"[::ffff:104.131.102.132]:7075" => {
191
+ # protocol_version: 20,
192
+ # node_id: "node_1y7j5rdqhg99uyab1145gu3yur1ax35a3b6qr417yt8cd6n86uiw3d4whty3",
193
+ # type: "udp"
194
+ # },
195
+ # :"[::ffff:104.131.114.102]:7075" => { ... }
196
+ # }
197
+ #
198
+ # @return [Hash{Symbol=>Hash{Symbol=>Integer|String}}]
222
199
  def peers
223
- rpc(:peers)[:peers]
200
+ rpc(:peers, peer_details: true, _access: :peers, _coerce: Hash)
224
201
  end
225
202
 
226
203
  # All representatives and their voting weight.
@@ -232,59 +209,86 @@ class Nanook
232
209
  # Example response:
233
210
  #
234
211
  # {
235
- # nano_1111111111111111111111111111111111111111111111111117353trpda: 3822372327060170000000000000000000000,
236
- # nano_1111111111111111111111111111111111111111111111111awsq94gtecn: 30999999999999999999999999000000,
237
- # nano_114nk4rwjctu6n6tr6g6ps61g1w3hdpjxfas4xj1tq6i8jyomc5d858xr1xi: 0
212
+ # Nanook::Account: 3822372327060170000000000000000000000,
213
+ # Nanook::Account: 30999999999999999999999999000000,
214
+ # Nanook::Account: 0
238
215
  # }
239
216
  #
240
- # @return [Hash{Symbol=>Integer}] known representatives and their voting weight
217
+ # @return [Hash{Nanook::Account=>Float|Integer}] known representatives and their voting weight
218
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
241
219
  def representatives(unit: Nanook.default_unit)
242
- unless Nanook::UNITS.include?(unit)
243
- raise ArgumentError.new("Unsupported unit: #{unit}")
244
- end
220
+ validate_unit!(unit)
245
221
 
246
- response = rpc(:representatives)[:representatives]
247
- return response if unit == :raw
222
+ response = rpc(:representatives, _access: :representatives, _coerce: Hash)
248
223
 
249
- r = response.map do |account_id, balance|
250
- balance = Nanook::Util.raw_to_NANO(balance)
224
+ r = response.map do |account_id, weight|
225
+ weight = raw_to_NANO(weight) if unit == :nano
251
226
 
252
- [account_id, balance]
227
+ [as_account(account_id), weight]
253
228
  end
254
229
 
255
- Hash[r].to_symbolized_hash
230
+ Hash[r]
256
231
  end
257
232
 
258
- # All online representatives that have voted recently. Note, due to the
259
- # design of the nano RPC, this method cannot return the voting weight
260
- # of the representatives.
233
+ # All online representatives that have voted recently and their weight.
261
234
  #
262
235
  # ==== Example:
263
236
  #
264
- # node.representatives_online # => ["nano_111...", "nano_222"]
237
+ # node.representatives_online # => [Nanook::Account, ...]
265
238
  #
266
- # @return [Array<String>] array of representative account ids
239
+ # @return [Nanook::Account] array of representative accounts
267
240
  def representatives_online
268
- rpc(:representatives_online)[:representatives].keys.map(&:to_s)
241
+ rpc(:representatives_online, _access: :representatives, _coerce: Array).map do |representative|
242
+ as_account(representative)
243
+ end
244
+ end
245
+
246
+ # Tells the node to look for any account in all available wallets.
247
+ #
248
+ # ==== Example:
249
+ #
250
+ # node.search_pending #=> true
251
+ # @return [Boolean] indicates if the action was successful
252
+ def search_pending
253
+ rpc(:search_pending_all).key?(:success)
269
254
  end
270
255
 
271
256
  # Safely shuts down the node.
272
257
  #
273
258
  # @return [Boolean] indicating if action was successful
274
259
  def stop
275
- rpc(:stop).has_key?(:success)
260
+ rpc(:stop).key?(:success)
276
261
  end
277
262
 
278
263
  # @param limit [Integer] number of synchronizing blocks to return
264
+ # @param unit (see Nanook::Account#balance)
265
+ #
279
266
  # @return [Hash{Symbol=>String}] information about the synchronizing blocks for this node
280
- def synchronizing_blocks(limit: 1000)
281
- response = rpc(:unchecked, count: limit)[:blocks]
282
- response = response.map do |block, info|
283
- [block, JSON.parse(info).to_symbolized_hash]
267
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
268
+ def synchronizing_blocks(limit: 1000, unit: Nanook.default_unit)
269
+ validate_unit!(unit)
270
+
271
+ params = {
272
+ count: limit,
273
+ json_block: true,
274
+ _access: :blocks,
275
+ _coerce: Hash
276
+ }
277
+
278
+ response = rpc(:unchecked, params).map do |block, info|
279
+ info[:account] = as_account(info[:account]) if info[:account]
280
+ info[:link_as_account] = as_account(info[:link_as_account]) if info[:link_as_account]
281
+ info[:representative] = as_account(info[:representative]) if info[:representative]
282
+ info[:previous] = as_block(info[:previous]) if info[:previous]
283
+ info[:link] = as_block(info[:link]) if info[:link]
284
+ info[:balance] = raw_to_NANO(info[:balance]) if unit == :nano && info[:balance]
285
+
286
+ [as_block(block), info]
284
287
  end
285
- Hash[response.sort].to_symbolized_hash
288
+
289
+ Hash[response]
286
290
  end
287
- alias_method :unchecked, :synchronizing_blocks
291
+ alias unchecked synchronizing_blocks
288
292
 
289
293
  # The percentage completeness of the synchronization process for
290
294
  # your node as it downloads the nano ledger. Note, it's normal for
@@ -295,11 +299,11 @@ class Nanook
295
299
  # @return [Float] the percentage completeness of the synchronization
296
300
  # process for your node
297
301
  def sync_progress
298
- response = rpc(:block_count)
302
+ response = rpc(:block_count, _coerce: Hash)
299
303
 
300
304
  count = response[:count]
301
305
  unchecked = response[:unchecked]
302
- total = count + unchecked
306
+ total = count + unchecked
303
307
 
304
308
  count.to_f * 100 / total.to_f
305
309
  end
@@ -308,29 +312,57 @@ class Nanook
308
312
  #
309
313
  # @return [Integer] seconds of uptime
310
314
  def uptime
311
- rpc(:uptime)['seconds']
315
+ rpc(:uptime, _access: :seconds, _coerce: Hash)
316
+ end
317
+
318
+ # Sets the receive minimum for wallets on the node. The value is in +Nano+ by default.
319
+ # To specify an amount in +raw+, pass the argument +unit: :raw+.
320
+ #
321
+ # ==== Example:
322
+ #
323
+ # account.change_receive_minimum(0.01) # true
324
+ #
325
+ # @return [Boolean] true if the action was successful
326
+ # @param minimum Amount to set as the receive minimum
327
+ # @param unit optional. Specify +raw+ if you want to set the amount in +raw+. (See Nanook::Account#balance)
328
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
329
+ def change_receive_minimum(minimum, unit: Nanook.default_unit)
330
+ validate_unit!(unit)
331
+
332
+ minimum = NANO_to_raw(minimum) if unit == :nano
333
+
334
+ rpc(:receive_minimum_set, amount: minimum).key?(:success)
312
335
  end
313
336
 
314
- # This method is deprecated and will be removed in 3.0, as a node never
315
- # reaches 100% synchronization.
337
+ # Returns receive minimum for wallets on the node.
338
+ #
339
+ # ==== Example:
340
+ #
341
+ # account.receive_minimum # => 0.01
316
342
  #
317
- # @return [Boolean] signalling if this node ever reaches 100% synchronized
318
- def synced?
319
- warn "[DEPRECATION] `synced?` is deprecated and will be removed in 3.0"
320
- rpc(:block_count)[:unchecked] == 0
343
+ # @return [Integer|Float] the receive minimum
344
+ # @param unit (see Nanook::Account#balance)
345
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
346
+ def receive_minimum(unit: Nanook.default_unit)
347
+ validate_unit!(unit)
348
+
349
+ amount = rpc(:receive_minimum, _access: :amount)
350
+
351
+ return amount unless unit == :nano
352
+
353
+ raw_to_NANO(amount)
321
354
  end
322
355
 
323
356
  # @return [Hash{Symbol=>Integer|String}] version information for this node
324
357
  def version
325
- rpc(:version)
358
+ rpc(:version, _coerce: Hash)
326
359
  end
327
- alias_method :info, :version
360
+ alias info version
328
361
 
329
362
  private
330
363
 
331
- def rpc(action, params={})
364
+ def rpc(action, params = {})
332
365
  @rpc.call(action, params)
333
366
  end
334
-
335
367
  end
336
368
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'util'
4
+
5
+ class Nanook
6
+ # The <tt>Nanook::PrivateKey</tt> class lets you manage your node's keys.
7
+ class PrivateKey
8
+ include Nanook::Util
9
+
10
+ def initialize(rpc, key = nil)
11
+ @rpc = rpc
12
+ @key = key.to_s if key
13
+ end
14
+
15
+ def id
16
+ @key
17
+ end
18
+
19
+ # @param other [Nanook::PrivateKey] private 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
+ # Generate a new private public key pair. Returns the new {Nanook::PrivateKey}.
36
+ # The public key can be retrieved by calling `#public_key` on the private key.
37
+ #
38
+ # ==== Examples:
39
+ #
40
+ # private_key = nanook.private_key.create
41
+ # private_key.public_key # => Nanook::PublicKey pair for the private key
42
+ #
43
+ # deterministic_private_key = nanook.private_key.create(seed: seed, index: 0)
44
+ #
45
+ # @param seed [String] optional seed to generate a deterministic private key.
46
+ # @param index [Integer] optional (but required if +seed+ is given) index to generate a deterministic private key.
47
+ # @return Nanook::PrivateKey
48
+ def create(seed: nil, index: nil)
49
+ skip_key_required!
50
+
51
+ params = {
52
+ _access: :private,
53
+ _coerce: Hash
54
+ }
55
+
56
+ @key = if seed.nil?
57
+ rpc(:key_create, params)
58
+ else
59
+ raise ArgumentError, 'index argument is required when seed is given' if index.nil?
60
+
61
+ rpc(:deterministic_key, params.merge(seed: seed, index: index))
62
+ end
63
+
64
+ self
65
+ end
66
+
67
+ # Returns the {Nanook::Account} that matches this private key. The
68
+ # account may not exist yet in the ledger.
69
+ #
70
+ # @return Nanook::Account
71
+ def account
72
+ as_account(memoized_key_expand[:account])
73
+ end
74
+
75
+ # Returns the {Nanook::PublicKey} pair for this private key.
76
+ #
77
+ # @return Nanook::PublicKey
78
+ def public_key
79
+ as_public_key(memoized_key_expand[:public])
80
+ end
81
+
82
+ # @return [String]
83
+ def to_s
84
+ "#{self.class.name}(id: \"#{short_id}\")"
85
+ end
86
+ alias inspect to_s
87
+
88
+ private
89
+
90
+ def memoized_key_expand
91
+ @memoized_key_expand ||= rpc(:key_expand, _coerce: Hash)
92
+ end
93
+
94
+ def rpc(action, params = {})
95
+ check_key_required!
96
+
97
+ p = { key: @key }.compact
98
+ @rpc.call(action, p.merge(params)).tap { reset_skip_key_required! }
99
+ end
100
+
101
+ def skip_key_required!
102
+ @skip_key_required_check = true
103
+ end
104
+
105
+ def reset_skip_key_required!
106
+ @skip_key_required_check = false
107
+ end
108
+
109
+ def check_key_required!
110
+ return if @key || @skip_key_required_check
111
+
112
+ raise ArgumentError, 'Key must be present'
113
+ end
114
+ end
115
+ end