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,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,65 +42,162 @@ 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
63
- #
64
- # Example response:
66
+ # Tells the node to send a keepalive packet to a specific IP address and port.
65
67
  #
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)
85
+ end
86
+
87
+ # Initialize lazy bootstrap with given block hash
88
+ #
89
+ # @param hash [String]
90
+ # @param force [Boolean] False by default. Manually force closing
91
+ # of all current bootstraps
92
+ # @return [Boolean] indicating if the action was successful
93
+ def bootstrap_lazy(hash, force: false)
94
+ rpc(:bootstrap_lazy, hash: hash, force: force, _access: :started) == 1
95
+ end
96
+
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
105
+ #
106
+ # ==== Example:
107
+ #
108
+ # node.confirmation_quorum
109
+ #
110
+ # Example response:
111
+ #
112
+ # {
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"
119
+ # }
120
+ #
121
+ # @return [Hash{Symbol=>String|Integer}]
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
137
+ end
138
+
139
+ # Returns the difficulty values (16 hexadecimal digits string, 64 bit)
140
+ # for the minimum required on the network (network_minimum) as well
141
+ # as the current active difficulty seen on the network (network_current,
142
+ # 5 minute trended average of adjusted difficulty seen on confirmed
143
+ # transactions) which can be used to perform rework for better
144
+ # prioritization of transaction processing. A multiplier of the
145
+ # network_current from the base difficulty of network_minimum is also
146
+ # provided for comparison.
147
+ #
148
+ # ==== Example:
149
+ #
150
+ # node.difficulty(include_trend: true)
151
+ #
152
+ # Example response:
153
+ #
154
+ # {
155
+ # network_minimum: "ffffffc000000000",
156
+ # network_current: "ffffffc1816766f2",
157
+ # multiplier: 1.024089858417128,
158
+ # difficulty_trend: [
159
+ # 1.156096135149775,
160
+ # 1.190133894573061,
161
+ # 1.135567138563921,
162
+ # 1.000000000000000,
163
+ # ]
164
+ # }
165
+ #
166
+ # @param include_trend [Boolean] false by default. Also returns the
167
+ # trend of difficulty seen on the network as a list of multipliers.
168
+ # Sampling occurs every 16 to 36 seconds. The list is ordered such
169
+ # that the first value is the most recent sample.
170
+ # @return [Hash{Symbol=>String|Float|Array}]
171
+ def difficulty(include_trend: false)
172
+ rpc(:active_difficulty, include_trend: include_trend, _coerce: Hash).tap do |response|
173
+ response[:multiplier] = response[:multiplier].to_f
174
+
175
+ response[:difficulty_trend].map!(&:to_f) if response.key?(:difficulty_trend)
176
+ end
91
177
  end
92
178
 
93
179
  # @return [String]
94
- def inspect
95
- "#{self.class.name}(object_id: \"#{"0x00%x" % (object_id << 1)}\")"
180
+ def to_s
181
+ self.class.name
96
182
  end
183
+ alias inspect to_s
97
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}}]
98
199
  def peers
99
- rpc(:peers)[:peers]
200
+ rpc(:peers, peer_details: true, _access: :peers, _coerce: Hash)
100
201
  end
101
202
 
102
203
  # All representatives and their voting weight.
@@ -108,59 +209,86 @@ class Nanook
108
209
  # Example response:
109
210
  #
110
211
  # {
111
- # xrb_1111111111111111111111111111111111111111111111111117353trpda: 3822372327060170000000000000000000000,
112
- # xrb_1111111111111111111111111111111111111111111111111awsq94gtecn: 30999999999999999999999999000000,
113
- # xrb_114nk4rwjctu6n6tr6g6ps61g1w3hdpjxfas4xj1tq6i8jyomc5d858xr1xi: 0
212
+ # Nanook::Account: 3822372327060170000000000000000000000,
213
+ # Nanook::Account: 30999999999999999999999999000000,
214
+ # Nanook::Account: 0
114
215
  # }
115
216
  #
116
- # @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
117
219
  def representatives(unit: Nanook.default_unit)
118
- unless Nanook::UNITS.include?(unit)
119
- raise ArgumentError.new("Unsupported unit: #{unit}")
120
- end
220
+ validate_unit!(unit)
121
221
 
