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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +145 -3
- data/README.md +157 -90
- data/bin/console +4 -3
- data/lib/nanook.rb +77 -21
- data/lib/nanook/account.rb +236 -168
- data/lib/nanook/block.rb +363 -119
- data/lib/nanook/errors.rb +10 -0
- data/lib/nanook/node.rb +229 -66
- data/lib/nanook/private_key.rb +115 -0
- data/lib/nanook/public_key.rb +55 -0
- data/lib/nanook/rpc.rb +104 -44
- data/lib/nanook/util.rb +68 -19
- data/lib/nanook/version.rb +3 -1
- data/lib/nanook/wallet.rb +413 -120
- data/lib/nanook/wallet_account.rb +154 -91
- data/lib/nanook/work_peer.rb +14 -7
- metadata +29 -28
- data/lib/nanook/error.rb +0 -5
- data/lib/nanook/key.rb +0 -46
data/lib/nanook/node.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
|
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,65 +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
|
45
|
+
rpc(:frontier_count, _access: :count)
|
42
46
|
end
|
43
|
-
|
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
|
-
#
|
59
|
-
#
|
60
|
-
# ==== Example:
|
61
|
-
#
|
62
|
-
# node.block_count_by_type
|
63
|
-
#
|
64
|
-
# Example response:
|
66
|
+
# Tells the node to send a keepalive packet to a specific IP address and port.
|
65
67
|
#
|
66
|
-
#
|
67
|
-
|
68
|
-
|
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).
|
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).
|
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
|
95
|
+
end
|
96
|
+
|
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
|
105
|
+
#
|
106
|
+
# ==== Example:
|
107
|
+
#
|
108
|
+
# node.confirmation_quorum
|
109
|
+
#
|
110
|
+
# Example response:
|
111
|
+
#
|
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
|
+
# }
|
120
|
+
#
|
121
|
+
# @return [Hash{Symbol=>String|Integer}]
|
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)
|
176
|
+
end
|
91
177
|
end
|
92
178
|
|
93
179
|
# @return [String]
|
94
|
-
def
|
95
|
-
|
180
|
+
def to_s
|
181
|
+
self.class.name
|
96
182
|
end
|
183
|
+
alias inspect to_s
|
97
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}}]
|
98
199
|
def peers
|
99
|
-
rpc(:peers
|
200
|
+
rpc(:peers, peer_details: true, _access: :peers, _coerce: Hash)
|
100
201
|
end
|
101
202
|
|
102
203
|
# All representatives and their voting weight.
|
@@ -108,59 +209,86 @@ class Nanook
|
|
108
209
|
# Example response:
|
109
210
|
#
|
110
211
|
# {
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
212
|
+
# Nanook::Account: 3822372327060170000000000000000000000,
|
213
|
+
# Nanook::Account: 30999999999999999999999999000000,
|
214
|
+
# Nanook::Account: 0
|
114
215
|
# }
|
115
216
|
#
|
116
|
-
# @return [Hash{
|
217
|
+
# @return [Hash{Nanook::Account=>Float|Integer}] known representatives and their voting weight
|
218
|
+
# @raise [Nanook::NanoUnitError] if `unit` is invalid
|
117
219
|
def representatives(unit: Nanook.default_unit)
|
118
|
-
|
119
|
-
raise ArgumentError.new("Unsupported unit: #{unit}")
|
120
|
-
end
|
220
|
+
validate_unit!(unit)
|
121
221
|
|
122
|
-
response = rpc(:representatives
|
123
|
-
return response if unit == :raw
|
222
|
+
response = rpc(:representatives, _access: :representatives, _coerce: Hash)
|
124
223
|
|
125
|
-
r = response.map do |account_id,
|
126
|
-
|
224
|
+
r = response.map do |account_id, weight|
|
225
|
+
weight = raw_to_NANO(weight) if unit == :nano
|
127
226
|
|
128
|
-
[account_id,
|
227
|
+
[as_account(account_id), weight]
|
129
228
|
end
|
130
229
|
|
131
|
-
Hash[r]
|
230
|
+
Hash[r]
|
132
231
|
end
|
133
232
|
|
134
|
-
# All online representatives that have voted recently
|
135
|
-
# design of the nano RPC, this method cannot return the voting weight
|
136
|
-
# of the representatives.
|
233
|
+
# All online representatives that have voted recently and their weight.
|
137
234
|
#
|
138
235
|
# ==== Example:
|
139
236
|
#
|
140
|
-
# node.representatives_online # => [
|
237
|
+
# node.representatives_online # => [Nanook::Account, ...]
|
141
238
|
#
|
142
|
-
# @return [
|
239
|
+
# @return [Nanook::Account] array of representative accounts
|
143
240
|
def representatives_online
|
144
|
-
|
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)
|
145
254
|
end
|
146
255
|
|
147
256
|
# Safely shuts down the node.
|
148
257
|
#
|
149
258
|
# @return [Boolean] indicating if action was successful
|
150
259
|
def stop
|
151
|
-
rpc(:stop).
|
260
|
+
rpc(:stop).key?(:success)
|
152
261
|
end
|
153
262
|
|
154
263
|
# @param limit [Integer] number of synchronizing blocks to return
|
264
|
+
# @param unit (see Nanook::Account#balance)
|
265
|
+
#
|
155
266
|
# @return [Hash{Symbol=>String}] information about the synchronizing blocks for this node
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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]
|
160
287
|
end
|
161
|
-
|
288
|
+
|
289
|
+
Hash[response]
|
162
290
|
end
|
163
|
-
|
291
|
+
alias unchecked synchronizing_blocks
|
164
292
|
|
165
293
|
# The percentage completeness of the synchronization process for
|
166
294
|
# your node as it downloads the nano ledger. Note, it's normal for
|
@@ -171,35 +299,70 @@ class Nanook
|
|
171
299
|
# @return [Float] the percentage completeness of the synchronization
|
172
300
|
# process for your node
|
173
301
|
def sync_progress
|
174
|
-
response = rpc(:block_count)
|
302
|
+
response = rpc(:block_count, _coerce: Hash)
|
175
303
|
|
176
304
|
count = response[:count]
|
177
305
|
unchecked = response[:unchecked]
|
178
|
-
total =
|
306
|
+
total = count + unchecked
|
179
307
|
|
180
308
|
count.to_f * 100 / total.to_f
|
181
309
|
end
|
182
310
|
|
183
|
-
#
|
184
|
-
#
|
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+.
|
185
320
|
#
|
186
|
-
#
|
187
|
-
|
188
|
-
|
189
|
-
|
321
|
+
# ==== Example:
|
322
|
+
#
|
323
|
+
# account.change_receive_minimum(0.01) # true
|
324
|
+
#
|
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)
|
190
354
|
end
|
191
355
|
|
192
356
|
# @return [Hash{Symbol=>Integer|String}] version information for this node
|
193
357
|
def version
|
194
|
-
rpc(:version)
|
358
|
+
rpc(:version, _coerce: Hash)
|
195
359
|
end
|
196
|
-
|
360
|
+
alias info version
|
197
361
|
|
198
362
|
private
|
199
363
|
|
200
|
-
def rpc(action, params={})
|
364
|
+
def rpc(action, params = {})
|
201
365
|
@rpc.call(action, params)
|
202
366
|
end
|
203
|
-
|
204
367
|
end
|
205
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
|