nanook 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/nanook/block.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::Block</tt> class contains methods to discover
4
7
  # publicly-available information about blocks on the nano network.
5
8
  #
@@ -17,21 +20,38 @@ class Nanook
17
20
  # rpc_conn = Nanook::Rpc.new
18
21
  # block = Nanook::Block.new(rpc_conn, "FBF8B0E...")
19
22
  class Block
23
+ include Nanook::Util
20
24
 
21
25
  def initialize(rpc, block)
22
26
  @rpc = rpc
23
- @block = block
24
- block_required! # All methods expect a block
27
+ @block = block.to_s
25
28
  end
26
29
 
27
- # Returns the {Nanook::Account} of the block.
30
+ # Returns the block hash id.
28
31
  #
29
32
  # ==== Example:
30
- # block.account # => Nanook::Account
31
33
  #
32
- # @return [Nanook::Account] the account of the block
33
- def account
34
- Nanook::Account.new(@rpc, rpc(:block_account, :hash)[:account])
34
+ # block.id #=> "FBF8B0E..."
35
+ #
36
+ # @return [String] the block hash id
37
+ def id
38
+ @block
39
+ end
40
+
41
+ # @param other [Nanook::Block] block to compare
42
+ # @return [Boolean] true if blocks are equal
43
+ def ==(other)
44
+ other.class == self.class &&
45
+ other.id == id
46
+ end
47
+ alias eql? ==
48
+
49
+ # The hash value is used along with #eql? by the Hash class to determine if two objects
50
+ # reference the same hash key.
51
+ #
52
+ # @return [Integer]
53
+ def hash
54
+ id.hash
35
55
  end
36
56
 
37
57
  # Stop generating work for a block.
@@ -45,8 +65,11 @@ class Nanook
45
65
  rpc(:work_cancel, :hash).empty?
46
66
  end
47
67
 
48
- # Returns an Array of block hashes in the account chain starting at
49
- # this block.
68
+ # Returns a consecutive list of block hashes in the account chain
69
+ # starting at block back to count (direction from frontier back to
70
+ # open block, from newer blocks to older). Will list all blocks back
71
+ # to the open block of this chain when count is set to "-1".
72
+ # The requested block hash is included in the answer.
50
73
  #
51
74
  # See also #successors.
52
75
  #
@@ -56,59 +79,48 @@ class Nanook
56
79
  #
57
80
  # ==== Example reponse:
58
81
  #
59
- # [
60
- # "36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E",
61
- # "FBF8B0E6623A31AB528EBD839EEAA91CAFD25C12294C46754E45FD017F7939EB"
62
- # ]
82
+ # [Nanook::Block, ...]
83
+ #
63
84
  # @param limit [Integer] maximum number of block hashes to return (default is 1000)
64
- def chain(limit: 1000)
65
- response = rpc(:chain, :block, count: limit)[:blocks]
66
- Nanook::Util.coerce_empty_string_to_type(response, Array)
67
- end
85
+ # @param offset [Integer] return the account chain block hashes offset by the specified number of blocks (default is 0)
86
+ def chain(limit: 1000, offset: 0)
87
+ params = {
88
+ count: limit,
89
+ offset: offset,
90
+ _access: :blocks,
91
+ _coerce: Array
92
+ }
68
93
 
69
- # Generate work for a block.
70
- #
71
- # ==== Example:
72
- # block.generate_work # => "2bf29ef00786a6bc"
73
- #
74
- # @return [String] the work id of the work completed.
75
- def generate_work
76
- rpc(:work_generate, :hash)[:work]
94
+ rpc(:chain, :block, params).map do |block|
95
+ as_block(block)
96
+ end
77
97
  end
98
+ alias ancestors chain
78
99
 
79
- # Returns Array of Hashes containing information about a chain of
80
- # send/receive blocks, starting from this block.
100
+ # Request confirmation for a block from online representative nodes.
101
+ # Will return immediately with a boolean to indicate if the request for
102
+ # confirmation was successful. Note that this boolean does not indicate
103
+ # the confirmation status of the block.
81
104
  #
82
105
  # ==== Example:
106
+ # block.confirm # => true
83
107
  #
