nanook 2.5.0 → 4.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.
@@ -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
- # Default RPC server and port to connect to
19
- DEFAULT_URI = "http://localhost:7076"
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 ['http', 'https'].include?(@rpc_server.scheme)
27
- raise ArgumentError.new("URI must have http or https in it. Was given: #{uri}")
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.host, @rpc_server.port)
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, {"user-agent" => "Ruby nanook gem"})
33
- @request.content_type = "application/json"
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
- # Stringify param values
44
- params = Hash[params.map {|k, v| [k, v.to_s] }]
55
+ def call(action, params = {})
56
+ coerce_to = params.delete(:_coerce)
57
+ access_as = params.delete(:_access)
45
58
 
46
- @request.body = { action: action }.merge(params).to_json
59
+ raw_hash = make_call(action, params)
47
60
 
48
- response = @http.request(@request)
61
+ check_for_errors!(raw_hash)
49
62
 
50
- if response.is_a?(Net::HTTPSuccess)
51
- hash = JSON.parse(response.body)
52
- process_hash(hash)
53
- else
54
- raise Nanook::Error.new("Encountered net/http error #{response.code}: #{response.class.name}")
55
- end
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 inspect
60
- "#{self.class.name}(host: \"#{@rpc_server}\", timeout: #{@http.read_timeout} object_id: \"#{"0x00%x" % (object_id << 1)}\")"
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 process_hash(h)
67
- new_hash = h.map do |k,v|
68
- v = if v.is_a?(Array)
69
- if v[0].is_a?(Hash)
70
- v.map{|v| process_hash(v)}
71
- else
72
- v.map{|v| parse_value(v)}
73
- end
74
- elsif v.is_a?(Hash)
75
- process_hash(v)
76
- else
77
- parse_value(v)
78
- end
79
-
80
- [k, v]
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(v)
88
- return v.to_i if v.match(/^\d+\Z/)
89
- return true if v == "true"
90
- return false if v == "false"
91
- v
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
- # Set of class utility methods.
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.new("10")**BigDecimal.new("30")
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 self.NANO_to_raw(nano)
16
- (BigDecimal.new(nano.to_s) * STEP).to_i
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 self.raw_to_NANO(raw)
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
- # Converts an empty String value into an empty version of another type.
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
- # The RPC often returns an empty String (<tt>""</tt>) as a value, when a
30
- # +nil+, or empty Array (<tt>[]</tt>), or empty Hash (<tt>{}</tt>) would be better.
31
- # If the response might be
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
- # @param response the value returned from the RPC server
34
- # @param type the type to return an empty of
35
- def self.coerce_empty_string_to_type(response, type)
36
- if response == "" || response.nil?
37
- return type.new
38
- end
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
- response
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