nanook 2.5.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +137 -0
- data/README.md +156 -99
- data/bin/console +4 -3
- data/lib/nanook.rb +76 -20
- data/lib/nanook/account.rb +271 -170
- data/lib/nanook/block.rb +384 -156
- data/lib/nanook/errors.rb +10 -0
- data/lib/nanook/node.rb +188 -131
- 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 +72 -19
- data/lib/nanook/version.rb +3 -1
- data/lib/nanook/wallet.rb +355 -164
- data/lib/nanook/wallet_account.rb +152 -92
- data/lib/nanook/work_peer.rb +14 -7
- metadata +30 -29
- data/lib/nanook/error.rb +0 -5
- data/lib/nanook/key.rb +0 -46
@@ -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
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'util'
|
4
|
+
|
5
|
+
class Nanook
|
6
|
+
# The <tt>Nanook::PublicKey</tt> class lets you manage your node's keys.
|
7
|
+
class PublicKey
|
8
|
+
include Nanook::Util
|
9
|
+
|
10
|
+
def initialize(rpc, key)
|
11
|
+
@rpc = rpc
|
12
|
+
@key = key.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def id
|
16
|
+
@key
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param other [Nanook::PublicKey] public 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
|
+
# Returns the account for a public key
|
36
|
+
#
|
37
|
+
# @return [Nanook::Account] account for the public key
|
38
|
+
def account
|
39
|
+
account = rpc(:account_get, _access: :account)
|
40
|
+
as_account(account)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [String]
|
44
|
+
def to_s
|
45
|
+
"#{self.class.name}(id: \"#{short_id}\")"
|
46
|
+
end
|
47
|
+
alias inspect to_s
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def rpc(action, params = {})
|
52
|
+
@rpc.call(action, { key: @key }.merge(params))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/nanook/rpc.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'symbolized'
|
3
5
|
|
4
6
|
class Nanook
|
5
|
-
|
6
7
|
# The <tt>Nanook::Rpc</tt> class is responsible for maintaining the
|
7
8
|
# connection to the RPC server, calling the RPC and parsing its response
|
8
9
|
# into Ruby primitives.
|
@@ -14,23 +15,35 @@ class Nanook
|
|
14
15
|
# nanook = Nanook.new
|
15
16
|
# nanook.rpc(:accounts_create, wallet: wallet_id, count: 2)
|
16
17
|
class Rpc
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
# Default request timeout in seconds
|
18
|
+
# Default RPC server and port to connect to.
|
19
|
+
DEFAULT_URI = 'http://[::1]:7076'
|
20
|
+
# Default request timeout in seconds.
|
21
21
|
DEFAULT_TIMEOUT = 60
|
22
|
+
# Error expected to be returned when the RPC makes a call that requires the
|
23
|
+
# `enable_control` setting to be enabled when it is disabled.
|
24
|
+
RPC_CONTROL_DISABLED_ERROR = 'RPC control is disabled'
|
22
25
|
|
23
|
-
def initialize(uri=DEFAULT_URI, timeout:DEFAULT_TIMEOUT)
|
26
|
+
def initialize(uri = DEFAULT_URI, timeout: DEFAULT_TIMEOUT)
|
24
27
|
@rpc_server = URI(uri)
|
25
28
|
|
26
|
-
unless [
|
27
|
-
raise ArgumentError
|
29
|
+
unless %w[http https].include?(@rpc_server.scheme)
|
30
|
+
raise ArgumentError, "URI must have http or https in it. Was given: #{uri}"
|
28
31
|
end
|
29
32
|
|
30
|
-
@http = Net::HTTP.new(@rpc_server.
|
33
|
+
@http = Net::HTTP.new(@rpc_server.hostname, @rpc_server.port)
|
31
34
|
@http.read_timeout = timeout
|
32
|
-
@request = Net::HTTP::Post.new(@rpc_server.request_uri, {
|
33
|
-
@request.content_type =
|
35
|
+
@request = Net::HTTP::Post.new(@rpc_server.request_uri, { 'user-agent' => "Ruby nanook gem v#{Nanook::VERSION}" })
|
36
|
+
@request.content_type = 'application/json'
|
37
|
+
end
|
38
|
+
|
39
|
+
# Tests the RPC connection. Returns +true+ if connection is successful,
|
40
|
+
# otherwise raises an exception.
|
41
|
+
#
|
42
|
+
# @raise [Errno::ECONNREFUSED] if connection is unsuccessful
|
43
|
+
# @return [Boolean] true if connection is successful
|
44
|
+
def test
|
45
|
+
call(:telemetry)
|
46
|
+
true
|
34
47
|
end
|
35
48
|
|
36
49
|
# Calls the RPC server and returns the response.
|
@@ -39,57 +52,104 @@ class Nanook
|
|
39
52
|
# expects an "action" param to identify what RPC action is being called.
|
40
53
|
# @param params [Hash] all other params to pass to the RPC
|
41
54
|
# @return [Hash] the response from the RPC
|
42
|
-
def call(action, params={})
|
43
|
-
|
44
|
-
|
55
|
+
def call(action, params = {})
|
56
|
+
coerce_to = params.delete(:_coerce)
|
57
|
+
access_as = params.delete(:_access)
|
45
58
|
|
46
|
-
|
59
|
+
raw_hash = make_call(action, params)
|
47
60
|
|
48
|
-
|
61
|
+
check_for_errors!(raw_hash)
|
49
62
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
63
|
+
hash = parse_values(raw_hash)
|
64
|
+
|
65
|
+
hash = hash[access_as] if access_as
|
66
|
+
hash = coerce_empty_string_to_type(hash, coerce_to) if coerce_to
|
67
|
+
|
68
|
+
hash
|
56
69
|
end
|
57
70
|
|
58
71
|
# @return [String]
|
59
|
-
def
|
60
|
-
"#{self.class.name}(host: \"#{@rpc_server}\", timeout: #{@http.read_timeout}
|
72
|
+
def to_s
|
73
|
+
"#{self.class.name}(host: \"#{@rpc_server}\", timeout: #{@http.read_timeout})"
|
61
74
|
end
|
75
|
+
alias inspect to_s
|
62
76
|
|
63
77
|
private
|
64
78
|
|
79
|
+
def make_call(action, params)
|
80
|
+
# Stringify param values
|
81
|
+
params = params.dup.transform_values do |v|
|
82
|
+
next v if v.is_a?(Array)
|
83
|
+
|
84
|
+
v.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
@request.body = { action: action }.merge(params).to_json
|
88
|
+
|
89
|
+
response = @http.request(@request)
|
90
|
+
|
91
|
+
raise Nanook::ConnectionError, "Encountered net/http error #{response.code}: #{response.class.name}" \
|
92
|
+
unless response.is_a?(Net::HTTPSuccess)
|
93
|
+
|
94
|
+
JSON.parse(response.body)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Raises a {Nanook::NodeRpcConfigurationError} or {Nanook::NodeRpcError} if the RPC
|
98
|
+
# response contains an `:error` key.
|
99
|
+
def check_for_errors!(response)
|
100
|
+
# Raise a special error for when `enable_control` should be enabled.
|
101
|
+
if response['error'] == RPC_CONTROL_DISABLED_ERROR
|
102
|
+
raise Nanook::NodeRpcConfigurationError,
|
103
|
+
'RPC must have the `enable_control` setting enabled to perform this action.'
|
104
|
+
end
|
105
|
+
|
106
|
+
# Raise any other error.
|
107
|
+
raise Nanook::NodeRpcError, "An error was returned from the RPC: #{response['error']}" if response.key?('error')
|
108
|
+
end
|
109
|
+
|
65
110
|
# Recursively parses the RPC response, sending values to #parse_value
|
66
|
-
def
|
67
|
-
new_hash =
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
111
|
+
def parse_values(hash)
|
112
|
+
new_hash = hash.map do |k, val|
|
113
|
+
new_val = case val
|
114
|
+
when Array
|
115
|
+
if val[0].is_a?(Hash)
|
116
|
+
val.map { |v| parse_values(v) }
|
117
|
+
else
|
118
|
+
val.map { |v| parse_value(v) }
|
119
|
+
end
|
120
|
+
when Hash
|
121
|
+
parse_values(val)
|
122
|
+
else
|
123
|
+
parse_value(val)
|
124
|
+
end
|
125
|
+
|
126
|
+
[k, new_val]
|
81
127
|
end
|
82
128
|
|
83
129
|
Hash[new_hash.sort].to_symbolized_hash
|
84
130
|
end
|
85
131
|
|
86
132
|
# Converts Strings to primitives
|
87
|
-
def parse_value(
|
88
|
-
return
|
89
|
-
return true if
|
90
|
-
return false if
|
91
|
-
|
133
|
+
def parse_value(value)
|
134
|
+
return value.to_i if value.match(/^\d+\Z/)
|
135
|
+
return true if value == 'true'
|
136
|
+
return false if value == 'false'
|
137
|
+
|
138
|
+
value
|
92
139
|
end
|
93
140
|
|
141
|
+
# Converts an empty String value into an empty version of another type.
|
142
|
+
#
|
143
|
+
# The RPC often returns an empty String as a value to signal
|
144
|
+
# emptiness, rather than consistent types like an empty Array,
|
145
|
+
# or empty Hash.
|
146
|
+
#
|
147
|
+
# @param response the value returned from the RPC server
|
148
|
+
# @param type the type to return an empty of
|
149
|
+
def coerce_empty_string_to_type(response, type)
|
150
|
+
return type.new if response == '' || response.nil?
|
151
|
+
|
152
|
+
response
|
153
|
+
end
|
94
154
|
end
|
95
155
|
end
|
data/lib/nanook/util.rb
CHANGED
@@ -1,44 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bigdecimal'
|
2
4
|
|
3
5
|
class Nanook
|
4
|
-
|
5
|
-
|
6
|
-
class Util
|
7
|
-
|
6
|
+
# Set of utility methods.
|
7
|
+
module Util
|
8
8
|
# Constant used to convert back and forth between raw and NANO.
|
9
|
-
STEP = BigDecimal
|
9
|
+
STEP = BigDecimal('10')**BigDecimal('30')
|
10
|
+
|
11
|
+
private
|
10
12
|
|
11
13
|
# Converts an amount of NANO to an amount of raw.
|
12
14
|
#
|
13
15
|
# @param nano [Float|Integer] amount in nano
|
14
16
|
# @return [Integer] amount in raw
|
15
|
-
def
|
16
|
-
|
17
|
+
def NANO_to_raw(nano)
|
18
|
+
return if nano.nil?
|
19
|
+
|
20
|
+
(BigDecimal(nano.to_s) * STEP).to_i
|
17
21
|
end
|
18
22
|
|
19
23
|
# Converts an amount of raw to an amount of NANO.
|
20
24
|
#
|
21
25
|
# @param raw [Integer] amount in raw
|
22
26
|
# @return [Float|Integer] amount in NANO
|
23
|
-
def
|
27
|
+
def raw_to_NANO(raw)
|
28
|
+
return if raw.nil?
|
29
|
+
|
24
30
|
(raw.to_f / STEP).to_f
|
25
31
|
end
|
26
32
|
|
27
|
-
#
|
33
|
+
# @return [TrueClass] if unit is valid.
|
34
|
+
# @raise [Nanook::NanoUnitError] if `unit` is invalid.
|
35
|
+
def validate_unit!(unit)
|
36
|
+
unless Nanook::UNITS.include?(unit.to_sym)
|
37
|
+
raise Nanook::NanoUnitError, "Unit #{unit} must be one of #{Nanook::UNITS}"
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the +id+ of the object as a short id.
|
44
|
+
# See #shorten_id.
|
28
45
|
#
|
29
|
-
#
|
30
|
-
|
31
|
-
|
46
|
+
# @return [String]
|
47
|
+
def short_id
|
48
|
+
shorten_id(id)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns an id string (hash or nano account) truncated with an ellipsis.
|
52
|
+
# The first 7 and last 4 characters are retained for easy identification.
|
32
53
|
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
54
|
+
# ==== Examples:
|
55
|
+
#
|
56
|
+
# shorten_id('nano_16u1uufyoig8777y6r8iqjtrw8sg8maqrm36zzcm95jmbd9i9aj5i8abr8u5')
|
57
|
+
# # => "16u1uuf...r8u5"
|
58
|
+
#
|
59
|
+
# shorten_id('A170D51B94E00371ACE76E35AC81DC9405D5D04D4CEBC399AEACE07AE05DD293')
|
60
|
+
# # => "A170D51...D293"
|
61
|
+
#
|
62
|
+
# @return [String]
|
63
|
+
def shorten_id(long_id)
|
64
|
+
return unless long_id
|
65
|
+
|
66
|
+
[long_id.sub('nano_', '')[0..6], long_id[-4, 4]].join('...')
|
67
|
+
end
|
68
|
+
|
69
|
+
def as_account(account_id)
|
70
|
+
Nanook::Account.new(@rpc, account_id) if account_id
|
71
|
+
end
|
72
|
+
|
73
|
+
def as_wallet_account(account_id, allow_blank: false)
|
74
|
+
return unless account_id || allow_blank
|
75
|
+
|
76
|
+
Nanook::WalletAccount.new(@rpc, @wallet, account_id)
|
77
|
+
end
|
78
|
+
|
79
|
+
def as_block(block_id)
|
80
|
+
Nanook::Block.new(@rpc, block_id) if block_id
|
81
|
+
end
|
82
|
+
|
83
|
+
def as_private_key(key, allow_blank: false)
|
84
|
+
return unless key || allow_blank
|
39
85
|
|
40
|
-
|
86
|
+
Nanook::PrivateKey.new(@rpc, key)
|
41
87
|
end
|
42
88
|
|
89
|
+
def as_public_key(key)
|
90
|
+
Nanook::PublicKey.new(@rpc, key) if key
|
91
|
+
end
|
92
|
+
|
93
|
+
def as_time(time)
|
94
|
+
Time.at(time).utc if time
|
95
|
+
end
|
43
96
|
end
|
44
97
|
end
|