84
- # block.history(limit: 1)
85
- #
86
- # ==== Example response:
87
- #
88
- # [
89
- # {
90
- # :account=>"xrb_3x7cjioqahgs5ppheys6prpqtb4rdknked83chf97bot1unrbdkaux37t31b",
91
- # :amount=>539834279601145558517940224,
92
- # :hash=>"36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E",
93
- # :type=>"send"
94
- # }
95
- # ]
96
- #
97
- # @param limit [Integer] maximum number of send/receive block hashes
98
- # to return in the chain (default is 1000)
99
- def history(limit: 1000)
100
- rpc(:history, :hash, count: limit)[:history]
108
+ # @return [Boolean] if the confirmation request was sent successful
109
+ def confirm
110
+ rpc(:block_confirm, :hash, _access: :started) == 1
101
111
  end
102
112
 
103
- # Returns the block hash id.
113
+ # Generate work for a block.
104
114
  #
105
115
  # ==== Example:
116
+ # block.generate_work # => "2bf29ef00786a6bc"
106
117
  #
107
- # block.id #=> "FBF8B0E..."
108
- #
109
- # @return [String] the block hash id
110
- def id
111
- @block
118
+ # @param use_peers [Boolean] if set to +true+, then the node will query
119
+ # its work peers (if it has any, see {Nanook::WorkPeer#list}).
120
+ # When +false+, the node will only generate work locally (default is +false+)
121
+ # @return [String] the work id of the work completed.
122
+ def generate_work(use_peers: false)
123
+ rpc(:work_generate, :hash, use_peers: use_peers, _access: :work)
112
124
  end
113
125
 
114
126
  # Returns a Hash of information about the block.
@@ -121,65 +133,84 @@ class Nanook
121
133
  # ==== Example response:
122
134
  #
123
135
  # {
124
- # :id=>"36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E",
125
- # :type=>"send",
126
- # :previous=>"FBF8B0E6623A31AB528EBD839EEAA91CAFD25C12294C46754E45FD017F7939EB",
127
- # :destination=>"xrb_3x7cjioqahgs5ppheys6prpqtb4rdknked83chf97bot1unrbdkaux37t31b",
128
- # :balance=>"00000000000000000000000000000000",
129
- # :work=>"44cc24b60705083a",
130
- # :signature=>"42ADFEFE7C3FFF188AE92A202F8A5734DE91779C454613E446EEC93D001D6C953E9FD16730AF32C891791BA8EDAECEB059A213E2FE1EEB7ADF9D5D0815464D06"
136
+ # "account": Nanook::Account,
137
+ # "amount": 34.2,
138
+ # "balance": 2.3
139
+ # "height": 58,
140
+ # "local_timestamp": Time,
141
+ # "confirmed": true,
142
+ # "type": "send",
143
+ # "account": Nanook::Account,
144
+ # "previous": Nanook::Block,
145
+ # "representative": Nanook::Account,
146
+ # "link": Nanook::Block,
147
+ # "link_as_account": Nanook::Account,
148
+ # "signature": "82D41BC16F313E4B2243D14DFFA2FB04679C540C2095FEE7EAE0F2F26880AD56DD48D87A7CC5DD760C5B2D76EE2C205506AA557BF00B60D8DEE312EC7343A501",
149
+ # "work": "8a142e07a10996d5"
131
150
  # }
132
151
  #
133
152
  # @param allow_unchecked [Boolean] (default is +false+). If +true+,
134
153
  # information can be returned about blocks that are unchecked (unverified).
135
- def info(allow_unchecked: false)
136
- if allow_unchecked
137
- response = rpc(:unchecked_get, :hash)
138
- unless response.has_key?(:error)
139
- return _parse_info_response(response)
140
- end
141
- # If unchecked not found, continue to checked block
154
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
155
+ # @raise [Nanook::NodeRpcError] if block is not found on the node.
156
+ def info(allow_unchecked: false, unit: Nanook.default_unit)
157
+ validate_unit!(unit)
158
+
159
+ # Params for both `unchecked_get` and `block_info` calls
160
+ params = {
161
+ json_block: true,
162
+ _coerce: Hash
163
+ }
164
+
165
+ begin
166
+ response = rpc(:block_info, :hash, params)
167
+ response.merge!(confirmed: true)
168
+ rescue Nanook::NodeRpcError => e
169
+ raise e unless allow_unchecked
170
+
171
+ response = rpc(:unchecked_get, :hash, params)
172
+ response.merge!(confirmed: false)
142
173
  end
143
174
 
144
- response = rpc(:block, :hash)
145
- _parse_info_response(response)
175
+ parse_info_response(response, unit)
146
176
  end
147
177
 
148
178
  # ==== Example:
