steem-ruby 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](https://badge.fury.io/rb/steem-ruby)
|
2
|
+
[](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
|