nanook 2.3.0 → 3.0.1

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