149
179
  #
150
- # block.is_valid_work?("2bf29ef00786a6bc") # => true
180
+ # block.valid_work?("2bf29ef00786a6bc") # => true
151
181
  #
152
182
  # @param work [String] the work id to check is valid
153
183
  # @return [Boolean] signalling if work is valid for the block
154
- def is_valid_work?(work)
184
+ def valid_work?(work)
155
185
  response = rpc(:work_validate, :hash, work: work)
156
- !response.empty? && response[:valid] == 1
186
+ response[:valid_all] == 1 || response[:valid_receive] == 1
157
187
  end
158
188
 
159
189
  # Republish blocks starting at this block up the account chain
160
190
  # back to the nano network.
161
191
  #
162
- # @return [Array<String>] block hashes that were republished
192
+ # @return [Array<Nanook::Block>] blocks that were republished
163
193
  #
164
194
  # ==== Example:
165
195
  #
166
- # block.republish
167
- #
168
- # ==== Example response:
169
- #
170
- # ["36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E"]
171
- def republish(destinations:nil, sources:nil)
196
+ # block.republish # => [Nanook::Block, ...]
197
+ def republish(destinations: nil, sources: nil)
172
198
  if !destinations.nil? && !sources.nil?
173
- raise ArgumentError.new("You must provide either destinations or sources but not both")
199
+ raise ArgumentError, 'You must provide either destinations or sources but not both'
174
200
  end
175
201
 
176
- # Add in optional arguments
177
- params = {}
202
+ params = {
203
+ _access: :blocks,
204
+ _coerce: Array
205
+ }
206
+
178
207
  params[:destinations] = destinations unless destinations.nil?
179
208
  params[:sources] = sources unless sources.nil?
180
- params[:count] = 1 unless params.empty?
209
+ params[:count] = 1 if destinations || sources
181
210
 
182
- rpc(:republish, :hash, params)[:blocks]
211
+ rpc(:republish, :hash, params).map do |block|
212
+ as_block(block)
213
+ end
183
214
  end
184
215
 
185
216
  # ==== Example:
@@ -188,74 +219,287 @@ class Nanook
188
219
  #
189
220
  # @return [Boolean] signalling if the block is a pending block.
190
221
  def pending?
191
- response = rpc(:pending_exists, :hash)
192
- !response.empty? && response[:exists] == 1
222
+ rpc(:pending_exists, :hash, _access: :exists) == 1
193
223
  end
194
224
 
195
- # Publish the block to the nano network.
225
+ # Returns an Array of block hashes in the account chain ending at
226
+ # this block.
196
227
  #
197
- # Note, if block has previously been published, use #republish instead.
228
+ # See also #chain.
198
229
  #
199
- # ==== Examples:
230
+ # ==== Example:
200
231
  #
201
- # block.publish # => "FBF8B0E..."
232
+ # block.successors # => [Nanook::Block, .. ]
202
233
  #
203
- # @return [String] the block hash, or false.
204
- def publish
205
- rpc(:process, :block)[:hash] || false
234
+ # @param limit [Integer] maximum number of send/receive block hashes
235
+ # to return in the chain (default is 1000)
236
+ # @param offset [Integer] return the account chain block hashes offset
237
+ # by the specified number of blocks (default is 0)
238
+ # @return [Array<Nanook::Block>] blocks in the account chain ending at this block
239
+ def successors(limit: 1000, offset: 0)
240
+ params = {
241
+ count: limit,
242
+ offset: offset,
243
+ _access: :blocks,
244
+ _coerce: Array
245
+ }
246
+
247
+ rpc(:successors, :block, params).map do |block|
248
+ as_block(block)
249
+ end
206
250
  end
207
- alias_method :process, :publish
208
251
 
209
- # Returns an Array of block hashes in the account chain ending at
210
- # this block.
252
+ # Returns the {Nanook::Account} of the block representative.
211
253
  #
212
- # See also #chain.
254
+ # ==== Example:
255
+ # block.representative # => Nanook::Account
256
+ #
257
+ # @return [Nanook::Account] representative account of the block. Can be nil.
258
+ def representative
259
+ memoized_info[:representative]
260
+ end
261
+
262
+ # Returns the {Nanook::Account} of the block.
213
263
  #
214
264
  # ==== Example:
265
+ # block.account # => Nanook::Account
266
+ #
267
+ # @return [Nanook::Account] the account of the block. Can be nil.
268
+ def account
269
+ memoized_info[:account]
270
+ end
271
+
272
+ # Returns the amount of the block.
215
273
  #
