dpay-ruby 0.1.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.
@@ -0,0 +1,21 @@
1
+ module DPay
2
+ module ChainConfig
3
+ EXPIRE_IN_SECS = 600
4
+ EXPIRE_IN_SECS_PROPOSAL = 24 * 60 * 60
5
+
6
+ NETWORKS_DPAY_CHAIN_ID = '38f14b346eb697ba04ae0f5adcfaa0a437ed3711197704aa256a14cb9b4a8f26'
7
+ NETWORKS_DPAY_ADDRESS_PREFIX = 'DWB'
8
+ NETWORKS_DPAY_CORE_ASSET = ["0", 3, "@@000000021"] # BEX
9
+ NETWORKS_DPAY_DEBT_ASSET = ["0", 3, "@@000000013"] # BBD
10
+ NETWORKS_DPAY_VEST_ASSET = ["0", 6, "@@000000037"] # VESTS
11
+ NETWORKS_DPAY_DEFAULT_NODE = 'https://api.dpays.io' # √
12
+ NETWORKS_TEST_CHAIN_ID = '46d82ab7d8db682eb1959aed0ada039a6d49afa1602491f93dde9cac3e8e6c32'
13
+ NETWORKS_TEST_ADDRESS_PREFIX = 'DWT'
14
+ NETWORKS_TEST_CORE_ASSET = ["0", 3, "@@000000021"] # TESTS
15
+ NETWORKS_TEST_DEBT_ASSET = ["0", 3, "@@000000013"] # TBD
16
+ NETWORKS_TEST_VEST_ASSET = ["0", 6, "@@000000037"] # VESTS
17
+ NETWORKS_TEST_DEFAULT_NODE = 'https://testnet.dpays.io'
18
+
19
+ NETWORK_CHAIN_IDS = [NETWORKS_DPAY_CHAIN_ID, NETWORKS_TEST_CHAIN_ID]
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module DPay
2
+ class Formatter
3
+ def self.reputation(raw)
4
+ raw = raw.to_i
5
+ neg = raw < 0
6
+ level = Math.log10(raw.abs)
7
+ level = [level - 9, 0].max
8
+ level = (neg ? -1 : 1) * level
9
+ level = (level * 9) + 25
10
+
11
+ level.round(1)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,108 @@
1
+ module DPay
2
+ # {Jsonrpc} allows you to inspect the available methods offered by a node.
3
+ # If a node runs a plugin you want, then all of the API methods it can exposes
4
+ # will automatically be available. This API is used internally to determine
5
+ # which APIs and methods are available on the node you specify.
6
+ #
7
+ # In theory, if a new plugin is created and enabled by the node, it will be
8
+ # available by this library without needing an update to its code.
9
+ class Jsonrpc < Api
10
+ API_METHODS = %i(get_signature get_methods)
11
+
12
+ def self.api_methods
13
+ @api_methods ||= {}
14
+ end
15
+
16
+ # Might help diagnose a cluster that has asymmetric plugin definitions.
17
+ def self.reset_api_methods
18
+ @api_methods = nil
19
+ end
20
+
21
+ def initialize(options = {})
22
+ @api_name = self.class.api_name = :jsonrpc
23
+ @methods = API_METHODS
24
+ super
25
+ end
26
+
27
+ def get_api_methods(&block)
28
+ api_methods = self.class.api_methods[@rpc_client.uri.to_s]
29
+
30
+ if api_methods.nil?
31
+ get_methods do |result, error, rpc_id|
32
+ raise NotAppBaseError, "#{@rpc_client.uri} does not appear to run AppBase" unless defined? result.map
33
+
34
+ methods = result.map do |method|
35
+ method.split('.').map(&:to_sym)
36
+ end
37
+
38
+ api_methods = Hashie::Mash.new
39
+
40
+ methods.each do |api, method|
41
+ api_methods[api] ||= []
42
+ api_methods[api] << method
43
+ end
44
+
45
+ self.class.api_methods[@rpc_client.uri.to_s] = api_methods
46
+ end
47
+ end
48
+
49
+ if !!block
50
+ api_methods.each do |api, methods|
51
+ yield api, methods
52
+ end
53
+ else
54
+ return api_methods
55
+ end
56
+ end
57
+
58
+ def get_all_signatures(&block)
59
+ request_object = []
60
+ method_names = []
61
+ method_map = {}
62
+ signatures = {}
63
+ offset = 0
64
+
65
+ get_api_methods do |api, methods|
66
+ request_object += methods.map do |method|
67
+ method_name = "#{api}.#{method}"
68
+ method_names << method_name
69
+ current_rpc_id = @rpc_client.rpc_id
70
+ offset += 1
71
+ method_map[current_rpc_id] = [api, method]
72
+
73
+ {
74
+ jsonrpc: '2.0',
75
+ id: current_rpc_id,
76
+ method: 'jsonrpc.get_signature',
77
+ params: {method: method_name}
78
+ }
79
+ end
80
+ end
81
+
82
+ chunks = if request_object.size > DPay::RPC::HttpClient::JSON_RPC_BATCH_SIZE_MAXIMUM
83
+ request_object.each_slice(DPay::RPC::HttpClient::JSON_RPC_BATCH_SIZE_MAXIMUM)
84
+ else
85
+ request_object
86
+ end
87
+
88
+ for request_object in chunks do
89
+ @rpc_client.rpc_batch_execute(request_object: request_object) do |result, error, id|
90
+ api, method = method_map[id]
91
+ api = api.to_sym
92
+ method = method.to_sym
93
+
94
+ signatures[api] ||= {}
95
+ signatures[api][method] = result
96
+ end
97
+
98
+ if !!block
99
+ signatures.each do |api, methods|
100
+ yield api, methods
101
+ end
102
+ end
103
+ end
104
+
105
+ return signatures unless !!block
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,58 @@
1
+ module DPay
2
+ module Retriable
3
+ # @private
4
+ MAX_RETRY_COUNT = 30
5
+
6
+ MAX_RETRY_ELAPSE = 60
7
+
8
+ # @private
9
+ MAX_BACKOFF = MAX_RETRY_ELAPSE / 4
10
+
11
+ RETRYABLE_EXCEPTIONS = [
12
+ NonCanonicalSignatureError, IncorrectRequestIdError,
13
+ IncorrectResponseIdError, RemoteDatabaseLockError
14
+ ]
15
+
16
+ def can_retry?(e = nil)
17
+ @retry_count ||= 0
18
+
19
+ return false if @retry_count >= MAX_RETRY_COUNT
20
+
21
+ @retry_count = if retry_reset?
22
+ @first_retry_at = nil
23
+ else
24
+ @retry_count + 1
25
+ end
26
+
27
+ can_retry = case e
28
+ when *RETRYABLE_EXCEPTIONS then true
29
+ else; false
30
+ end
31
+
32
+ backoff if can_retry
33
+
34
+ can_retry
35
+ end
36
+ private
37
+ # @private
38
+ def first_retry_at
39
+ @first_retry_at ||= Time.now.utc
40
+ end
41
+
42
+ # @private
43
+ def retry_reset?
44
+ Time.now.utc - first_retry_at > MAX_RETRY_ELAPSE
45
+ end
46
+
47
+ # Expontential backoff.
48
+ #
49
+ # @private
50
+ def backoff
51
+ @backoff ||= 0.1
52
+ @backoff *= 2
53
+ @backoff = 0.1 if @backoff > MAX_BACKOFF
54
+
55
+ sleep @backoff
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,166 @@
1
+ module DPay
2
+ module RPC
3
+ class BaseClient
4
+ include ChainConfig
5
+
6
+ attr_accessor :url, :chain, :error_pipe
7
+
8
+ # @private
9
+ MAX_TIMEOUT_RETRY_COUNT = 100
10
+
11
+ # @private
12
+ MAX_TIMEOUT_BACKOFF = 30
13
+
14
+ # @private
15
+ TIMEOUT_ERRORS = [Net::ReadTimeout, Errno::EBADF, IOError]
16
+
17
+ def initialize(options = {})
18
+ @chain = options[:chain] || :dpay
19
+ @error_pipe = options[:error_pipe] || STDERR
20
+ @api_name = options[:api_name]
21
+ @url = case @chain
22
+ when :dpay then options[:url] || NETWORKS_DPAY_DEFAULT_NODE
23
+ when :test then options[:url] || NETWORKS_TEST_DEFAULT_NODE
24
+ else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
25
+ end
26
+ end
27
+
28
+ def uri
29
+ @uri ||= URI.parse(url)
30
+ end
31
+
32
+ # Adds a request object to the stack. Usually, this method is called
33
+ # internally by {BaseClient#rpc_execute}. If you want to create a batched
34
+ # request, use this method to add to the batch then execute {BaseClient#rpc_batch_execute}.
35
+ def put(api_name = @api_name, api_method = nil, options = {})
36
+ current_rpc_id = rpc_id
37
+ rpc_method_name = "#{api_name}.#{api_method}"
38
+ options ||= {}
39
+ request_object = defined?(options.delete) ? options.delete(:request_object) : []
40
+ request_object ||= []
41
+
42
+ request_object << {
43
+ jsonrpc: '2.0',
44
+ id: current_rpc_id,
45
+ method: rpc_method_name,
46
+ params: options
47
+ }
48
+
49
+ request_object
50
+ end
51
+
52
+ # @abstract Subclass is expected to implement #rpc_execute.
53
+ # @!method rpc_execute
54
+
55
+ # @abstract Subclass is expected to implement #rpc_batch_execute.
56
+ # @!method rpc_batch_execute
57
+
58
+ # To be called by {BaseClient#rpc_execute} and {BaseClient#rpc_batch_execute}
59
+ # when a response has been consructed.
60
+ def yield_response(response, &block)
61
+ if !!block
62
+ case response
63
+ when Hashie::Mash then yield response.result, response.error, response.id
64
+ when Hashie::Array
65
+ response.each do |r|
66
+ r = Hashie::Mash.new(r)
67
+ block.call r.result, r.error, r.id
68
+ end
69
+ else; block.call response
70
+ end
71
+ end
72
+
73
+ response
74
+ end
75
+
76
+ # Checks json-rpc request/response for corrilated id. If they do not
77
+ # match, {IncorrectResponseIdError} is thrown. This is usually caused by
78
+ # the client, involving thread safety. It can also be caused by the node
79
+ # responding without an id.
80
+ #
81
+ # To avoid {IncorrectResponseIdError}, make sure you implement your client
82
+ # correctly.
83
+ #
84
+ # Setting DEBUG=true in the envrionment will cause this method to output
85
+ # both the request and response json.
86
+ #
87
+ # @param options [Hash] options
88
+ # @option options [Boolean] :debug Enable or disable debug output.
89
+ # @option options [Hash] :request to compare id
90
+ # @option options [Hash] :response to compare id
91
+ # @option options [String] :api_method
92
+ # @see {ThreadSafeHttpClient}
93
+ def evaluate_id(options = {})
94
+ debug = options[:debug] || ENV['DEBUG'] == 'true'
95
+ request = options[:request]
96
+ response = options[:response]
97
+ api_method = options[:api_method]
98
+ req_id = request[:id].to_i
99
+ res_id = !!response['id'] ? response['id'].to_i : nil
100
+ method = [@api_name, api_method].join('.')
101
+
102
+ if debug
103
+ req = JSON.pretty_generate(request)
104
+ res = JSON.parse(response) rescue response
105
+ res = JSON.pretty_generate(response) rescue response
106
+
107
+ error_pipe.puts '=' * 80
108
+ error_pipe.puts "Request:"
109
+ error_pipe.puts req
110
+ error_pipe.puts '=' * 80
111
+ error_pipe.puts "Response:"
112
+ error_pipe.puts res
113
+ error_pipe.puts '=' * 80
114
+ error_pipe.puts Thread.current.backtrace.join("\n")
115
+ end
116
+
117
+ error = response['error'].to_json if !!response['error']
118
+
119
+ if req_id != res_id
120
+ raise IncorrectResponseIdError, "#{method}: The json-rpc id did not match. Request was: #{req_id}, got: #{res_id.inspect}", BaseError.send(:build_backtrace, error)
121
+ end
122
+ end
123
+
124
+ # Current json-rpc id used for a request. This version auto-increments
125
+ # for each call. Subclasses can use their own strategy.
126
+ def rpc_id
127
+ @rpc_id ||= 0
128
+ @rpc_id += 1
129
+ end
130
+ private
131
+ # @private
132
+ def reset_timeout
133
+ @timeout_retry_count = 0
134
+ @back_off = 0.1
135
+ end
136
+
137
+ # @private
138
+ def retry_timeout(context, cause = nil)
139
+ @timeout_retry_count += 1
140
+
141
+ if @timeout_retry_count > MAX_TIMEOUT_RETRY_COUNT
142
+ raise TooManyTimeoutsError.new("Too many timeouts for: #{context}", cause)
143
+ elsif @timeout_retry_count % 10 == 0
144
+ msg = "#{@timeout_retry_count} retry attempts for: #{context}"
145
+ msg += "; cause: #{cause}" if !!cause
146
+ error_pipe.puts msg
147
+ end
148
+
149
+ backoff_timeout
150
+
151
+ context
152
+ end
153
+
154
+ # Expontential backoff.
155
+ #
156
+ # @private
157
+ def backoff_timeout
158
+ @backoff ||= 0.1
159
+ @backoff *= 2
160
+ @backoff = 0.1 if @backoff > MAX_TIMEOUT_BACKOFF
161
+
162
+ sleep @backoff
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,127 @@
1
+ module DPay
2
+ module RPC
3
+ # {HttpClient} is intended for single-threaded applications. For
4
+ # multi-threaded apps, use {ThreadSafeHttpClient}.
5
+ class HttpClient < BaseClient
6
+ # Timeouts are lower level errors, related in that retrying them is
7
+ # trivial, unlike, for example TransactionExpiredError, that *requires*
8
+ # the client to do something before retrying.
9
+ #
10
+ # These situations are hopefully momentary interruptions or rate limiting
11
+ # but they might indicate a bigger problem with the node, so they are not
12
+ # retried forever, only up to MAX_TIMEOUT_RETRY_COUNT and then we give up.
13
+ #
14
+ # *Note:* {JSON::ParserError} is included in this list because under
15
+ # certain timeout conditions, a web server may respond with a generic
16
+ # http status code of 200 and HTML page.
17
+ #
18
+ # @private
19
+ TIMEOUT_ERRORS = [Net::OpenTimeout, JSON::ParserError, Net::ReadTimeout,
20
+ Errno::EBADF, IOError, Errno::ENETDOWN]
21
+
22
+ # @private
23
+ POST_HEADERS = {
24
+ 'Content-Type' => 'application/json; charset=utf-8',
25
+ 'User-Agent' => DPay::AGENT_ID
26
+ }
27
+
28
+ JSON_RPC_BATCH_SIZE_MAXIMUM = 50
29
+
30
+ def http
31
+ @http ||= Net::HTTP.new(uri.host, uri.port).tap do |http|
32
+ http.use_ssl = true if uri.to_s =~ /^https/i
33
+ http.keep_alive_timeout = 150 # seconds
34
+
35
+ # WARNING This method opens a serious security hole. Never use this
36
+ # method in production code.
37
+ # http.set_debug_output(STDOUT) if !!ENV['DEBUG']
38
+ end
39
+ end
40
+
41
+ def http_post
42
+ @http_post ||= Net::HTTP::Post.new(uri.request_uri, POST_HEADERS)
43
+ end
44
+
45
+ def http_request(request)
46
+ http.request(request)
47
+ end
48
+
49
+ # This is the main method used by API instances to actually fetch data
50
+ # from the remote node. It abstracts the api namespace, method name, and
51
+ # parameters so that the API instance can be decoupled from the protocol.
52
+ #
53
+ # @param api_name [String] API namespace of the method being called.
54
+ # @param api_method [String] API method name being called.
55
+ # @param options [Hash] options
56
+ # @option options [Object] :request_object Hash or Array to become json in request body.
57
+ def rpc_execute(api_name = @api_name, api_method = nil, options = {}, &block)
58
+ reset_timeout
59
+
60
+ catch :tota_cera_pila do; begin
61
+ request = http_post
62
+
63
+ request_object = if !!api_name && !!api_method
64
+ put(api_name, api_method, options)
65
+ elsif !!options && defined?(options.delete)
66
+ options.delete(:request_object)
67
+ end
68
+
69
+ if request_object.size > JSON_RPC_BATCH_SIZE_MAXIMUM
70
+ raise JsonRpcBatchMaximumSizeExceededError, "Maximum json-rpc-batch is #{JSON_RPC_BATCH_SIZE_MAXIMUM} elements."
71
+ end
72
+
73
+ request.body = if request_object.class == Hash
74
+ request_object
75
+ elsif request_object.size == 1
76
+ request_object.first
77
+ else
78
+ request_object
79
+ end.to_json
80
+
81
+ response = catch :http_request do; begin; http_request(request)
82
+ rescue *TIMEOUT_ERRORS => e
83
+ throw retry_timeout(:http_request, e)
84
+ end; end
85
+
86
+ if response.nil?
87
+ throw retry_timeout(:tota_cera_pila, 'response was nil')
88
+ end
89
+
90
+ case response.code
91
+ when '200'
92
+ response = catch :parse_json do; begin; JSON[response.body]
93
+ rescue *TIMEOUT_ERRORS => e
94
+ throw retry_timeout(:parse_json, e)
95
+ end; end
96
+
97
+ response = case response
98
+ when Hash
99
+ Hashie::Mash.new(response).tap do |r|
100
+ evaluate_id(request: request_object.first, response: r, api_method: api_method)
101
+ end
102
+ when Array
103
+ Hashie::Array.new(response).tap do |r|
104
+ request_object.each_with_index do |req, index|
105
+ evaluate_id(request: req, response: r[index], api_method: api_method)
106
+ end
107
+ end
108
+ else; response
109
+ end
110
+
111
+ yield_response response, &block
112
+ when '504' # Gateway Timeout
113
+ throw retry_timeout(:tota_cera_pila, response.body)
114
+ when '502' # Bad Gateway
115
+ throw retry_timeout(:tota_cera_pila, response.body)
116
+ else
117
+ raise UnknownError, "#{api_name}.#{api_method}: #{response.body}"
118
+ end
119
+ end; end
120
+ end
121
+
122
+ def rpc_batch_execute(options = {}, &block)
123
+ yield_response rpc_execute(nil, nil, options), &block
124
+ end
125
+ end
126
+ end
127
+ end