steem-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ module Steem
2
+ module ChainConfig
3
+ EXPIRE_IN_SECS = 600
4
+ EXPIRE_IN_SECS_PROPOSAL = 24 * 60 * 60
5
+
6
+ NETWORKS_STEEM_CHAIN_ID = '0000000000000000000000000000000000000000000000000000000000000000'
7
+ NETWORKS_STEEM_ADDRESS_PREFIX = 'STM'
8
+ NETWORKS_STEEM_CORE_ASSET = ["0", 3, "@@000000021"] # STEEM
9
+ NETWORKS_STEEM_DEBT_ASSET = ["0", 3, "@@000000013"] # SBD
10
+ NETWORKS_STEEM_VEST_ASSET = ["0", 6, "@@000000037"] # VESTS
11
+ NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steemit.com' # √
12
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steemitstage.com' # √
13
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steemitdev.com' # √
14
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://appbasetest.timcliff.com'
15
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://gtg.steem.house:8090'
16
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steem.house' # √?
17
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://seed.bitcoiner.me'
18
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.minnowsupportproject.org'
19
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.privex.io'
20
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.steemliberator.com'
21
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.curiesteem.com'
22
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.buildteam.io'
23
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.pevo.science'
24
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.steemviz.com'
25
+ # NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.steemgigs.org'
26
+
27
+ NETWORKS_TEST_CHAIN_ID = '46d82ab7d8db682eb1959aed0ada039a6d49afa1602491f93dde9cac3e8e6c32'
28
+ NETWORKS_TEST_ADDRESS_PREFIX = 'TST'
29
+ NETWORKS_TEST_CORE_ASSET = ["0", 3, "@@000000021"] # TESTS
30
+ NETWORKS_TEST_DEBT_ASSET = ["0", 3, "@@000000013"] # TBD
31
+ NETWORKS_TEST_VEST_ASSET = ["0", 6, "@@000000037"] # VESTS
32
+ NETWORKS_TEST_DEFAULT_NODE = 'https://testnet.steemitdev.com'
33
+
34
+ NETWORK_CHAIN_IDS = [NETWORKS_STEEM_CHAIN_ID, NETWORKS_TEST_CHAIN_ID]
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ module Steem
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,98 @@
1
+ module Steem
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
+ 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
+ methods = result.map do |method|
33
+ method.split('.').map(&:to_sym)
34
+ end
35
+
36
+ api_methods = Hashie::Mash.new
37
+
38
+ methods.each do |api, method|
39
+ api_methods[api] ||= []
40
+ api_methods[api] << method
41
+ end
42
+
43
+ self.class.api_methods[@rpc_client.uri.to_s] = api_methods
44
+ end
45
+ end
46
+
47
+ if !!block
48
+ api_methods.each do |api, methods|
49
+ yield api, methods
50
+ end
51
+ else
52
+ return api_methods
53
+ end
54
+ end
55
+
56
+ def get_all_signatures(&block)
57
+ request_body = []
58
+ method_names = []
59
+ method_map = {}
60
+ signatures = {}
61
+ offset = 0
62
+
63
+ get_api_methods do |api, methods|
64
+ request_body += methods.map do |method|
65
+ method_name = "#{api}.#{method}"
66
+ method_names << method_name
67
+ current_rpc_id = @rpc_client.rpc_id
68
+ offset += 1
69
+ method_map[current_rpc_id] = [api, method]
70
+
71
+ {
72
+ jsonrpc: '2.0',
73
+ id: current_rpc_id,
74
+ method: 'jsonrpc.get_signature',
75
+ params: {method: method_name}
76
+ }
77
+ end
78
+ end
79
+
80
+ @rpc_client.rpc_post(nil, nil, {request_body: request_body}) do |result, error, id|
81
+ api, method = method_map[id]
82
+ api = api.to_sym
83
+ method = method.to_sym
84
+
85
+ signatures[api] ||= {}
86
+ signatures[api][method] = result
87
+ end
88
+
89
+ if !!block
90
+ signatures.each do |api, methods|
91
+ yield api, methods
92
+ end
93
+ else
94
+ return signatures
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,56 @@
1
+ module Steem
2
+ module Retriable
3
+ # @private
4
+ MAX_RETRY_COUNT = 100
5
+
6
+ # @private
7
+ MAX_BACKOFF = 30
8
+
9
+ MAX_RETRY_ELAPSE = 300
10
+
11
+ RETRYABLE_EXCEPTIONS = [
12
+ NonCanonicalSignatureError, IncorrectRequestIdError,
13
+ IncorrectResponseIdError, Errno::EBADF, Errno::ECONNREFUSED,
14
+ JSON::ParserError, IOError, Net::OpenTimeout
15
+ ]
16
+
17
+ # Expontential backoff.
18
+ #
19
+ # @private
20
+ def backoff
21
+ @backoff ||= 0.1
22
+ @backoff *= 2
23
+ if @backoff > MAX_BACKOFF
24
+ @backoff = 0.1
25
+
26
+ if Time.now.utc - @first_retry_at > MAX_RETRY_ELAPSE
27
+ @retry_count = nil
28
+ @first_retry_at = nil
29
+ end
30
+ end
31
+
32
+ sleep @backoff
33
+ end
34
+
35
+ def can_retry?(e = nil)
36
+ @retry_count ||= 0
37
+ @first_retry_at ||= Time.now.utc
38
+
39
+ return false if @retry_count >= MAX_RETRY_COUNT
40
+
41
+ @retry_count += 1
42
+
43
+ can_retry = case e
44
+ when *RETRYABLE_EXCEPTIONS
45
+ true
46
+ when RemoteNodeError
47
+ e.inspect.include?('Unable to acquire database lock')
48
+ else; false
49
+ end
50
+
51
+ backoff if can_retry
52
+
53
+ can_retry
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,154 @@
1
+ module Steem
2
+ module RPC
3
+ class BaseClient
4
+ include ChainConfig
5
+
6
+ attr_accessor :chain, :error_pipe
7
+
8
+ # @private
9
+ POST_HEADERS = {
10
+ 'Content-Type' => 'application/json; charset=utf-8',
11
+ 'User-Agent' => Steem::AGENT_ID
12
+ }
13
+
14
+ def initialize(options = {})
15
+ @chain = options[:chain] || :steem
16
+ @error_pipe = options[:error_pipe] || STDERR
17
+ @api_name = options[:api_name]
18
+ @url = case @chain
19
+ when :steem then options[:url] || NETWORKS_STEEM_DEFAULT_NODE
20
+ when :test then options[:url] || NETWORKS_TEST_DEFAULT_NODE
21
+ else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
22
+ end
23
+ end
24
+
25
+ def uri
26
+ @uri ||= URI.parse(@url)
27
+ end
28
+
29
+ def http
30
+ @http ||= Net::HTTP.new(uri.host, uri.port).tap do |http|
31
+ http.use_ssl = true
32
+ http.keep_alive_timeout = 2 # seconds
33
+
34
+ # WARNING This method opens a serious security hole. Never use this
35
+ # method in production code.
36
+ # http.set_debug_output(STDOUT) if !!ENV['DEBUG']
37
+ end
38
+ end
39
+
40
+ def http_post
41
+ @http_post ||= Net::HTTP::Post.new(uri.request_uri, POST_HEADERS)
42
+ end
43
+
44
+ def put(api_name = @api_name, api_method = nil, options = {})
45
+ current_rpc_id = rpc_id
46
+ rpc_method_name = "#{api_name}.#{api_method}"
47
+ options ||= {}
48
+ request_body = defined?(options.delete) ? options.delete(:request_body) : []
49
+ request_body ||= []
50
+
51
+ request_body << {
52
+ jsonrpc: '2.0',
53
+ id: current_rpc_id,
54
+ method: rpc_method_name,
55
+ params: options
56
+ }
57
+
58
+ request_body
59
+ end
60
+
61
+ def evaluate_id(options = {})
62
+ debug = options[:debug] || ENV['DEBUG'] == 'true'
63
+ request = options[:request]
64
+ response = options[:response]
65
+ api_method = options[:api_method]
66
+ req_id = request[:id].to_i
67
+ res_id = !!response['id'] ? response['id'].to_i : nil
68
+ method = [@api_name, api_method].join('.')
69
+
70
+ if debug
71
+ req = JSON.pretty_generate(request)
72
+ res = JSON.parse(response) rescue response
73
+ res = JSON.pretty_generate(response) rescue response
74
+
75
+ error_pipe.puts '=' * 80
76
+ error_pipe.puts "Request:"
77
+ error_pipe.puts req
78
+ error_pipe.puts '=' * 80
79
+ error_pipe.puts "Response:"
80
+ error_pipe.puts res
81
+ error_pipe.puts '=' * 80
82
+ error_pipe.puts Thread.current.backtrace.join("\n")
83
+ end
84
+
85
+ error = response['error'].to_json if !!response['error']
86
+
87
+ if req_id != res_id
88
+ raise IncorrectResponseIdError, "#{method}: The json-rpc id did not match. Request was: #{req_id}, got: #{res_id.inspect}", error.nil? ? nil : error.to_json
89
+ end
90
+ end
91
+
92
+ def http_request(request)
93
+ http.request(request)
94
+ end
95
+
96
+ def rpc_post(api_name = @api_name, api_method = nil, options = {}, &block)
97
+ request = http_post
98
+
99
+ request_body = if !!api_name && !!api_method
100
+ put(api_name, api_method, options)
101
+ elsif !!options && defined?(options.delete)
102
+ options.delete(:request_body)
103
+ end
104
+
105
+ request.body = if request_body.size == 1
106
+ request_body.first.to_json
107
+ else
108
+ request_body.to_json
109
+ end
110
+
111
+ response = http_request(request)
112
+
113
+ case response.code
114
+ when '200'
115
+ response = JSON[response.body]
116
+ response = case response
117
+ when Hash
118
+ Hashie::Mash.new(response).tap do |r|
119
+ evaluate_id(request: request_body.first, response: r, api_method: api_method)
120
+ end
121
+ when Array
122
+ Hashie::Array.new(response).tap do |r|
123
+ request_body.each_with_index do |req, index|
124
+ evaluate_id(request: req, response: r[index], api_method: api_method)
125
+ end
126
+ end
127
+ else; response
128
+ end
129
+
130
+ if !!block
131
+ case response
132
+ when Hashie::Mash then yield response.result, response.error, response.id
133
+ when Hashie::Array
134
+ response.each do |r|
135
+ r = Hashie::Mash.new(r)
136
+ yield r.result, r.error, r.id
137
+ end
138
+ else; yield response
139
+ end
140
+ else
141
+ return response
142
+ end
143
+ else
144
+ raise UnknownError, "#{api_name}.#{api_method}: #{response.body}"
145
+ end
146
+ end
147
+
148
+ def rpc_id
149
+ @rpc_id ||= 0
150
+ @rpc_id += 1
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,23 @@
1
+ module Steem
2
+ module RPC
3
+ # {ThreadSafeClient} is the default RPC Client used by `steem-ruby.` It's
4
+ # perfect for simple requests. But for higher performance, it's better to
5
+ # override {BaseClient} and implement something other than {Net::HTTP}.
6
+ class ThreadSafeClient < BaseClient
7
+ SEMAPHORE = Mutex.new.freeze
8
+
9
+ def http_request(request)
10
+ response = SEMAPHORE.synchronize do
11
+ http.request(request)
12
+ end
13
+ end
14
+
15
+ def rpc_id
16
+ SEMAPHORE.synchronize do
17
+ @rpc_id ||= 0
18
+ @rpc_id += 1
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,266 @@
1
+ module Steem
2
+ # {TransactionBuilder} can be used to create a transaction that the
3
+ # {NetworkBroadcastApi} can broadcast to the rest of the platform. The main
4
+ # feature of this class is the ability to cryptographically sign the
5
+ # transaction so that it conforms to the consensus rules that are required by
6
+ # the blockchain.
7
+ #
8
+ # wif = '5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC'
9
+ # builder = Steem::TransactionBuilder.new(wif: wif)
10
+ # builder.put(vote: {
11
+ # voter: 'alice',
12
+ # author: 'bob',
13
+ # permlink: 'my-burgers',
14
+ # weight: 10000
15
+ # })
16
+ #
17
+ # trx = builder.transaction
18
+ # network_broadcast_api = Steem::NetworkBroadcastApi.new
19
+ # network_broadcast_api.broadcast_transaction_synchronous(trx: trx)
20
+ #
21
+ class TransactionBuilder
22
+ include Retriable
23
+ include ChainConfig
24
+ include Utils
25
+
26
+ attr_accessor :database_api, :block_api, :wif, :expiration, :operations
27
+
28
+ def initialize(options = {})
29
+ @database_api = options[:database_api] || Steem::DatabaseApi.new(options)
30
+ @block_api = options[:block_api] || Steem::BlockApi.new(options)
31
+ @wif = options[:wif]
32
+ @ref_block_num = options[:ref_block_num]
33
+ @ref_block_prefix = options[:ref_block_prefix]
34
+ @expiration = nil
35
+ @operations = options[:operations] || []
36
+ @extensions = []
37
+ @signatures = []
38
+ @chain = options[:chain] || :steem
39
+ @error_pipe = options[:error_pipe] || STDERR
40
+ @chain_id = case @chain
41
+ when :steem then NETWORKS_STEEM_CHAIN_ID
42
+ when :test then NETWORKS_TEST_CHAIN_ID
43
+ else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
44
+ end
45
+ end
46
+
47
+ def inspect
48
+ properties = %w(
49
+ ref_block_num ref_block_prefix expiration operations
50
+ extensions signatures
51
+ ).map do |prop|
52
+ if !!(v = instance_variable_get("@#{prop}"))
53
+ "@#{prop}=#{v}"
54
+ end
55
+ end.compact.join(', ')
56
+
57
+ "#<#{self.class.name} [#{properties}]>"
58
+ end
59
+
60
+ def reset
61
+ @ref_block_num = nil
62
+ @ref_block_prefix = nil
63
+ @expiration = nil
64
+ @operations = []
65
+ @extensions = []
66
+ @signatures = []
67
+
68
+ self
69
+ end
70
+
71
+ def expired?
72
+ @expiration.nil? || @expiration < Time.now
73
+ end
74
+
75
+ # If the transaction can be prepared, this method will do so and set the
76
+ # expiration. Once the expiration is set, it will not re-prepare. If you
77
+ # call {#put}, the expiration is set {::Nil} so that it can be re-prepared.
78
+ #
79
+ # Usually, this method is called automatically by {#put} and/or {#transaction}.
80
+ #
81
+ # @return {TransactionBuilder}
82
+ def prepare
83
+ if expired?
84
+ catch :prepare_header do; begin
85
+ @database_api.get_dynamic_global_properties do |properties|
86
+ block_number = properties.last_irreversible_block_num
87
+
88
+ @block_api.get_block_header(block_num: block_number) do |result|
89
+ header = result.header
90
+
91
+ @ref_block_num = (block_number - 1) & 0xFFFF
92
+ @ref_block_prefix = unhexlify(header.previous[8..-1]).unpack('V*')[0]
93
+ @expiration = (Time.parse(properties.time + 'Z') + EXPIRE_IN_SECS).utc
94
+ end
95
+ end
96
+ rescue => e
97
+ if can_retry? e
98
+ @error_pipe.puts "#{e} ... retrying."
99
+ throw :prepare_header
100
+ else
101
+ raise e
102
+ end
103
+ end; end
104
+ end
105
+
106
+ self
107
+ end
108
+
109
+ # Sets operations all at once, then prepares.
110
+ def operations=(operations)
111
+ @operations = operations
112
+ prepare
113
+ @operations
114
+ end
115
+
116
+ # A quick and flexible way to append a new operation to the transaction.
117
+ # This method uses ducktyping to figure out how to form the operation.
118
+ #
119
+ # There are three main ways you can call this method. These assume that
120
+ # `op_type` is a {::Symbol} (or {::String}) representing the type of operation and `op` is the
121
+ # operation {::Hash}.
122
+ #
123
+ # put(op_type, op)
124
+ #
125
+ # ... or ...
126
+ #
127
+ # put(op_type => op)
128
+ #
129
+ # ... or ...
130
+ #
131
+ # put([op_type, op])
132
+ #
133
+ # You can also chain multiple operations:
134
+ #
135
+ # builder = Steem::TransactionBuilder.new
136
+ # builder.put(vote: vote1).put(vote: vote2)
137
+ # @return {TransactionBuilder}
138
+ def put(type, op = nil)
139
+ @expiration = nil
140
+
141
+ case type
142
+ when Symbol then @operations << [type, op]
143
+ when String then @operations << [type.to_sym, op]
144
+ when Hash then @operations << [type.keys.first.to_sym, type.values.first]
145
+ when Array then @operations << type
146
+ else
147
+ # don't know what to do with it, skipped
148
+ end
149
+
150
+ prepare
151
+
152
+ self
153
+ end
154
+
155
+ # If all of the required values are set, this returns a fully formed
156
+ # transaction that is ready to broadcast.
157
+ #
158
+ # @return
159
+ # {
160
+ # :ref_block_num => 18912,
161
+ # :ref_block_prefix => 575781536,
162
+ # :expiration => "2018-04-26T15:26:12",
163
+ # :extensions => [],
164
+ # :operations => [[:vote, {
165
+ # :voter => "alice",
166
+ # :author => "bob",
167
+ # :permlink => "my-burgers",
168
+ # :weight => 10000
169
+ # }
170
+ # ]],
171
+ # :signatures => ["1c45b65740b4b2c17c4bcf6bcc3f8d90ddab827d50532729fc3b8f163f2c465a532b0112ae4bf388ccc97b7c2e0bc570caadda78af48cf3c261037e65eefcd941e"]
172
+ # }
173
+ def transaction
174
+ prepare
175
+ sign
176
+ end
177
+
178
+ # Appends to the `signatures` array of the transaction, built from a
179
+ # serialized digest.
180
+ #
181
+ # @return {Hash | TransactionBuilder} The fully signed transaction if a `wif` is provided or the instance of the {TransactionBuilder} if a `wif` has not yet been provided.
182
+ def sign
183
+ return self unless !!@wif
184
+ return self if expired?
185
+
186
+ trx = {
187
+ ref_block_num: @ref_block_num,
188
+ ref_block_prefix: @ref_block_prefix,
189
+ expiration: @expiration.strftime('%Y-%m-%dT%H:%M:%S'),
190
+ operations: @operations,
191
+ extensions: @extensions,
192
+ signatures: @signatures
193
+ }
194
+
195
+ catch :serialize do; begin
196
+ @database_api.get_transaction_hex(trx: trx) do |result|
197
+ hex = @chain_id + result.hex[0..-4] # Why do we have to chop the last two bytes?
198
+ digest = unhexlify(hex)
199
+ digest_hex = Digest::SHA256.digest(digest)
200
+ private_key = Bitcoin::Key.from_base58 @wif
201
+ public_key_hex = private_key.pub
202
+ ec = Bitcoin::OpenSSL_EC
203
+ count = 0
204
+ sig = nil
205
+
206
+ loop do
207
+ count += 1
208
+ @error_pipe.puts "#{count} attempts to find canonical signature" if count % 40 == 0
209
+ sig = ec.sign_compact(digest_hex, private_key.priv, public_key_hex, false)
210
+
211
+ next if public_key_hex != ec.recover_compact(digest_hex, sig)
212
+ break if canonical? sig
213
+ end
214
+
215
+ trx[:signatures] = @signatures = [hexlify(sig)]
216
+ end
217
+ rescue => e
218
+ if can_retry? e
219
+ @error_pipe.puts "#{e} ... retrying."
220
+ throw :serialize
221
+ else
222
+ raise e
223
+ end
224
+ end; end
225
+
226
+ trx
227
+ end
228
+
229
+ # @return [Array] All public keys that could possibly sign for a given transaction.
230
+ def potential_signatures
231
+ @database_api.get_potential_signatures(trx: transaction) do |result|
232
+ result[:keys]
233
+ end
234
+ end
235
+
236
+ # This API will take a partially signed transaction and a set of public keys
237
+ # that the owner has the ability to sign for and return the minimal subset
238
+ # of public keys that should add signatures to the transaction.
239
+ #
240
+ # @return [Array] The minimal subset of public keys that should add signatures to the transaction.
241
+ def required_signatures
242
+ @database_api.get_required_signatures(trx: transaction) do |result|
243
+ result[:keys]
244
+ end
245
+ end
246
+
247
+ # @return [Boolean] True if the transaction has all of the required signatures.
248
+ def valid?
249
+ @database_api.verify_authority(trx: transaction) do |result|
250
+ result.valid
251
+ end
252
+ end
253
+ private
254
+ # See: https://github.com/steemit/steem/issues/1944
255
+ def canonical?(sig)
256
+ sig = sig.unpack('C*')
257
+
258
+ !(
259
+ ((sig[0] & 0x80 ) != 0) || ( sig[0] == 0 ) ||
260
+ ((sig[1] & 0x80 ) != 0) ||
261
+ ((sig[32] & 0x80 ) != 0) || ( sig[32] == 0 ) ||
262
+ ((sig[33] & 0x80 ) != 0)
263
+ )
264
+ end
265
+ end
266
+ end