216
- # block.successors
274
+ # ==== Example:
275
+ # block.amount # => 3.01
217
276
  #
218
- # ==== Example response:
277
+ # @param unit (see Nanook::Account#balance)
278
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
279
+ # @return [Float]
280
+ def amount(unit: Nanook.default_unit)
281
+ validate_unit!(unit)
282
+
283
+ amount = memoized_info[:amount]
284
+ return amount unless unit == :nano
285
+
286
+ raw_to_NANO(amount)
287
+ end
288
+
289
+ # Returns the balance of the account at the time the block was created.
219
290
  #
220
- # ["36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E"]
291
+ # ==== Example:
292
+ # block.balance # => 3.01
221
293
  #
222
- # @param limit [Integer] maximum number of send/receive block hashes
223
- # to return in the chain (default is 1000)
224
- # @return [Array<String>] block hashes in the account chain ending at this block
225
- def successors(limit: 1000)
226
- response = rpc(:successors, :block, count: limit)[:blocks]
227
- Nanook::Util.coerce_empty_string_to_type(response, Array)
294
+ # @param unit (see Nanook::Account#balance)
295
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
296
+ # @return [Float]
297
+ def balance(unit: Nanook.default_unit)
298
+ validate_unit!(unit)
299
+
300
+ balance = memoized_info[:balance]
301
+ return balance unless unit == :nano
302
+
303
+ raw_to_NANO(balance)
304
+ end
305
+
306
+ # Returns true if block is confirmed.
307
+ #
308
+ # ==== Example:
309
+ # block.confirmed # => true
310
+ #
311
+ # @return [Boolean]
312
+ def confirmed?
313
+ memoized_info[:confirmed]
314
+ end
315
+ alias checked? confirmed?
316
+
317
+ # Returns true if block is unconfirmed.
318
+ #
319
+ # ==== Example:
320
+ # block.unconfirmed? # => true
321
+ #
322
+ # @return [Boolean]
323
+ def unconfirmed?
324
+ !confirmed?
325
+ end
326
+ alias unchecked? unconfirmed?
327
+
328
+ # Returns true if block exists in the node's ledger. This will return
329
+ # false for blocks that exist on the nano ledger but have not yet
330
+ # synchronized to the node.
331
+ #
332
+ # ==== Example:
333
+ #
334
+ # block.exists? # => false
335
+ # block.exists?(allow_unchecked: true) # => true
336
+ #
337
+ # @param allow_unchecked [Boolean] defaults to +false+
338
+ # @return [Boolean]
339
+ def exists?(allow_unchecked: false)
340
+ begin
341
+ allow_unchecked ? memoized_info : info
342
+ rescue Nanook::NodeRpcError
343
+ return false
344
+ end
345
+
346
+ true
347
+ end
348
+
349
+ # Returns the height of the block.
350
+ #
351
+ # ==== Example:
352
+ # block.height # => 5
353
+ #
354
+ # @return [Integer]
355
+ def height
356
+ memoized_info[:height]
357
+ end
358
+
359
+ # Returns the block work.
360
+ #
361
+ # ==== Example:
362
+ # block.work # => "8a142e07a10996d5"
363
+ #
364
+ # @return [String]
365
+ def work
366
+ memoized_info[:work]
367
+ end
368
+
369
+ # Returns the block signature.
370
+ #
371
+ # ==== Example:
372
+ # block.signature # => "82D41BC16F313E4B2243D14DFFA2FB04679C540C2095FEE7EAE0F2F26880AD56DD48D87A7CC5DD760C5B2D76EE2C205506AA557BF00B60D8DEE312EC7343A501"
373
+ #
374
+ # @return [String]
375
+ def signature
376
+ memoized_info[:signature]
377
+ end
378
+
379
+ # Returns the timestamp of when the node saw the block.
380
+ #
381
+ # ==== Example:
382
+ # block.timestamp # => 2018-05-30 16:41:48 UTC
383
+ #
384
+ # @return [Time] Time in UTC of when the node saw the block. Can be nil.
385
+ def timestamp
386
+ memoized_info[:local_timestamp]
228
387
  end
229
388
 
