crea-ruby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|