crea-ruby 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +55 -0
- data/CONTRIBUTING.md +79 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +234 -0
- data/Rakefile +332 -0
- data/crea-ruby.gemspec +39 -0
- data/gource.sh +6 -0
- data/lib/crea.rb +85 -0
- data/lib/crea/api.rb +208 -0
- data/lib/crea/base_error.rb +218 -0
- data/lib/crea/block_api.rb +78 -0
- data/lib/crea/broadcast.rb +1334 -0
- data/lib/crea/chain_config.rb +36 -0
- data/lib/crea/formatter.rb +14 -0
- data/lib/crea/jsonrpc.rb +108 -0
- data/lib/crea/marshal.rb +231 -0
- data/lib/crea/mixins/jsonable.rb +37 -0
- data/lib/crea/mixins/retriable.rb +58 -0
- data/lib/crea/mixins/serializable.rb +45 -0
- data/lib/crea/operation.rb +141 -0
- data/lib/crea/operation/account_create.rb +10 -0
- data/lib/crea/operation/account_create_with_delegation.rb +12 -0
- data/lib/crea/operation/account_update.rb +8 -0
- data/lib/crea/operation/account_witness_proxy.rb +4 -0
- data/lib/crea/operation/account_witness_vote.rb +5 -0
- data/lib/crea/operation/cancel_transfer_from_savings.rb +4 -0
- data/lib/crea/operation/challenge_authority.rb +5 -0
- data/lib/crea/operation/change_recovery_account.rb +5 -0
- data/lib/crea/operation/claim_account.rb +5 -0
- data/lib/crea/operation/claim_reward_balance.rb +6 -0
- data/lib/crea/operation/comment.rb +9 -0
- data/lib/crea/operation/comment_options.rb +10 -0
- data/lib/crea/operation/convert.rb +5 -0
- data/lib/crea/operation/create_claimed_account.rb +10 -0
- data/lib/crea/operation/custom.rb +5 -0
- data/lib/crea/operation/custom_binary.rb +8 -0
- data/lib/crea/operation/custom_json.rb +6 -0
- data/lib/crea/operation/decline_voting_rights.rb +4 -0
- data/lib/crea/operation/delegate_vesting_shares.rb +5 -0
- data/lib/crea/operation/delete_comment.rb +4 -0
- data/lib/crea/operation/escrow_approve.rb +8 -0
- data/lib/crea/operation/escrow_dispute.rb +7 -0
- data/lib/crea/operation/escrow_release.rb +10 -0
- data/lib/crea/operation/escrow_transfer.rb +12 -0
- data/lib/crea/operation/feed_publish.rb +4 -0
- data/lib/crea/operation/limit_order_cancel.rb +4 -0
- data/lib/crea/operation/limit_order_create.rb +8 -0
- data/lib/crea/operation/limit_order_create2.rb +8 -0
- data/lib/crea/operation/prove_authority.rb +4 -0
- data/lib/crea/operation/recover_account.rb +6 -0
- data/lib/crea/operation/report_over_production.rb +5 -0
- data/lib/crea/operation/request_account_recovery.rb +6 -0
- data/lib/crea/operation/reset_account.rb +5 -0
- data/lib/crea/operation/set_reset_account.rb +5 -0
- data/lib/crea/operation/set_withdraw_vesting_route.rb +6 -0
- data/lib/crea/operation/transfer.rb +6 -0
- data/lib/crea/operation/transfer_from_savings.rb +7 -0
- data/lib/crea/operation/transfer_to_savings.rb +6 -0
- data/lib/crea/operation/transfer_to_vesting.rb +5 -0
- data/lib/crea/operation/vote.rb +6 -0
- data/lib/crea/operation/withdraw_vesting.rb +4 -0
- data/lib/crea/operation/witness_set_properties.rb +5 -0
- data/lib/crea/operation/witness_update.rb +7 -0
- data/lib/crea/rpc/base_client.rb +179 -0
- data/lib/crea/rpc/http_client.rb +143 -0
- data/lib/crea/rpc/thread_safe_http_client.rb +35 -0
- data/lib/crea/stream.rb +385 -0
- data/lib/crea/transaction.rb +96 -0
- data/lib/crea/transaction_builder.rb +393 -0
- data/lib/crea/type/amount.rb +107 -0
- data/lib/crea/type/base_type.rb +10 -0
- data/lib/crea/utils.rb +17 -0
- data/lib/crea/version.rb +4 -0
- metadata +478 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
module Crea
|
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] || :crea
|
19
|
+
@error_pipe = options[:error_pipe] || STDERR
|
20
|
+
@api_name = options[:api_name]
|
21
|
+
@url = case @chain
|
22
|
+
when :crea then options[:url] || NETWORKS_CREA_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
|
+
|
165
|
+
# @private
|
166
|
+
def raise_error_response(rpc_method_name, rpc_args, response)
|
167
|
+
raise UnknownError, "#{rpc_method_name}: #{response}" if response.error.nil?
|
168
|
+
|
169
|
+
error = response.error
|
170
|
+
|
171
|
+
if error.message == 'Invalid Request'
|
172
|
+
raise Crea::ArgumentError, "Unexpected arguments: #{rpc_args.inspect}. Expected: #{rpc_method_name} (#{args_keys_to_s(rpc_method_name)})"
|
173
|
+
end
|
174
|
+
|
175
|
+
BaseError.build_error(error, rpc_method_name)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Crea
|
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, Crea::RemoteDatabaseLockError]
|
21
|
+
|
22
|
+
# @private
|
23
|
+
POST_HEADERS = {
|
24
|
+
'Content-Type' => 'application/json; charset=utf-8',
|
25
|
+
'User-Agent' => Crea::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
|
+
[response].flatten.each_with_index do |r, i|
|
112
|
+
if defined?(r.error) && !!r.error
|
113
|
+
if !!r.error.message
|
114
|
+
begin
|
115
|
+
rpc_method_name = "#{api_name}.#{api_method}"
|
116
|
+
rpc_args = [request_object].flatten[i]
|
117
|
+
raise_error_response rpc_method_name, rpc_args, r
|
118
|
+
rescue *TIMEOUT_ERRORS => e
|
119
|
+
throw retry_timeout(:tota_cera_pila, e)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
raise Crea::ArgumentError, r.error.inspect
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
yield_response response, &block
|
128
|
+
when '504' # Gateway Timeout
|
129
|
+
throw retry_timeout(:tota_cera_pila, response.body)
|
130
|
+
when '502' # Bad Gateway
|
131
|
+
throw retry_timeout(:tota_cera_pila, response.body)
|
132
|
+
else
|
133
|
+
raise UnknownError, "#{api_name}.#{api_method}: #{response.body}"
|
134
|
+
end
|
135
|
+
end; end
|
136
|
+
end
|
137
|
+
|
138
|
+
def rpc_batch_execute(options = {}, &block)
|
139
|
+
yield_response rpc_execute(nil, nil, options), &block
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Crea
|
2
|
+
module RPC
|
3
|
+
# {ThreadSafeHttpClient} is the default RPC Client used by `crea-ruby.`
|
4
|
+
# It's perfect for simple requests. But for higher performance, it's better
|
5
|
+
# to override {HttpClient} and implement something other than {Net::HTTP}.
|
6
|
+
#
|
7
|
+
# It performs http requests in a {Mutex} critical section because {Net::HTTP}
|
8
|
+
# is not thread safe. This is the very minimum level thread safety
|
9
|
+
# available.
|
10
|
+
class ThreadSafeHttpClient < HttpClient
|
11
|
+
SEMAPHORE = Mutex.new.freeze
|
12
|
+
|
13
|
+
# Same as #{HttpClient#http_post}, but scoped to each thread so it is
|
14
|
+
# thread safe.
|
15
|
+
def http_post
|
16
|
+
thread = Thread.current
|
17
|
+
http_post = thread.thread_variable_get(:http_post)
|
18
|
+
http_post ||= Net::HTTP::Post.new(uri.request_uri, POST_HEADERS)
|
19
|
+
thread.thread_variable_set(:http_post, http_post)
|
20
|
+
end
|
21
|
+
|
22
|
+
def http_request(request); SEMAPHORE.synchronize{super}; end
|
23
|
+
|
24
|
+
# Same as #{BaseClient#rpc_id}, auto-increment, but scoped to each thread
|
25
|
+
# so it is thread safe.
|
26
|
+
def rpc_id
|
27
|
+
thread = Thread.current
|
28
|
+
rpc_id = thread.thread_variable_get(:rpc_id)
|
29
|
+
rpc_id ||= 0
|
30
|
+
rpc_id += 1
|
31
|
+
thread.thread_variable_set(:rpc_id, rpc_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|