122
- response = rpc(:representatives)[:representatives]
123
- return response if unit == :raw
222
+ response = rpc(:representatives, _access: :representatives, _coerce: Hash)
124
223
 
125
- r = response.map do |account_id, balance|
126
- balance = Nanook::Util.raw_to_NANO(balance)
224
+ r = response.map do |account_id, weight|
225
+ weight = raw_to_NANO(weight) if unit == :nano
127
226
 
128
- [account_id, balance]
227
+ [as_account(account_id), weight]
129
228
  end
130
229
 
131
- Hash[r].to_symbolized_hash
230
+ Hash[r]
132
231
  end
133
232
 
134
- # All online representatives that have voted recently. Note, due to the
135
- # design of the nano RPC, this method cannot return the voting weight
136
- # of the representatives.
233
+ # All online representatives that have voted recently and their weight.
137
234
  #
138
235
  # ==== Example:
139
236
  #
140
- # node.representatives_online # => ["xrb_111...", "xrb_222"]
237
+ # node.representatives_online # => [Nanook::Account, ...]
141
238
  #
142
- # @return [Array<String>] array of representative account ids
239
+ # @return [Nanook::Account] array of representative accounts
143
240
  def representatives_online
144
- response = 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)
145
254
  end
146
255
 
147
256
  # Safely shuts down the node.
148
257
  #
149
258
  # @return [Boolean] indicating if action was successful
150
259
  def stop
151
- rpc(:stop).has_key?(:success)
260
+ rpc(:stop).key?(:success)
152
261
  end
153
262
 
154
263
  # @param limit [Integer] number of synchronizing blocks to return
264
+ # @param unit (see Nanook::Account#balance)
265
+ #
155
266
  # @return [Hash{Symbol=>String}] information about the synchronizing blocks for this node
156
- def synchronizing_blocks(limit: 1000)
157
- response = rpc(:unchecked, count: limit)[:blocks]
158
- response = response.map do |block, info|
159
- [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]
160
287
  end
161
- Hash[response.sort].to_symbolized_hash
288
+
289
+ Hash[response]
162
290
  end
163
- alias_method :unchecked, :synchronizing_blocks
291
+ alias unchecked synchronizing_blocks
164
292
 
165
293
  # The percentage completeness of the synchronization process for
166
294
  # your node as it downloads the nano ledger. Note, it's normal for
@@ -171,35 +299,70 @@ class Nanook
171
299
  # @return [Float] the percentage completeness of the synchronization
172
300
  # process for your node
173
301
  def sync_progress
174
- response = rpc(:block_count)
302
+ response = rpc(:block_count, _coerce: Hash)
175
303
 
176
304
  count = response[:count]
177
305
  unchecked = response[:unchecked]
178
- total = count + unchecked
306
+ total = count + unchecked
179
307
 
180
308
  count.to_f * 100 / total.to_f
181
309
  end
182
310
 
183
- # This method is deprecated and will be removed in 3.0, as a node never
184
- # reaches 100% synchronization.
311
+ # Returns node uptime in seconds
312
+ #
313
+ # @return [Integer] seconds of uptime
314
+ def uptime
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+.
185
320
  #
186
- # @return [Boolean] signalling if this node ever reaches 100% synchronized
187
- def synced?
188
- warn "[DEPRECATION] `synced?` is deprecated and will be removed in 3.0"
189
- rpc(:block_count)[:unchecked] == 0
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)
335
+ end
336
+
337
+ # Returns receive minimum for wallets on the node.
338
+ #
339
+ # ==== Example:
340
+ #
341
+ # account.receive_minimum # => 0.01
342
+ #
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)
190
354
  end
191
355
 
192
356
  # @return [Hash{Symbol=>Integer|String}] version information for this node
193
357
  def version
194
- rpc(:version)
358
+ rpc(:version, _coerce: Hash)
195
359
  end
196
- alias_method :info, :version
360
+ alias info version
197
361
 
198
362
  private
199
363
 
200
- def rpc(action, params={})
364
+ def rpc(action, params = {})
201
365
  @rpc.call(action, params)
202
366
  end
203
-
204
367
  end
205
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