nanook 2.4.0 → 3.1.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,93 +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:
66
+ # Tells the node to send a keepalive packet to a specific IP address and port.
61
67
  #
62
- # node.block_count_by_type
63
- #
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)
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
91
95
  end
92
96
 
93
- # Returns block and tally weight (in raw) for recent elections winners
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
94
105
  #
95
106
  # ==== Example:
96
107
  #
97
- # node.confirmation_history
108
+ # node.confirmation_quorum
98
109
  #
99
110
  # Example response:
100
111
  #
101
- # [
102
- # {
103
- # block: "EA70B32C55C193345D625F766EEA2FCA52D3F2CCE0B3A30838CC543026BB0FEA",
104
- # tally: 80394786589602980996311817874549318248
105
- # },
106
- # {
107
- # block: "F2F8DA6D2CA0A4D78EB043A7A29E12BDE5B4CE7DE1B99A93A5210428EE5B8667",
108
- # tally: 68921714529890443063672782079965877749
109
- # }
110
- # ]
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
+ # }
111
120
  #
112
121
  # @return [Hash{Symbol=>String|Integer}]
113
- def confirmation_history
114
- rpc(:confirmation_history)[:confirmations].map do |history|
115
- # Rename hash key to block
116
- block = history.delete(:hash)
117
- {block: block}.merge(history)
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)
118
176
  end
119
177
  end
120
178
 
121
179
  # @return [String]
122
- def inspect
123
- "#{self.class.name}(object_id: \"#{"0x00%x" % (object_id << 1)}\")"
180
+ def to_s
181
+ self.class.name
124
182
  end
183
+ alias inspect to_s
125
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}}]
126
199
  def peers
127
- rpc(:peers)[:peers]
200
+ rpc(:peers, peer_details: true, _access: :peers, _coerce: Hash)
128
201
  end
129
202
 
130
203
  # All representatives and their voting weight.
@@ -136,59 +209,86 @@ class Nanook
136
209
  # Example response:
137
210
  #
138
211
  # {
139
- # xrb_1111111111111111111111111111111111111111111111111117353trpda: 3822372327060170000000000000000000000,
140
- # xrb_1111111111111111111111111111111111111111111111111awsq94gtecn: 30999999999999999999999999000000,
141
- # xrb_114nk4rwjctu6n6tr6g6ps61g1w3hdpjxfas4xj1tq6i8jyomc5d858xr1xi: 0
212
+ # Nanook::Account: 3822372327060170000000000000000000000,
213
+ # Nanook::Account: 30999999999999999999999999000000,
214
+ # Nanook::Account: 0
142
215
  # }
143
216
  #
144
- # @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
145
219
  def representatives(unit: Nanook.default_unit)
146
- unless Nanook::UNITS.include?(unit)
147
- raise ArgumentError.new("Unsupported unit: #{unit}")
148
- end
220
+ validate_unit!(unit)
149
221
 
150
- response = rpc(:representatives)[:representatives]
151
- return response if unit == :raw
222
+ response = rpc(:representatives, _access: :representatives, _coerce: Hash)
152
223
 
153
- r = response.map do |account_id, balance|
154
- balance = Nanook::Util.raw_to_NANO(balance)
224
+ r = response.map do |account_id, weight|
225
+ weight = raw_to_NANO(weight) if unit == :nano
155
226
 
156
- [account_id, balance]
227
+ [as_account(account_id), weight]
157
228
  end
158
229
 
159
- Hash[r].to_symbolized_hash
230
+ Hash[r]
160
231
  end
161
232
 
162
- # All online representatives that have voted recently. Note, due to the
163
- # design of the nano RPC, this method cannot return the voting weight
164
- # of the representatives.
233
+ # All online representatives that have voted recently and their weight.
165
234
  #
166
235
  # ==== Example:
167
236
  #
168
- # node.representatives_online # => ["xrb_111...", "xrb_222"]
237
+ # node.representatives_online # => [Nanook::Account, ...]
169
238
  #
170
- # @return [Array<String>] array of representative account ids
239
+ # @return [Nanook::Account] array of representative accounts
171
240
  def representatives_online
172
- 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)
173
254
  end
174
255
 
175
256
  # Safely shuts down the node.
176
257
  #
177
258
  # @return [Boolean] indicating if action was successful
178
259
  def stop
179
- rpc(:stop).has_key?(:success)
260
+ rpc(:stop).key?(:success)
180
261
  end
181
262
 
182
263
  # @param limit [Integer] number of synchronizing blocks to return
264
+ # @param unit (see Nanook::Account#balance)
265
+ #
183
266
  # @return [Hash{Symbol=>String}] information about the synchronizing blocks for this node
184
- def synchronizing_blocks(limit: 1000)
185
- response = rpc(:unchecked, count: limit)[:blocks]
186
- response = response.map do |block, info|
187
- [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]
188
287
  end
189
- Hash[response.sort].to_symbolized_hash
288
+
289
+ Hash[response]
190
290
  end
191
- alias_method :unchecked, :synchronizing_blocks
291
+ alias unchecked synchronizing_blocks
192
292
 
193
293
  # The percentage completeness of the synchronization process for
194
294
  # your node as it downloads the nano ledger. Note, it's normal for
@@ -199,35 +299,70 @@ class Nanook
199
299
  # @return [Float] the percentage completeness of the synchronization
200
300
  # process for your node
201
301
  def sync_progress
202
- response = rpc(:block_count)
302
+ response = rpc(:block_count, _coerce: Hash)
203
303
 
204
304
  count = response[:count]
205
305
  unchecked = response[:unchecked]
206
- total = count + unchecked
306
+ total = count + unchecked
207
307
 
208
308
  count.to_f * 100 / total.to_f
209
309
  end
210
310
 
211
- # This method is deprecated and will be removed in 3.0, as a node never
212
- # 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+.
320
+ #
321
+ # ==== Example:
322
+ #
323
+ # account.change_receive_minimum(0.01) # true
213
324
  #
214
- # @return [Boolean] signalling if this node ever reaches 100% synchronized
215
- def synced?
216
- warn "[DEPRECATION] `synced?` is deprecated and will be removed in 3.0"
217
- rpc(:block_count)[:unchecked] == 0
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)
218
354
  end
219
355
 
220
356
  # @return [Hash{Symbol=>Integer|String}] version information for this node
221
357
  def version
222
- rpc(:version)
358
+ rpc(:version, _coerce: Hash)
223
359
  end
224
- alias_method :info, :version
360
+ alias info version
225
361
 
226
362
  private
227
363
 
228
- def rpc(action, params={})
364
+ def rpc(action, params = {})
229
365
  @rpc.call(action, params)
230
366
  end
231
-
232
367
  end
233
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