nanook 2.3.0 → 3.0.1

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,141 +133,384 @@ 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
 
178
+ # Returns true if work is valid for the block.
179
+ #
148
180
  # ==== Example:
149
181
  #
150
- # block.is_valid_work?("2bf29ef00786a6bc") # => true
182
+ # block.valid_work?("2bf29ef00786a6bc") # => true
151
183
  #
152
184
  # @param work [String] the work id to check is valid
153
185
  # @return [Boolean] signalling if work is valid for the block
154
- def is_valid_work?(work)
186
+ def valid_work?(work)
155
187
  response = rpc(:work_validate, :hash, work: work)
156
- !response.empty? && response[:valid] == 1
188
+ response[:valid_all] == 1 || response[:valid_receive] == 1
157
189
  end
158
190
 
159
191
  # Republish blocks starting at this block up the account chain
160
192
  # back to the nano network.
161
193
  #
162
- # @return [Array<String>] block hashes that were republished
194
+ # @return [Array<Nanook::Block>] blocks that were republished
163
195
  #
164
196
  # ==== Example:
165
197
  #
166
- # block.republish
167
- #
168
- # ==== Example response:
169
- #
170
- # ["36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E"]
171
- def republish(destinations:nil, sources:nil)
198
+ # block.republish # => [Nanook::Block, ...]
199
+ def republish(destinations: nil, sources: nil)
172
200
  if !destinations.nil? && !sources.nil?
173
- raise ArgumentError.new("You must provide either destinations or sources but not both")
201
+ raise ArgumentError, 'You must provide either destinations or sources but not both'
174
202
  end
175
203
 
176
- # Add in optional arguments
177
- params = {}
204
+ params = {
205
+ _access: :blocks,
206
+ _coerce: Array
207
+ }
208
+
178
209
  params[:destinations] = destinations unless destinations.nil?
179
210
  params[:sources] = sources unless sources.nil?
180
- params[:count] = 1 unless params.empty?
211
+ params[:count] = 1 if destinations || sources
181
212
 
182
- rpc(:republish, :hash, params)[:blocks]
213
+ rpc(:republish, :hash, params).map do |block|
214
+ as_block(block)
215
+ end
183
216
  end
184
217
 
218
+ # Returns true if block is a pending block.
219
+ #
185
220
  # ==== Example:
186
221
  #
187
222
  # block.pending? #=> false
188
223
  #
189
224
  # @return [Boolean] signalling if the block is a pending block.
190
225
  def pending?
191
- response = rpc(:pending_exists, :hash)
192
- !response.empty? && response[:exists] == 1
226
+ rpc(:pending_exists, :hash, _access: :exists) == 1
193
227
  end
194
228
 
195
- # Publish the block to the nano network.
229
+ # Returns an Array of block hashes in the account chain ending at
230
+ # this block.
196
231
  #
197
- # Note, if block has previously been published, use #republish instead.
232
+ # See also #chain.
198
233
  #
199
- # ==== Examples:
234
+ # ==== Example:
200
235
  #
201
- # block.publish # => "FBF8B0E..."
236
+ # block.successors # => [Nanook::Block, .. ]
202
237
  #
203
- # @return [String] the block hash, or false.
204
- def publish
205
- rpc(:process, :block)[:hash] || false
238
+ # @param limit [Integer] maximum number of send/receive block hashes
239
+ # to return in the chain (default is 1000)
240
+ # @param offset [Integer] return the account chain block hashes offset
241
+ # by the specified number of blocks (default is 0)
242
+ # @return [Array<Nanook::Block>] blocks in the account chain ending at this block
243
+ def successors(limit: 1000, offset: 0)
244
+ params = {
245
+ count: limit,
246
+ offset: offset,
247
+ _access: :blocks,
248
+ _coerce: Array
249
+ }
250
+
251
+ rpc(:successors, :block, params).map do |block|
252
+ as_block(block)
253
+ end
206
254
  end
207
- alias_method :process, :publish
208
255
 
209
- # Returns an Array of block hashes in the account chain ending at
210
- # this block.
256
+ # Returns the {Nanook::Account} of the block representative.
211
257
  #
212
- # See also #chain.
258
+ # ==== Example:
259
+ # block.representative # => Nanook::Account
260
+ #
261
+ # @return [Nanook::Account] representative account of the block. Can be nil.
262
+ def representative
263
+ memoized_info[:representative]
264
+ end
265
+
266
+ # Returns the {Nanook::Account} of the block.
213
267
  #
