steem-ruby 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +52 -0
- data/Rakefile +119 -1
- data/lib/steem/api.rb +12 -7
- data/lib/steem/base_error.rb +47 -19
- data/lib/steem/block_api.rb +29 -16
- data/lib/steem/broadcast.rb +100 -10
- data/lib/steem/jsonrpc.rb +25 -15
- data/lib/steem/mixins/retriable.rb +2 -6
- data/lib/steem/rpc/base_client.rb +88 -75
- data/lib/steem/rpc/http_client.rb +126 -0
- data/lib/steem/rpc/thread_safe_http_client.rb +35 -0
- data/lib/steem/transaction_builder.rb +17 -0
- data/lib/steem/version.rb +1 -1
- data/lib/steem.rb +3 -2
- metadata +4 -3
- data/lib/steem/rpc/thread_safe_client.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24650742975337b2832ba5ab426f609a5ff8c9ef
|
4
|
+
data.tar.gz: 827c4848766e9b54dc8f04f5e20c920d3964c4eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40b66f82639c1db97bf844bc07414f4ef512decec1fa4bf99bbc789ac1916379d96d03eebc417a89a7fcc7dcbbcbd9c29d82c3dd08958e3331712b2af4450f04
|
7
|
+
data.tar.gz: 37183706e9bdb150df65afb03d697c6a8cca050d3e3a3ac605054e46ba10d6a0897dc28e55a8035cc10f0a8df7d144fe548bd887344b132fa47f29518ae945be
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,28 @@
|
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/steem-ruby.svg)](https://badge.fury.io/rb/steem-ruby)
|
2
|
+
[![Inline docs](http://inch-ci.org/github/steemit/steem-ruby.svg?branch=master&style=shields)](http://inch-ci.org/github/steemit/steem-ruby)
|
3
|
+
|
1
4
|
# `steem-ruby`
|
2
5
|
|
3
6
|
Steem-ruby the Ruby API for Steem blockchain.
|
4
7
|
|
5
8
|
Full documentation: http://www.rubydoc.info/gems/steem-ruby
|
6
9
|
|
10
|
+
## `radiator` vs. `steem-ruby`
|
11
|
+
|
12
|
+
The `steem-ruby` gem was written from the ground up by `@inertia`, who is also the author of [`radiator`](https://github.com/inertia186/radiator).
|
13
|
+
|
14
|
+
> "I intend to continue work on `radiator` indefinitely. But in `radiator-0.5`, I intend to refactor `radiator` so that is uses `steem-ruby` as its core. This means that some features of `radiator` like Serialization will become redundant. I think it's still useful for radiator to do its own serialzation because it reduces the number of API requests." - @inertia
|
15
|
+
|
16
|
+
| `radiator` | `steem-ruby` |
|
17
|
+
|-|-|
|
18
|
+
| Has internal failover logic | Can have failover delegated externally |
|
19
|
+
| Passes `error` responses to the caller | Handles `error` responses and raises exceptions |
|
20
|
+
| Supports tx signing, does its own serialization | Also supports tx signing, but delegates serialization to `database_api.get_transaction_hex` |
|
21
|
+
| All apis and methods are hardcoded | Asks `jsonrpc` what apis and methods are available from the node |
|
22
|
+
| (`radiator-0.4.x`) Only supports AppBase but relies on `condenser_api` | Only supports AppBase but does not rely on `condenser_api` **(WIP)**
|
23
|
+
| Small list of helper methods for select ops (in addition to build your own transaction) | Complete implementation of helper methods for every op (in addition to build your own transaction) |
|
24
|
+
| Does not (yet) support `json-rpc-batch` requests | Supports `json-rpc-batch` requests |
|
25
|
+
|
7
26
|
## Getting Started
|
8
27
|
|
9
28
|
The steem-ruby gem is compatible with Ruby 2.2.5 or later.
|
@@ -47,6 +66,8 @@ Steem::Broadcast.vote(wif: wif, params: params) do |result|
|
|
47
66
|
end
|
48
67
|
```
|
49
68
|
|
69
|
+
*See: [Broadcast](https://www.rubydoc.info/gems/steem-ruby/Steem/Broadcast)*
|
70
|
+
|
50
71
|
### Get Accounts
|
51
72
|
|
52
73
|
```ruby
|
@@ -57,6 +78,8 @@ api.find_accounts(accounts: ['steemit', 'alice']) do |result|
|
|
57
78
|
end
|
58
79
|
```
|
59
80
|
|
81
|
+
*See: [Api](https://www.rubydoc.info/gems/steem-ruby/Steem/Api)*
|
82
|
+
|
60
83
|
### Reputation Formatter
|
61
84
|
|
62
85
|
```ruby
|
@@ -64,6 +87,35 @@ rep = Steem::Formatter.reputation(account.reputation)
|
|
64
87
|
puts rep
|
65
88
|
```
|
66
89
|
|
90
|
+
### Tests
|
91
|
+
|
92
|
+
* Clone the client repository into a directory of your choice:
|
93
|
+
* `git clone https://github.com/steemit/steem-ruby.git`
|
94
|
+
* Navigate into the new folder
|
95
|
+
* `cd steem-ruby`
|
96
|
+
* All tests can be invoked as follows:
|
97
|
+
* `bundle exec rake test`
|
98
|
+
* To run `static` tests:
|
99
|
+
* `bundle exec rake test:static`
|
100
|
+
* To run `broadcast` tests (broadcast is simulated, only `verify` is actually used):
|
101
|
+
* `bundle exec rake test:broadcast`
|
102
|
+
* To run `threads` tests (which quickly verifies thread safety):
|
103
|
+
* `bundle exec rake test:threads`
|
104
|
+
* To run `testnet` tests (which does actual broadcasts)
|
105
|
+
* `TEST_NODE=https://testnet.steemitdev.com bundle exec rake test:testnet`
|
106
|
+
|
107
|
+
You can also run other tests that are not part of the above `test` execution:
|
108
|
+
|
109
|
+
* To run `block_range`, which streams blocks (using `json-rpc-batch`)
|
110
|
+
* `bundle exec rake stream:block_range`
|
111
|
+
|
112
|
+
|
113
|
+
If you want to point to any node for tests, instead of letting the test suite pick the default, set the environment variable to `TEST_NODE`, e.g.:
|
114
|
+
|
115
|
+
```bash
|
116
|
+
$ TEST_NODE=https://api.steemitdev.com bundle exec rake test
|
117
|
+
```
|
118
|
+
|
67
119
|
## Contributions
|
68
120
|
|
69
121
|
Patches are welcome! Contributors are listed in the `steem-ruby.gemspec` file. Please run the tests (`rake test`) before opening a pull request and make sure that you are passing all of them. If you would like to contribute, but don't know what to work on, check the issues list.
|
data/Rakefile
CHANGED
@@ -3,7 +3,7 @@ require 'rake/testtask'
|
|
3
3
|
require 'yard'
|
4
4
|
require 'steem'
|
5
5
|
|
6
|
-
Rake::TestTask.new(test: 'clean:vcr') do |t|
|
6
|
+
Rake::TestTask.new(test: ['clean:vcr', 'test:threads']) do |t|
|
7
7
|
t.libs << 'test'
|
8
8
|
t.libs << 'lib'
|
9
9
|
t.test_files = FileList['test/**/*_test.rb']
|
@@ -59,6 +59,124 @@ namespace :test do
|
|
59
59
|
'-W1'
|
60
60
|
end
|
61
61
|
end
|
62
|
+
|
63
|
+
Rake::TestTask.new(testnet: 'clean:vcr') do |t|
|
64
|
+
t.description = <<-EOD
|
65
|
+
Run testnet tests, which are those that use network_broadcast_api to do
|
66
|
+
actual broadcast operations, on a specified (or default) testnet.
|
67
|
+
EOD
|
68
|
+
t.libs << 'test'
|
69
|
+
t.libs << 'lib'
|
70
|
+
t.test_files = [
|
71
|
+
'test/steem/testnet_test.rb'
|
72
|
+
]
|
73
|
+
t.ruby_opts << if ENV['HELL_ENABLED']
|
74
|
+
'-W2'
|
75
|
+
else
|
76
|
+
'-W1'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
desc 'Tests the API using multiple threads.'
|
81
|
+
task :threads do
|
82
|
+
threads = []
|
83
|
+
api = Steem::Api.new(url: ENV['TEST_NODE'])
|
84
|
+
database_api = Steem::DatabaseApi.new(url: ENV['TEST_NODE'])
|
85
|
+
witnesses = {}
|
86
|
+
keys = %i(created url total_missed props running_version
|
87
|
+
hardfork_version_vote hardfork_time_vote)
|
88
|
+
|
89
|
+
if defined? Thread.report_on_exception
|
90
|
+
Thread.report_on_exception = true
|
91
|
+
end
|
92
|
+
|
93
|
+
database_api.get_active_witnesses do |result|
|
94
|
+
print "Found #{result.witnesses.size} witnesses ..."
|
95
|
+
|
96
|
+
result.witnesses.each do |witness_name|
|
97
|
+
threads << Thread.new do
|
98
|
+
api.get_witness_by_account(witness_name) do |witness|
|
99
|
+
witnesses[witness.owner] = witness.map do |k, v|
|
100
|
+
[k, v] if keys.include? k.to_sym
|
101
|
+
end.compact.to_h
|
102
|
+
|
103
|
+
sbd_exchange_rate = witness[:sbd_exchange_rate]
|
104
|
+
base = sbd_exchange_rate[:base].to_f
|
105
|
+
|
106
|
+
if (quote = sbd_exchange_rate[:quote].to_f) > 0
|
107
|
+
rate = (base / quote).round(3)
|
108
|
+
witnesses[witness.owner][:sbd_exchange_rate] = rate
|
109
|
+
else
|
110
|
+
witnesses[witness.owner][:sbd_exchange_rate] = nil
|
111
|
+
end
|
112
|
+
|
113
|
+
last_sbd_exchange_update = witness[:last_sbd_exchange_update]
|
114
|
+
last_sbd_exchange_update = Time.parse(last_sbd_exchange_update + 'Z')
|
115
|
+
last_sbd_exchange_elapsed = '%.2f hours ago' % ((Time.now.utc - last_sbd_exchange_update) / 60)
|
116
|
+
witnesses[witness.owner][:last_sbd_exchange_elapsed] = last_sbd_exchange_elapsed
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
threads.each do |thread|
|
123
|
+
print '.'
|
124
|
+
thread.join
|
125
|
+
end
|
126
|
+
|
127
|
+
puts ' done!'
|
128
|
+
|
129
|
+
if threads.size != witnesses.size
|
130
|
+
puts "Bug: expected #{threads.size} witnesses, only found #{witnesses.size}."
|
131
|
+
else
|
132
|
+
puts JSON.pretty_generate witnesses rescue puts witnesses
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
namespace :stream do
|
138
|
+
desc 'Test the ability to stream a block range.'
|
139
|
+
task :block_range do
|
140
|
+
block_api = Steem::BlockApi.new(url: ENV['TEST_NODE'])
|
141
|
+
api = Steem::Api.new(url: ENV['TEST_NODE'])
|
142
|
+
last_block_num = nil
|
143
|
+
first_block_num = nil
|
144
|
+
last_timestamp = nil
|
145
|
+
|
146
|
+
loop do
|
147
|
+
api.get_dynamic_global_properties do |properties|
|
148
|
+
current_block_num = properties.last_irreversible_block_num
|
149
|
+
# First pass replays latest 100 blocks.
|
150
|
+
first_block_num ||= current_block_num - (rand * 200).to_i
|
151
|
+
|
152
|
+
if current_block_num >= first_block_num
|
153
|
+
range = first_block_num..current_block_num
|
154
|
+
puts "Got block range: #{range.size}"
|
155
|
+
block_api.get_blocks(block_range: range) do |block, block_num|
|
156
|
+
current_timestamp = Time.parse(block.timestamp + 'Z')
|
157
|
+
|
158
|
+
if !!last_timestamp && block_num != last_block_num + 1
|
159
|
+
puts "Bug: Last block number was #{last_block_num} then jumped to: #{block_num}"
|
160
|
+
exit
|
161
|
+
end
|
162
|
+
|
163
|
+
if !!last_timestamp && current_timestamp < last_timestamp
|
164
|
+
puts "Bug: Went back in time. Last timestamp was #{last_timestamp}, then jumped back to #{current_timestamp}"
|
165
|
+
exit
|
166
|
+
end
|
167
|
+
|
168
|
+
puts "\t#{block_num} Timestamp: #{current_timestamp}, witness: #{block.witness}"
|
169
|
+
last_block_num = block_num
|
170
|
+
last_timestamp = current_timestamp
|
171
|
+
end
|
172
|
+
|
173
|
+
first_block_num = range.max + 1
|
174
|
+
end
|
175
|
+
|
176
|
+
sleep 3
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
62
180
|
end
|
63
181
|
|
64
182
|
YARD::Rake::YardocTask.new do |t|
|
data/lib/steem/api.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Steem
|
2
2
|
# This ruby API works with
|
3
|
-
# {https://github.com/steemit/steem/releases steemd-0.19.4} and other
|
3
|
+
# {https://github.com/steemit/steem/releases steemd-0.19.4} and other AppBase
|
4
4
|
# compatible upstreams. To access different API namespaces, use the
|
5
5
|
# following:
|
6
6
|
#
|
@@ -34,13 +34,13 @@ module Steem
|
|
34
34
|
# * {TagsApi}
|
35
35
|
# * {WitnessApi}
|
36
36
|
#
|
37
|
-
# Also see: {https://developers.steem.io/apidefinitions
|
37
|
+
# Also see: {https://developers.steem.io/apidefinitions/ Complete API Definitions}
|
38
38
|
class Api
|
39
39
|
attr_accessor :chain, :methods
|
40
40
|
|
41
41
|
# Use this for debugging naive thread handler.
|
42
42
|
# DEFAULT_RPC_CLIENT = RPC::BaseClient
|
43
|
-
DEFAULT_RPC_CLIENT = RPC::
|
43
|
+
DEFAULT_RPC_CLIENT = RPC::ThreadSafeHttpClient
|
44
44
|
|
45
45
|
def self.api_name=(api_name)
|
46
46
|
@api_name = api_name.to_s.
|
@@ -103,16 +103,19 @@ module Steem
|
|
103
103
|
"#<#{self.class.api_class_name} [#{properties}]>"
|
104
104
|
end
|
105
105
|
private
|
106
|
+
# @private
|
106
107
|
def self.args_keys_to_s(rpc_method_name)
|
107
108
|
args = signature(rpc_method_name).args
|
108
109
|
args_keys = JSON[args.to_json]
|
109
110
|
end
|
110
111
|
|
112
|
+
# @private
|
111
113
|
def self.signature(rpc_method_name)
|
112
114
|
@@signatures ||= {}
|
113
115
|
@@signatures[rpc_method_name] ||= jsonrpc.get_signature(method: rpc_method_name).result
|
114
116
|
end
|
115
117
|
|
118
|
+
# @private
|
116
119
|
def self.raise_error_response(rpc_method_name, rpc_args, response)
|
117
120
|
raise UnknownError, "#{rpc_method_name}: #{response}" if response.error.nil?
|
118
121
|
|
@@ -125,10 +128,12 @@ module Steem
|
|
125
128
|
BaseError.build_error(error, rpc_method_name)
|
126
129
|
end
|
127
130
|
|
131
|
+
# @private
|
128
132
|
def respond_to_missing?(m, include_private = false)
|
129
133
|
methods.nil? ? false : methods.include?(m.to_sym)
|
130
134
|
end
|
131
135
|
|
136
|
+
# @private
|
132
137
|
def method_missing(m, *args, &block)
|
133
138
|
super unless respond_to_missing?(m)
|
134
139
|
|
@@ -150,19 +155,19 @@ module Steem
|
|
150
155
|
# Some argument are optional, but if the arguments passed are greater
|
151
156
|
# than the expected arguments size, we can warn.
|
152
157
|
if args_size > expected_args_size
|
153
|
-
error_pipe.puts "Warning #{rpc_method_name} expects arguments: #{expected_args_size}, got: #{args_size}"
|
158
|
+
@error_pipe.puts "Warning #{rpc_method_name} expects arguments: #{expected_args_size}, got: #{args_size}"
|
154
159
|
end
|
155
160
|
rescue NoMethodError => e
|
156
161
|
error = Steem::ArgumentError.new("#{rpc_method_name} expects arguments: #{expected_args_size}", e)
|
157
162
|
raise error
|
158
|
-
rescue
|
159
|
-
raise
|
163
|
+
rescue => e
|
164
|
+
raise UnknownError.new("#{rpc_method_name} unknown error.", e)
|
160
165
|
end
|
161
166
|
|
162
167
|
args
|
163
168
|
end
|
164
169
|
|
165
|
-
response = @rpc_client.
|
170
|
+
response = @rpc_client.rpc_execute(@api_name, m, rpc_args)
|
166
171
|
|
167
172
|
if defined?(response.error) && !!response.error
|
168
173
|
if !!response.error.message
|
data/lib/steem/base_error.rb
CHANGED
@@ -15,7 +15,7 @@ module Steem
|
|
15
15
|
|
16
16
|
def self.build_error(error, context)
|
17
17
|
if error.message == 'Unable to acquire database lock'
|
18
|
-
raise Steem::
|
18
|
+
raise Steem::RemoteDatabaseLockError, error.message, JSON.pretty_generate(error)
|
19
19
|
end
|
20
20
|
|
21
21
|
if error.message.include? 'Internal Error'
|
@@ -23,7 +23,7 @@ module Steem
|
|
23
23
|
end
|
24
24
|
|
25
25
|
if error.message.include? 'plugin not enabled'
|
26
|
-
raise Steem::
|
26
|
+
raise Steem::PluginNotEnabledError, error.message, JSON.pretty_generate(error)
|
27
27
|
end
|
28
28
|
|
29
29
|
if error.message.include? 'argument'
|
@@ -43,9 +43,29 @@ module Steem
|
|
43
43
|
end
|
44
44
|
|
45
45
|
if error.message.include? 'unknown key'
|
46
|
+
raise Steem::ArgumentError, "#{context}: #{error.message} (or content has been deleted)", JSON.pretty_generate(error)
|
47
|
+
end
|
48
|
+
|
49
|
+
if error.message.include? 'Comment is not in account\'s comments'
|
50
|
+
raise Steem::ArgumentError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
51
|
+
end
|
52
|
+
|
53
|
+
if error.message.include? 'Could not find comment'
|
54
|
+
raise Steem::ArgumentError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
55
|
+
end
|
56
|
+
|
57
|
+
if error.message.include? 'unable to convert ISO-formatted string to fc::time_point_sec'
|
58
|
+
raise Steem::ArgumentError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
59
|
+
end
|
60
|
+
|
61
|
+
if error.message.include? 'Input data have to treated as object.'
|
46
62
|
raise Steem::ArgumentError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
47
63
|
end
|
48
64
|
|
65
|
+
if error.message.include? 'blk->transactions.size() > itr->trx_in_block'
|
66
|
+
raise Steem::VirtualOperationsNotAllowedError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
67
|
+
end
|
68
|
+
|
49
69
|
if error.message.include? 'A transaction must have at least one operation'
|
50
70
|
raise Steem::EmptyTransactionError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
51
71
|
end
|
@@ -86,6 +106,10 @@ module Steem
|
|
86
106
|
raise Steem::MissingOtherAuthorityError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
87
107
|
end
|
88
108
|
|
109
|
+
if error.message.include? 'operator has disabled operation indexing by transaction_id'
|
110
|
+
raise Steem::TransactionIndexDisabledError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
111
|
+
end
|
112
|
+
|
89
113
|
if error.message.include? 'is_valid_account_name'
|
90
114
|
raise Steem::InvalidAccountError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
91
115
|
end
|
@@ -114,10 +138,6 @@ module Steem
|
|
114
138
|
raise Steem::UnexpectedAssetError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
115
139
|
end
|
116
140
|
|
117
|
-
if error.message.include? 'unable to convert ISO-formatted string to fc::time_point_sec'
|
118
|
-
raise Steem::ArgumentError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
119
|
-
end
|
120
|
-
|
121
141
|
puts JSON.pretty_generate(error) if ENV['DEBUG']
|
122
142
|
raise UnknownError, "#{context}: #{error.message}", JSON.pretty_generate(error)
|
123
143
|
end
|
@@ -126,26 +146,34 @@ module Steem
|
|
126
146
|
class UnsupportedChainError < BaseError; end
|
127
147
|
class ArgumentError < BaseError; end
|
128
148
|
class RemoteNodeError < BaseError; end
|
149
|
+
class RemoteDatabaseLockError < RemoteNodeError; end
|
150
|
+
class PluginNotEnabledError < RemoteNodeError; end
|
129
151
|
class TypeError < BaseError; end
|
130
|
-
class EmptyTransactionError <
|
152
|
+
class EmptyTransactionError < ArgumentError; end
|
153
|
+
class InvalidAccountError < ArgumentError; end
|
154
|
+
class AuthorNotFoundError < ArgumentError; end
|
155
|
+
class ReachedMaximumTimeError < ArgumentError; end
|
156
|
+
class VirtualOperationsNotAllowedError < ArgumentError; end
|
157
|
+
class TheftError < ArgumentError; end
|
158
|
+
class NonZeroRequiredError < ArgumentError; end
|
159
|
+
class UnexpectedAssetError < ArgumentError; end
|
131
160
|
class TransactionExpiredError < BaseError; end
|
132
|
-
class DuplicateTransactionError <
|
133
|
-
class NonCanonicalSignatureError <
|
161
|
+
class DuplicateTransactionError < TransactionExpiredError; end
|
162
|
+
class NonCanonicalSignatureError < TransactionExpiredError; end
|
134
163
|
class BlockTooOldError < BaseError; end
|
135
164
|
class IrrelevantSignatureError < BaseError; end
|
136
|
-
class
|
137
|
-
class
|
138
|
-
class
|
139
|
-
class
|
140
|
-
class
|
141
|
-
class AuthorNotFoundError < BaseError; end
|
142
|
-
class ReachedMaximumTimeError < BaseError; end
|
143
|
-
class TheftError < BaseError; end
|
144
|
-
class NonZeroRequiredError < BaseError; end
|
145
|
-
class UnexpectedAssetError < BaseError; end
|
165
|
+
class MissingAuthorityError < BaseError; end
|
166
|
+
class MissingPostingAuthorityError < MissingAuthorityError; end
|
167
|
+
class MissingActiveAuthorityError < MissingAuthorityError; end
|
168
|
+
class MissingOwnerAuthorityError < MissingAuthorityError; end
|
169
|
+
class MissingOtherAuthorityError < MissingAuthorityError; end
|
146
170
|
class IncorrectRequestIdError < BaseError; end
|
147
171
|
class IncorrectResponseIdError < BaseError; end
|
172
|
+
class TransactionIndexDisabledError < BaseError; end
|
173
|
+
class NotAppBaseError < BaseError; end
|
148
174
|
class UnknownApiError < BaseError; end
|
149
175
|
class UnknownOperationError < BaseError; end
|
176
|
+
class JsonRpcBatchMaximumSizeExceededError < BaseError; end
|
177
|
+
class TooManyTimeoutsError < BaseError; end
|
150
178
|
class UnknownError < BaseError; end
|
151
179
|
end
|
data/lib/steem/block_api.rb
CHANGED
@@ -5,7 +5,7 @@ module Steem
|
|
5
5
|
#
|
6
6
|
# Also see: {https://developers.steem.io/apidefinitions/block-api Block API Definitions}
|
7
7
|
class BlockApi < Api
|
8
|
-
MAX_RANGE_SIZE =
|
8
|
+
MAX_RANGE_SIZE = 50
|
9
9
|
|
10
10
|
def initialize(options = {})
|
11
11
|
self.class.api_name = :block_api
|
@@ -16,30 +16,43 @@ module Steem
|
|
16
16
|
#
|
17
17
|
# @param options [Hash] The attributes to get a block range with.
|
18
18
|
# @option options [Range] :block_range starting on one block number and ending on an higher block number.
|
19
|
-
def get_blocks(options = {block_range:
|
20
|
-
block_range = options[:block_range] ||
|
19
|
+
def get_blocks(options = {block_range: (0..0)}, &block)
|
20
|
+
block_range = options[:block_range] || (0..0)
|
21
21
|
|
22
|
-
if block_range.
|
23
|
-
raise Steem::ArgumentError, "
|
22
|
+
if (start = block_range.first) < 1
|
23
|
+
raise Steem::ArgumentError, "Invalid starting block: #{start}"
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
chunks = if block_range.size > MAX_RANGE_SIZE
|
27
|
+
block_range.each_slice(MAX_RANGE_SIZE)
|
28
|
+
else
|
29
|
+
[block_range]
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
for sub_range in chunks do
|
33
|
+
request_object = []
|
34
|
+
|
35
|
+
for i in sub_range do
|
36
|
+
@rpc_client.put(self.class.api_name, :get_block, block_num: i, request_object: request_object)
|
35
37
|
end
|
36
|
-
else
|
37
|
-
blocks = []
|
38
38
|
|
39
|
-
|
40
|
-
|
39
|
+
if !!block
|
40
|
+
index = 0
|
41
|
+
@rpc_client.rpc_batch_execute(request_object: request_object) do |result, error, id|
|
42
|
+
block_num = sub_range.to_a[index]
|
43
|
+
index = index += 1
|
44
|
+
yield(result.nil? ? nil : result.block, block_num)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
blocks = []
|
48
|
+
|
49
|
+
@rpc_client.rpc_batch_execute(request_object: request_object) do |result, error, id|
|
50
|
+
blocks << result
|
51
|
+
end
|
41
52
|
end
|
42
53
|
end
|
54
|
+
|
55
|
+
blocks
|
43
56
|
end
|
44
57
|
end
|
45
58
|
end
|
data/lib/steem/broadcast.rb
CHANGED
@@ -105,6 +105,24 @@ module Steem
|
|
105
105
|
# }
|
106
106
|
#
|
107
107
|
# Steem::Broadcast.comment(options)
|
108
|
+
#
|
109
|
+
# In addition to the above denormalized `comment_options` fields, the author
|
110
|
+
# can also vote for the content in the same transaction by setting `author_vote_weight`:
|
111
|
+
#
|
112
|
+
# options = {
|
113
|
+
# wif: wif,
|
114
|
+
# params: {
|
115
|
+
# author: author,
|
116
|
+
# title: 'This is my fancy post title.',
|
117
|
+
# body: 'This is my fancy post body.',
|
118
|
+
# metadata: {
|
119
|
+
# tags: %w(these are my fancy tags)
|
120
|
+
# },
|
121
|
+
# author_vote_weight: 10000
|
122
|
+
# }
|
123
|
+
# }
|
124
|
+
#
|
125
|
+
# Steem::Broadcast.comment(options)
|
108
126
|
#
|
109
127
|
# @param options [Hash] options
|
110
128
|
# @option options [String] :wif Posting wif
|
@@ -113,6 +131,7 @@ module Steem
|
|
113
131
|
# * :title (String) Title of the content.
|
114
132
|
# * :body (String) Body of the content.
|
115
133
|
# * :metadata (Hash) Metadata of the content, becomes `json_metadata`.
|
134
|
+
# * :json_metadata (String) String version of `metadata` (use one or the other).
|
116
135
|
# * :permlink (String) (automatic) Permlink of the content, defaults to formatted title.
|
117
136
|
# * :parent_permlink (String) (automatic) Parent permlink of the content, defaults to first tag.
|
118
137
|
# * :parent_author (String) (optional) Parent author of the content (only used if reply).
|
@@ -121,12 +140,19 @@ module Steem
|
|
121
140
|
# * :allow_votes (Numeric) (true) Allow votes for this content.
|
122
141
|
# * :allow_curation_rewards (Numeric) (true) Allow curation rewards for this content.
|
123
142
|
# * :beneficiaries (Array<Hash>) Sets the beneficiaries of this content.
|
143
|
+
# * :author_vote_weight (Number) (optional) Cast a vote by the author in the same transaction.
|
124
144
|
# * :pretend (Boolean) Just validate, do not broadcast.
|
125
145
|
# @see https://developers.steem.io/apidefinitions/broadcast-ops#broadcast_ops_comment
|
126
146
|
def self.comment(options, &block)
|
127
147
|
required_fields = %i(author body permlink parent_permlink)
|
128
148
|
params = options[:params]
|
129
|
-
|
149
|
+
|
150
|
+
if !!params[:metadata] && !!params[:json_metadata]
|
151
|
+
raise Steem::ArgumentError, 'Assign either metadata or json_metadata, not both.'
|
152
|
+
end
|
153
|
+
|
154
|
+
metadata = params[:metadata] || {}
|
155
|
+
metadata ||= (JSON[params[:json_metadata]] || nil) || {}
|
130
156
|
metadata['app'] ||= Steem::AGENT_ID
|
131
157
|
tags = metadata['tags'] || []
|
132
158
|
params[:parent_permlink] ||= tags.first
|
@@ -181,6 +207,17 @@ module Steem
|
|
181
207
|
|
182
208
|
ops << [:comment_options, comment_options]
|
183
209
|
|
210
|
+
if !!params[:author_vote_weight]
|
211
|
+
author_vote = {
|
212
|
+
voter: params[:author],
|
213
|
+
author: params[:author],
|
214
|
+
permlink: params[:permlink],
|
215
|
+
weight: params[:author_vote_weight]
|
216
|
+
}
|
217
|
+
|
218
|
+
ops << [:vote, author_vote]
|
219
|
+
end
|
220
|
+
|
184
221
|
process(options.merge(ops: ops), &block)
|
185
222
|
end
|
186
223
|
|
@@ -374,7 +411,7 @@ module Steem
|
|
374
411
|
params = options[:params]
|
375
412
|
check_required_fields(params, *required_fields)
|
376
413
|
|
377
|
-
exchange_rate = params[:exchange_rate] rescue {}
|
414
|
+
exchange_rate = params[:exchange_rate] rescue nil || {}
|
378
415
|
base = exchange_rate[:base]
|
379
416
|
quote = exchange_rate[:quote]
|
380
417
|
params[:exchange_rate][:base] = Type::Amount.to_nia(base)
|
@@ -447,12 +484,22 @@ module Steem
|
|
447
484
|
# * :active (Hash)
|
448
485
|
# * :posting (Hash)
|
449
486
|
# * :memo_key (String)
|
450
|
-
# * :
|
487
|
+
# * :metadata (Hash) Metadata of the account, becomes `json_metadata`.
|
488
|
+
# * :json_metadata (String) String version of `metadata` (use one or the other).
|
451
489
|
# @option options [Boolean] :pretend Just validate, do not broadcast.
|
452
490
|
# @see https://developers.steem.io/apidefinitions/broadcast-ops#broadcast_ops_account_create
|
453
491
|
def self.account_create(options, &block)
|
454
492
|
required_fields = %i(fee creator new_account_name owner active posting memo_key json_metadata)
|
455
493
|
params = options[:params]
|
494
|
+
|
495
|
+
if !!params[:metadata] && !!params[:json_metadata]
|
496
|
+
raise Steem::ArgumentError, 'Assign either metadata or json_metadata, not both.'
|
497
|
+
end
|
498
|
+
|
499
|
+
metadata = params.delete(:metadata) || {}
|
500
|
+
metadata ||= (JSON[params[:json_metadata]] || nil) || {}
|
501
|
+
params[:json_metadata] = metadata.to_json
|
502
|
+
|
456
503
|
check_required_fields(params, *required_fields)
|
457
504
|
|
458
505
|
params[:fee] = Type::Amount.to_nia(params[:fee])
|
@@ -497,12 +544,22 @@ module Steem
|
|
497
544
|
# * :active (Hash) (optional)
|
498
545
|
# * :posting (Hash) (optional)
|
499
546
|
# * :memo_key (String) (optional)
|
500
|
-
#
|
547
|
+
# * :metadata (Hash) Metadata of the account, becomes `json_metadata`.
|
548
|
+
# * :json_metadata (String) String version of `metadata` (use one or the other).
|
501
549
|
# @option options [Boolean] :pretend Just validate, do not broadcast.
|
502
550
|
# @see https://developers.steem.io/apidefinitions/broadcast-ops#broadcast_ops_account_update
|
503
551
|
def self.account_update(options, &block)
|
504
552
|
required_fields = %i(account)
|
505
553
|
params = options[:params]
|
554
|
+
|
555
|
+
if !!params[:metadata] && !!params[:json_metadata]
|
556
|
+
raise Steem::ArgumentError, 'Assign either metadata or json_metadata, not both.'
|
557
|
+
end
|
558
|
+
|
559
|
+
metadata = params.delete(:metadata) || {}
|
560
|
+
metadata ||= (JSON[params[:json_metadata]] || nil) || {}
|
561
|
+
params[:json_metadata] = metadata.to_json
|
562
|
+
|
506
563
|
check_required_fields(params, *required_fields)
|
507
564
|
|
508
565
|
ops = [[:account_update, params]]
|
@@ -642,12 +699,22 @@ module Steem
|
|
642
699
|
# * :required_auths (Array<String>)
|
643
700
|
# * :required_posting_auths (Arrat<String>)
|
644
701
|
# * :id (String)
|
645
|
-
# * :
|
702
|
+
# * :data (Hash) Data of the custom json, becomes `json`.
|
703
|
+
# * :json (String) String version of `data` (use one or the other).
|
646
704
|
# @option options [Boolean] :pretend Just validate, do not broadcast.
|
647
705
|
# @see https://developers.steem.io/apidefinitions/broadcast-ops#broadcast_ops_custom_json
|
648
706
|
def self.custom_json(options, &block)
|
649
|
-
required_fields = %i(id
|
707
|
+
required_fields = %i(id)
|
650
708
|
params = options[:params]
|
709
|
+
|
710
|
+
if !!params[:data] && !!params[:json]
|
711
|
+
raise Steem::ArgumentError, 'Assign either data or json, not both.'
|
712
|
+
end
|
713
|
+
|
714
|
+
data = params.delete(:data) || {}
|
715
|
+
data ||= (JSON[params[:json]] || nil) || {}
|
716
|
+
params[:json] = data.to_json
|
717
|
+
|
651
718
|
check_required_fields(params, *required_fields)
|
652
719
|
|
653
720
|
params[:required_auths] ||= []
|
@@ -759,12 +826,22 @@ module Steem
|
|
759
826
|
# * :fee (String)
|
760
827
|
# * :ratification_deadline (String)
|
761
828
|
# * :escrow_expiration (String)
|
762
|
-
# * :
|
829
|
+
# * :meta (Hash) Meta of the escrow transfer, becomes `json_meta`.
|
830
|
+
# * :json_meta (String) String version of `metadata` (use one or the other).
|
763
831
|
# @option options [Boolean] :pretend Just validate, do not broadcast.
|
764
832
|
# @see https://developers.steem.io/apidefinitions/broadcast-ops#broadcast_ops_escrow_transfer
|
765
833
|
def self.escrow_transfer(options, &block)
|
766
|
-
required_fields = %i(from to agent escrow_id fee ratification_deadline
|
834
|
+
required_fields = %i(from to agent escrow_id fee ratification_deadline)
|
767
835
|
params = options[:params]
|
836
|
+
|
837
|
+
if !!params[:meta] && !!params[:json_meta]
|
838
|
+
raise Steem::ArgumentError, 'Assign either meta or json_meta, not both.'
|
839
|
+
end
|
840
|
+
|
841
|
+
meta = params.delete(:meta) || {}
|
842
|
+
meta ||= (JSON[params[:json_meta]] || nil) || {}
|
843
|
+
params[:json_meta] = meta.to_json
|
844
|
+
|
768
845
|
check_required_fields(params, *required_fields)
|
769
846
|
|
770
847
|
params[:sbd_amount] = Type::Amount.to_nia(params[:sbd_amount])
|
@@ -979,13 +1056,23 @@ module Steem
|
|
979
1056
|
# * :active (String)
|
980
1057
|
# * :posting (String)
|
981
1058
|
# * :memo_key (String)
|
982
|
-
# * :
|
1059
|
+
# * :metadata (Hash) Metadata of the account, becomes `json_metadata`.
|
1060
|
+
# * :json_metadata (String) String version of `metadata` (use one or the other).
|
983
1061
|
# * :extensions (Array)
|
984
1062
|
# @option options [Boolean] :pretend Just validate, do not broadcast.
|
985
1063
|
# @see https://developers.steem.io/apidefinitions/broadcast-ops#broadcast_ops_account_create_with_delegation
|
986
1064
|
def self.account_create_with_delegation(options, &block)
|
987
|
-
required_fields = %i(fee delegation creator new_account_name owner active posting memo_key
|
1065
|
+
required_fields = %i(fee delegation creator new_account_name owner active posting memo_key)
|
988
1066
|
params = options[:params]
|
1067
|
+
|
1068
|
+
if !!params[:metadata] && !!params[:json_metadata]
|
1069
|
+
raise Steem::ArgumentError, 'Assign either metadata or json_metadata, not both.'
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
metadata = params.delete(:metadata) || {}
|
1073
|
+
metadata ||= (JSON[params[:json_metadata]] || nil) || {}
|
1074
|
+
params[:json_metadata] = metadata.to_json
|
1075
|
+
|
989
1076
|
check_required_fields(params, *required_fields)
|
990
1077
|
|
991
1078
|
params[:fee] = Type::Amount.to_nia(params[:fee])
|
@@ -1032,14 +1119,17 @@ module Steem
|
|
1032
1119
|
end
|
1033
1120
|
end
|
1034
1121
|
private
|
1122
|
+
# @private
|
1035
1123
|
def self.database_api(options)
|
1036
1124
|
options[:database_api] ||= Steem::DatabaseApi.new(options)
|
1037
1125
|
end
|
1038
1126
|
|
1127
|
+
# @private
|
1039
1128
|
def self.network_broadcast_api(options)
|
1040
1129
|
options[:network_broadcast_api] ||= Steem::NetworkBroadcastApi.new(options)
|
1041
1130
|
end
|
1042
1131
|
|
1132
|
+
# @private
|
1043
1133
|
def self.check_required_fields(hash, *fields)
|
1044
1134
|
fields.each do |field|
|
1045
1135
|
value = hash[field]
|
data/lib/steem/jsonrpc.rb
CHANGED
@@ -19,7 +19,7 @@ module Steem
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def initialize(options = {})
|
22
|
-
self.class.api_name = :jsonrpc
|
22
|
+
@api_name = self.class.api_name = :jsonrpc
|
23
23
|
@methods = API_METHODS
|
24
24
|
super
|
25
25
|
end
|
@@ -29,6 +29,8 @@ module Steem
|
|
29
29
|
|
30
30
|
if api_methods.nil?
|
31
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
|
+
|
32
34
|
methods = result.map do |method|
|
33
35
|
method.split('.').map(&:to_sym)
|
34
36
|
end
|
@@ -54,14 +56,14 @@ module Steem
|
|
54
56
|
end
|
55
57
|
|
56
58
|
def get_all_signatures(&block)
|
57
|
-
|
59
|
+
request_object = []
|
58
60
|
method_names = []
|
59
61
|
method_map = {}
|
60
62
|
signatures = {}
|
61
63
|
offset = 0
|
62
64
|
|
63
65
|
get_api_methods do |api, methods|
|
64
|
-
|
66
|
+
request_object += methods.map do |method|
|
65
67
|
method_name = "#{api}.#{method}"
|
66
68
|
method_names << method_name
|
67
69
|
current_rpc_id = @rpc_client.rpc_id
|
@@ -77,22 +79,30 @@ module Steem
|
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
signatures[api] ||= {}
|
86
|
-
signatures[api][method] = result
|
82
|
+
chunks = if request_object.size > Steem::RPC::HttpClient::JSON_RPC_BATCH_SIZE_MAXIMUM
|
83
|
+
request_object.each_slice(Steem::RPC::HttpClient::JSON_RPC_BATCH_SIZE_MAXIMUM)
|
84
|
+
else
|
85
|
+
request_object
|
87
86
|
end
|
88
87
|
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
92
102
|
end
|
93
|
-
else
|
94
|
-
return signatures
|
95
103
|
end
|
104
|
+
|
105
|
+
return signatures unless !!block
|
96
106
|
end
|
97
107
|
end
|
98
108
|
end
|
@@ -10,8 +10,7 @@ module Steem
|
|
10
10
|
|
11
11
|
RETRYABLE_EXCEPTIONS = [
|
12
12
|
NonCanonicalSignatureError, IncorrectRequestIdError,
|
13
|
-
IncorrectResponseIdError,
|
14
|
-
JSON::ParserError, IOError, Net::OpenTimeout
|
13
|
+
IncorrectResponseIdError, RemoteDatabaseLockError
|
15
14
|
]
|
16
15
|
|
17
16
|
# Expontential backoff.
|
@@ -41,10 +40,7 @@ module Steem
|
|
41
40
|
@retry_count += 1
|
42
41
|
|
43
42
|
can_retry = case e
|
44
|
-
when *RETRYABLE_EXCEPTIONS
|
45
|
-
true
|
46
|
-
when RemoteNodeError
|
47
|
-
e.inspect.include?('Unable to acquire database lock')
|
43
|
+
when *RETRYABLE_EXCEPTIONS then true
|
48
44
|
else; false
|
49
45
|
end
|
50
46
|
|
@@ -6,10 +6,14 @@ module Steem
|
|
6
6
|
attr_accessor :chain, :error_pipe
|
7
7
|
|
8
8
|
# @private
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
MAX_TIMEOUT_RETRY_COUNT = 100
|
10
|
+
|
11
|
+
# @private
|
12
|
+
MAX_TIMEOUT_BACKOFF = 30
|
13
|
+
|
14
|
+
# @private
|
15
|
+
TIMEOUT_ERRORS = [Net::ReadTimeout, Errno::EBADF, Errno::ECONNREFUSED,
|
16
|
+
IOError]
|
13
17
|
|
14
18
|
def initialize(options = {})
|
15
19
|
@chain = options[:chain] || :steem
|
@@ -26,38 +30,67 @@ module Steem
|
|
26
30
|
@uri ||= URI.parse(@url)
|
27
31
|
end
|
28
32
|
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
33
|
+
# Adds a request object to the stack. Usually, this method is called
|
34
|
+
# internally by {BaseClient#rpc_execute}. If you want to create a batched
|
35
|
+
# request, use this method to add to the batch then execute {BaseClient#rpc_batch_execute}.
|
44
36
|
def put(api_name = @api_name, api_method = nil, options = {})
|
45
37
|
current_rpc_id = rpc_id
|
46
38
|
rpc_method_name = "#{api_name}.#{api_method}"
|
47
39
|
options ||= {}
|
48
|
-
|
49
|
-
|
40
|
+
request_object = defined?(options.delete) ? options.delete(:request_object) : []
|
41
|
+
request_object ||= []
|
50
42
|
|
51
|
-
|
43
|
+
request_object << {
|
52
44
|
jsonrpc: '2.0',
|
53
45
|
id: current_rpc_id,
|
54
46
|
method: rpc_method_name,
|
55
47
|
params: options
|
56
48
|
}
|
57
49
|
|
58
|
-
|
50
|
+
request_object
|
59
51
|
end
|
60
|
-
|
52
|
+
|
53
|
+
# @abstract Subclass is expected to implement #rpc_execute.
|
54
|
+
# @!method rpc_execute
|
55
|
+
|
56
|
+
# @abstract Subclass is expected to implement #rpc_batch_execute.
|
57
|
+
# @!method rpc_batch_execute
|
58
|
+
|
59
|
+
# To be called by {BaseClient#rpc_execute} and {BaseClient#rpc_batch_execute}
|
60
|
+
# when a response has been consructed.
|
61
|
+
def yield_response(response, &block)
|
62
|
+
if !!block
|
63
|
+
case response
|
64
|
+
when Hashie::Mash then yield response.result, response.error, response.id
|
65
|
+
when Hashie::Array
|
66
|
+
response.each do |r|
|
67
|
+
r = Hashie::Mash.new(r)
|
68
|
+
block.call r.result, r.error, r.id
|
69
|
+
end
|
70
|
+
else; block.call response
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
response
|
75
|
+
end
|
76
|
+
|
77
|
+
# Checks json-rpc request/response for corrilated id. If they do not
|
78
|
+
# match, {IncorrectResponseIdError} is thrown. This is usually caused by
|
79
|
+
# the client, involving thread safety. It can also be caused by the node
|
80
|
+
# responding without an id.
|
81
|
+
#
|
82
|
+
# To avoid {IncorrectResponseIdError}, make sure you implement your client
|
83
|
+
# correctly.
|
84
|
+
#
|
85
|
+
# Setting DEBUG=true in the envrionment will cause this method to output
|
86
|
+
# both the request and response json.
|
87
|
+
#
|
88
|
+
# @param options [Hash] options
|
89
|
+
# @option options [Boolean] :debug Enable or disable debug output.
|
90
|
+
# @option options [Hash] :request to compare id
|
91
|
+
# @option options [Hash] :response to compare id
|
92
|
+
# @option options [String] :api_method
|
93
|
+
# @see {ThreadSafeHttpClient}
|
61
94
|
def evaluate_id(options = {})
|
62
95
|
debug = options[:debug] || ENV['DEBUG'] == 'true'
|
63
96
|
request = options[:request]
|
@@ -89,65 +122,45 @@ module Steem
|
|
89
122
|
end
|
90
123
|
end
|
91
124
|
|
92
|
-
|
93
|
-
|
125
|
+
# Current json-rpc id used for a request. This version auto-increments
|
126
|
+
# for each call. Subclasses can use their own strategy.
|
127
|
+
def rpc_id
|
128
|
+
@rpc_id ||= 0
|
129
|
+
@rpc_id += 1
|
130
|
+
end
|
131
|
+
private
|
132
|
+
# @private
|
133
|
+
def reset_timeout
|
134
|
+
@timeout_retry_count = 0
|
135
|
+
@back_off = 0.1
|
94
136
|
end
|
95
137
|
|
96
|
-
|
97
|
-
|
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
|
138
|
+
# @private
|
139
|
+
def retry_timeout(context, cause = nil)
|
140
|
+
@timeout_retry_count += 1
|
104
141
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
142
|
+
if @timeout_retry_count > MAX_TIMEOUT_RETRY_COUNT
|
143
|
+
raise TooManyTimeoutsError.new("Too many timeouts for: #{context}", cause)
|
144
|
+
elsif @timeout_retry_count % 10 == 0
|
145
|
+
msg = "#{@timeout_retry_count} retry attempts for: #{context}"
|
146
|
+
msg += "; cause: #{e}" if !!cause
|
147
|
+
error_pipe.puts msg
|
109
148
|
end
|
110
149
|
|
111
|
-
|
150
|
+
backoff_timeout
|
112
151
|
|
113
|
-
|
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
|
152
|
+
context
|
146
153
|
end
|
147
154
|
|
148
|
-
|
149
|
-
|
150
|
-
|
155
|
+
# Expontential backoff.
|
156
|
+
#
|
157
|
+
# @private
|
158
|
+
def backoff_timeout
|
159
|
+
@backoff ||= 0.1
|
160
|
+
@backoff *= 2
|
161
|
+
@backoff = 0.1 if @backoff > MAX_TIMEOUT_BACKOFF
|
162
|
+
|
163
|
+
sleep @backoff
|
151
164
|
end
|
152
165
|
end
|
153
166
|
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Steem
|
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, Net::ReadTimeout, JSON::ParserError]
|
20
|
+
|
21
|
+
# @private
|
22
|
+
POST_HEADERS = {
|
23
|
+
'Content-Type' => 'application/json; charset=utf-8',
|
24
|
+
'User-Agent' => Steem::AGENT_ID
|
25
|
+
}
|
26
|
+
|
27
|
+
JSON_RPC_BATCH_SIZE_MAXIMUM = 50
|
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 http_request(request)
|
45
|
+
http.request(request)
|
46
|
+
end
|
47
|
+
|
48
|
+
# This is the main method used by API instances to actually fetch data
|
49
|
+
# from the remote node. It abstracts the api namespace, method name, and
|
50
|
+
# parameters so that the API instance can be decoupled from the protocol.
|
51
|
+
#
|
52
|
+
# @param api_name [String] API namespace of the method being called.
|
53
|
+
# @param api_method [String] API method name being called.
|
54
|
+
# @param options [Hash] options
|
55
|
+
# @option options [Object] :request_object Hash or Array to become json in request body.
|
56
|
+
def rpc_execute(api_name = @api_name, api_method = nil, options = {}, &block)
|
57
|
+
reset_timeout
|
58
|
+
|
59
|
+
catch :tota_cera_pila do; begin
|
60
|
+
request = http_post
|
61
|
+
|
62
|
+
request_object = if !!api_name && !!api_method
|
63
|
+
put(api_name, api_method, options)
|
64
|
+
elsif !!options && defined?(options.delete)
|
65
|
+
options.delete(:request_object)
|
66
|
+
end
|
67
|
+
|
68
|
+
if request_object.size > JSON_RPC_BATCH_SIZE_MAXIMUM
|
69
|
+
raise JsonRpcBatchMaximumSizeExceededError, 'Maximum json-rpc-batch is 50 elements.'
|
70
|
+
end
|
71
|
+
|
72
|
+
request.body = if request_object.class == Hash
|
73
|
+
request_object
|
74
|
+
elsif request_object.size == 1
|
75
|
+
request_object.first
|
76
|
+
else
|
77
|
+
request_object
|
78
|
+
end.to_json
|
79
|
+
|
80
|
+
response = catch :http_request do; begin; http_request(request)
|
81
|
+
rescue *TIMEOUT_ERRORS => e
|
82
|
+
throw retry_timeout(:http_request, e)
|
83
|
+
end; end
|
84
|
+
|
85
|
+
if response.nil?
|
86
|
+
throw retry_timeout(:tota_cera_pila, 'response was nil')
|
87
|
+
end
|
88
|
+
|
89
|
+
case response.code
|
90
|
+
when '200'
|
91
|
+
response = catch :parse_json do; begin; JSON[response.body]
|
92
|
+
rescue *TIMEOUT_ERRORS => e
|
93
|
+
throw retry_timeout(:parse_json, e)
|
94
|
+
end; end
|
95
|
+
|
96
|
+
response = case response
|
97
|
+
when Hash
|
98
|
+
Hashie::Mash.new(response).tap do |r|
|
99
|
+
evaluate_id(request: request_object.first, response: r, api_method: api_method)
|
100
|
+
end
|
101
|
+
when Array
|
102
|
+
Hashie::Array.new(response).tap do |r|
|
103
|
+
request_object.each_with_index do |req, index|
|
104
|
+
evaluate_id(request: req, response: r[index], api_method: api_method)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
else; response
|
108
|
+
end
|
109
|
+
|
110
|
+
yield_response response, &block
|
111
|
+
when '504' # Gateway Timeout
|
112
|
+
throw retry_timeout(:tota_cera_pila, response.body)
|
113
|
+
when '502' # Bad Gateway
|
114
|
+
throw retry_timeout(:tota_cera_pila, response.body)
|
115
|
+
else
|
116
|
+
raise UnknownError, "#{api_name}.#{api_method}: #{response.body}"
|
117
|
+
end
|
118
|
+
end; end
|
119
|
+
end
|
120
|
+
|
121
|
+
def rpc_batch_execute(options = {}, &block)
|
122
|
+
yield_response rpc_execute(nil, nil, options), &block
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Steem
|
2
|
+
module RPC
|
3
|
+
# {ThreadSafeHttpClient} is the default RPC Client used by `steem-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
|
@@ -138,6 +138,22 @@ module Steem
|
|
138
138
|
def put(type, op = nil)
|
139
139
|
@expiration = nil
|
140
140
|
|
141
|
+
## Saving this for later. This block, or something like it, might replace
|
142
|
+
## API broadcast operation structure.
|
143
|
+
# case type
|
144
|
+
# when Symbol, String
|
145
|
+
# type_value = "#{type}_operation"
|
146
|
+
# @operations << {type: type_value, value: op}
|
147
|
+
# when Hash
|
148
|
+
# type_value = "#{type.keys.first}_operation"
|
149
|
+
# @operations << {type: type_value, value: type.values.first}
|
150
|
+
# when Array
|
151
|
+
# type_value = "#{type[0]}_operation"
|
152
|
+
# @operations << {type: type_value, value: type[1]}
|
153
|
+
# else
|
154
|
+
# # don't know what to do with it, skipped
|
155
|
+
# end
|
156
|
+
|
141
157
|
case type
|
142
158
|
when Symbol then @operations << [type, op]
|
143
159
|
when String then @operations << [type.to_sym, op]
|
@@ -252,6 +268,7 @@ module Steem
|
|
252
268
|
end
|
253
269
|
private
|
254
270
|
# See: https://github.com/steemit/steem/issues/1944
|
271
|
+
# @private
|
255
272
|
def canonical?(sig)
|
256
273
|
sig = sig.unpack('C*')
|
257
274
|
|
data/lib/steem/version.rb
CHANGED
data/lib/steem.rb
CHANGED
@@ -12,7 +12,8 @@ require 'steem/type/base_type'
|
|
12
12
|
require 'steem/type/amount'
|
13
13
|
require 'steem/transaction_builder'
|
14
14
|
require 'steem/rpc/base_client'
|
15
|
-
require 'steem/rpc/
|
15
|
+
require 'steem/rpc/http_client'
|
16
|
+
require 'steem/rpc/thread_safe_http_client'
|
16
17
|
require 'steem/api'
|
17
18
|
require 'steem/jsonrpc'
|
18
19
|
require 'steem/block_api'
|
@@ -26,7 +27,7 @@ module Steem
|
|
26
27
|
|
27
28
|
def self.const_missing(api_name)
|
28
29
|
api = api_classes[api_name]
|
29
|
-
api ||= Api.clone(freeze: true)
|
30
|
+
api ||= Api.clone(freeze: true) rescue Api.clone
|
30
31
|
api.api_name = api_name
|
31
32
|
api_classes[api_name] = api
|
32
33
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: steem-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anthony Martin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-05-
|
11
|
+
date: 2018-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -356,7 +356,8 @@ files:
|
|
356
356
|
- lib/steem/jsonrpc.rb
|
357
357
|
- lib/steem/mixins/retriable.rb
|
358
358
|
- lib/steem/rpc/base_client.rb
|
359
|
-
- lib/steem/rpc/
|
359
|
+
- lib/steem/rpc/http_client.rb
|
360
|
+
- lib/steem/rpc/thread_safe_http_client.rb
|
360
361
|
- lib/steem/transaction_builder.rb
|
361
362
|
- lib/steem/type/amount.rb
|
362
363
|
- lib/steem/type/base_type.rb
|
@@ -1,23 +0,0 @@
|
|
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
|