230
- def inspect
231
- "#{self.class.name}(id: \"#{id}\", object_id: \"#{"0x00%x" % (object_id << 1)}\")"
389
+ # Returns the {Nanook::Block} of the previous block in the chain.
390
+ #
391
+ # ==== Example:
392
+ # block.previous # => Nanook::Block
393
+ #
394
+ # @return [Nanook::Block] previous block in the chain. Can be nil.
395
+ def previous
396
+ memoized_info[:previous]
397
+ end
398
+
399
+ # Returns the type of the block. One of "open", "send", "receive", "change", "epoch".
400
+ #
401
+ # ==== Example:
402
+ # block.type # => "open"
403
+ #
404
+ # @return [String] type of block. Returns nil for unconfirmed blocks.
405
+ def type
406
+ memoized_info[:type]
407
+ end
408
+
409
+ # Returns true if block is type "send".
410
+ #
411
+ # ==== Example:
412
+ # block.send? # => true
413
+ #
414
+ # @return [Boolean]
415
+ def send?
416
+ type == 'send'
417
+ end
418
+
419
+ # Returns true if block is type "open".
420
+ #
421
+ # ==== Example:
422
+ # block.open? # => true
423
+ #
424
+ # @return [Boolean]
425
+ def open?
426
+ type == 'open'
427
+ end
428
+
429
+ # Returns true if block is type "receive".
430
+ #
431
+ # ==== Example:
432
+ # block.receive? # => true
433
+ #
434
+ # @return [Boolean]
435
+ def receive?
436
+ type == 'receive'
437
+ end
438
+
439
+ # Returns true if block is type "change" (change of representative).
440
+ #
441
+ # ==== Example:
442
+ # block.change? # => true
443
+ #
444
+ # @return [Boolean]
445
+ def change?
446
+ type == 'change'
447
+ end
448
+
449
+ # Returns true if block is type "epoch".
450
+ #
451
+ # ==== Example:
452
+ # block.epoch? # => true
453
+ #
454
+ # @return [Boolean]
455
+ def epoch?
456
+ type == 'epoch'
457
+ end
458
+
459
+ # @return [String]
460
+ def to_s
461
+ "#{self.class.name}(id: \"#{short_id}\")"
232
462
  end
463
+ alias inspect to_s
233
464
 
234
465
  private
235
466
 
236
467
  # Some RPC calls expect the param that represents the block to be named
237
468
  # "hash", and others "block".
238
469
  # The param_name argument allows us to specify which it should be for this call.
239
- def rpc(action, param_name, params={})
240
- p = @block.nil? ? {} : { param_name.to_sym => @block }
470
+ def rpc(action, param_name, params = {})
471
+ p = { param_name.to_sym => @block }
241
472
  @rpc.call(action, p.merge(params))
242
473
  end
243
474
 
244
- def block_required!
245
- if @block.nil?
246
- raise ArgumentError.new("Block must be present")
247
- end
475
+ # Memoize the `#info` response as we can refer to it for other methods (`type`, `#open?`, `#send?` etc.)
476
+ def memoized_info
477
+ @memoized_info ||= info(allow_unchecked: true, unit: :raw)
248
478
  end
249
479
 
250
- def _parse_info_response(response)
251
- # The contents is a stringified JSON
252
- if response[:contents]
253
- r = JSON.parse(response[:contents]).to_symbolized_hash
254
- return r.merge(id: id)
480
+ def parse_info_response(response, unit)
481
+ response.merge!(id: id)
482
+ contents = response.delete(:contents)
483
+ response.merge!(contents) if contents
484
+
485
+ response.delete(:block_account) # duplicate of contents.account
486
+ response[:type] = response.delete(:subtype) # rename key
487
+ response[:last_modified_at] = response.delete(:modified_timestamp) # rename key
488
+
489
+ response[:account] = as_account(response[:account]) if response[:account]
490
+ response[:representative] = as_account(response[:representative]) if response[:representative]
491
+ response[:previous] = as_block(response[:previous]) if response[:previous]
492
+ response[:link] = as_block(response[:link]) if response[:link]
493
+ response[:link_as_account] = as_account(response[:link_as_account]) if response[:link_as_account]
494
+ response[:local_timestamp] = as_time(response[:local_timestamp])
495
+ response[:last_modified_at] = as_time(response[:last_modified_at])
496
+
497
+ if unit == :nano
498
+ response[:amount] = raw_to_NANO(response[:amount])
499
+ response[:balance] = raw_to_NANO(response[:balance])
255
500
  end
256
501
 
257
- response
502
+ response.compact
258
503
  end
259
-
260
504
  end
261
505
  end