214
268
  # ==== Example:
269
+ # block.account # => Nanook::Account
270
+ #
271
+ # @return [Nanook::Account] the account of the block. Can be nil.
272
+ def account
273
+ memoized_info[:account]
274
+ end
275
+
276
+ # Returns the amount of the block.
215
277
  #
216
- # block.successors
278
+ # ==== Example:
279
+ # block.amount # => 3.01
217
280
  #
218
- # ==== Example response:
281
+ # @param unit (see Nanook::Account#balance)
282
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
283
+ # @return [Float]
284
+ def amount(unit: Nanook.default_unit)
285
+ validate_unit!(unit)
286
+
287
+ amount = memoized_info[:amount]
288
+ return amount unless unit == :nano
289
+
290
+ raw_to_NANO(amount)
291
+ end
292
+
293
+ # Returns the balance of the account at the time the block was created.
219
294
  #
220
- # ["36A0FB717368BA8CF8D255B63DC207771EABC6C6FFC22A7F455EC2209464897E"]
295
+ # ==== Example:
296
+ # block.balance # => 3.01
221
297
  #
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)
298
+ # @param unit (see Nanook::Account#balance)
299
+ # @raise [Nanook::NanoUnitError] if `unit` is invalid
300
+ # @return [Float]
301
+ def balance(unit: Nanook.default_unit)
302
+ validate_unit!(unit)
303
+
304
+ balance = memoized_info[:balance]
305
+ return balance unless unit == :nano
306
+
307
+ raw_to_NANO(balance)
308
+ end
309
+
310
+ # Returns true if block is confirmed.
311
+ #
312
+ # ==== Example:
313
+ # block.confirmed # => true
314
+ #
315
+ # @return [Boolean]
316
+ def confirmed?
317
+ memoized_info[:confirmed]
318
+ end
319
+ alias checked? confirmed?
320
+
321
+ # Returns true if block is unconfirmed.
322
+ #
323
+ # ==== Example:
324
+ # block.unconfirmed? # => true
325
+ #
326
+ # @return [Boolean]
327
+ def unconfirmed?
328
+ !confirmed?
329
+ end
330
+ alias unchecked? unconfirmed?
331
+
332
+ # Returns true if block exists in the node's ledger. This will return
333
+ # false for blocks that exist on the nano ledger but have not yet
334
+ # synchronized to the node.
335
+ #
336
+ # ==== Example:
337
+ #
338
+ # block.exists? # => false
339
+ # block.exists?(allow_unchecked: true) # => true
340
+ #
341
+ # @param allow_unchecked [Boolean] defaults to +false+
342
+ # @return [Boolean]
343
+ def exists?(allow_unchecked: false)
344
+ begin
345
+ allow_unchecked ? memoized_info : info
346
+ rescue Nanook::NodeRpcError
347
+ return false
348
+ end
349
+
350
+ true
351
+ end
352
+
353
+ # Returns the height of the block.
354
+ #
355
+ # ==== Example:
356
+ # block.height # => 5
357
+ #
358
+ # @return [Integer]
359
+ def height
360
+ memoized_info[:height]
361
+ end
362
+
363
+ # Returns the block work.
364
+ #
365
+ # ==== Example:
366
+ # block.work # => "8a142e07a10996d5"
367
+ #
368
+ # @return [String]
369
+ def work
370
+ memoized_info[:work]
371
+ end
372
+
373
+ # Returns the block signature.
374
+ #
375
+ # ==== Example:
376
+ # block.signature # => "82D41BC16F313E4B2243D14DFFA2FB04679C540C2095FEE7EAE0F2F26880AD56DD48D87A7CC5DD760C5B2D76EE2C205506AA557BF00B60D8DEE312EC7343A501"
377
+ #
378
+ # @return [String]
379
+ def signature
380
+ memoized_info[:signature]
381
+ end
382
+
383
+ # Returns the timestamp of when the node saw the block.
384
+ #
385
+ # ==== Example:
386
+ # block.timestamp # => 2018-05-30 16:41:48 UTC
387
+ #
388
+ # @return [Time] Time in UTC of when the node saw the block. Can be nil.
389
+ def timestamp
390
+ memoized_info[:local_timestamp]
391
+ end
392
+
393
+ # Returns the {Nanook::Block} of the previous block in the chain.
394
+ #
395
+ # ==== Example:
396
+ # block.previous # => Nanook::Block
397
+ #
398
+ # @return [Nanook::Block] previous block in the chain. Can be nil.
399
+ def previous
400
+ memoized_info[:previous]
228
401
  end
