nanook 2.5.1 → 3.0.0

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