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.
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