229
402
 
230
- def inspect
231
- "#{self.class.name}(id: \"#{id}\", object_id: \"#{"0x00%x" % (object_id << 1)}\")"
403
+ # Returns the type of the block. One of "open", "send", "receive", "change", "epoch".
404
+ #
405
+ # ==== Example:
406
+ # block.type # => "open"
407
+ #
408
+ # @return [String] type of block. Returns nil for unconfirmed blocks.
409
+ def type
410
+ memoized_info[:type]
232
411
  end
233
412
 
413
+ # Returns true if block is type "send".
414
+ #
415
+ # ==== Example:
416
+ # block.send? # => true
417
+ #
418
+ # @return [Boolean]
419
+ def send?
420
+ type == 'send'
421
+ end
422
+
423
+ # Returns true if block is type "open".
424
+ #
425
+ # ==== Example:
426
+ # block.open? # => true
427
+ #
428
+ # @return [Boolean]
429
+ def open?
430
+ type == 'open'
431
+ end
432
+
433
+ # Returns true if block is type "receive".
434
+ #
435
+ # ==== Example:
436
+ # block.receive? # => true
437
+ #
438
+ # @return [Boolean]
439
+ def receive?
440
+ type == 'receive'
441
+ end
442
+
443
+ # Returns true if block is type "change" (change of representative).
444
+ #
445
+ # ==== Example:
446
+ # block.change? # => true
447
+ #
448
+ # @return [Boolean]
449
+ def change?
450
+ type == 'change'
451
+ end
452
+
453
+ # Returns true if block is type "epoch".
454
+ #
455
+ # ==== Example:
456
+ # block.epoch? # => true
457
+ #
458
+ # @return [Boolean]
459
+ def epoch?
460
+ type == 'epoch'
461
+ end
462
+
463
+ # @return [String]
464
+ def to_s
465
+ "#{self.class.name}(id: \"#{short_id}\")"
466
+ end
467
+ alias inspect to_s
468
+
234
469
  private
235
470
 
236
471
  # Some RPC calls expect the param that represents the block to be named
237
472
  # "hash", and others "block".
238
473
  # 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 }
474
+ def rpc(action, param_name, params = {})
475
+ p = { param_name.to_sym => @block }
241
476
  @rpc.call(action, p.merge(params))
242
477
  end
243
478
 
244
- def block_required!
245
- if @block.nil?
246
- raise ArgumentError.new("Block must be present")
247
- end
479
+ # Memoize the `#info` response as we can refer to it for other methods (`type`, `#open?`, `#send?` etc.)
480
+ def memoized_info
481
+ @memoized_info ||= info(allow_unchecked: true, unit: :raw)
248
482
  end
249
483
 
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)
484
+ def parse_info_response(response, unit)
485
+ response.merge!(id: id)
486
+ contents = response.delete(:contents)
487
+ response.merge!(contents) if contents
488
+
489
+ response.delete(:block_account) # duplicate of contents.account
490
+ response[:last_modified_at] = response.delete(:modified_timestamp) # rename key
491
+
492
+ # `type` can be "open", "send", "receive", "change", "epoch" or "state".
493
+ # blocks with `type` == "state" may have a `subtype` that gives more information
494
+ # about the block ("send", "receive", "change", "epoch"), in which case replace
495
+ # the `type` with this value.
496
+ if response[:type] == 'state' && (subtype = response.delete(:subtype))
497
+ response[:type] = subtype
255
498
  end
256
499
 
257
- response
258
- end
500
+ response[:account] = as_account(response[:account]) if response[:account]
501
+ response[:representative] = as_account(response[:representative]) if response[:representative]
502
+ response[:previous] = as_block(response[:previous]) if response[:previous]
503
+ response[:link] = as_block(response[:link]) if response[:link]
504
+ response[:link_as_account] = as_account(response[:link_as_account]) if response[:link_as_account]
505
+ response[:local_timestamp] = as_time(response[:local_timestamp])
506
+ response[:last_modified_at] = as_time(response[:last_modified_at])
259
507
 
508
+ if unit == :nano
509
+ response[:amount] = raw_to_NANO(response[:amount])
510
+ response[:balance] = raw_to_NANO(response[:balance])
511
+ end
512
+
513
+ response.compact
514
+ end
260
515
  end
261
516
  end