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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: af48ecaba4ce86c2a21a91bd5299939408505ab7
4
- data.tar.gz: e8dfa541ebb4000a6dbb6f05e318fdecda6a9f98
3
+ metadata.gz: 24650742975337b2832ba5ab426f609a5ff8c9ef
4
+ data.tar.gz: 827c4848766e9b54dc8f04f5e20c920d3964c4eb
5
5
  SHA512:
6
- metadata.gz: 5d57efdf7b7e1d64b7b513b6e44c68304603525e6bb09001df3494c7616f823d3adf90572e6b73ee474f79c78aa55530a1a73338f8b2af519e38f070d41835b8
7
- data.tar.gz: 1056b940f606dd018b5cfcd1f134273b7e59e0587776d7841f2a6ef80aa3b1cbe3e562ddcfb31581c885205e608e816c78a510831a29cdc46c4308d59c6659c9
6
+ metadata.gz: 40b66f82639c1db97bf844bc07414f4ef512decec1fa4bf99bbc789ac1916379d96d03eebc417a89a7fcc7dcbbcbd9c29d82c3dd08958e3331712b2af4450f04
7
+ data.tar.gz: 37183706e9bdb150df65afb03d697c6a8cca050d3e3a3ac605054e46ba10d6a0897dc28e55a8035cc10f0a8df7d144fe548bd887344b132fa47f29518ae945be
data/.gitignore CHANGED
@@ -7,7 +7,7 @@
7
7
  /spec/reports/
8
8
  /spec/examples.txt
9
9
  /test/tmp/
10
- /test/fixtures/vcr_cassettes/
10
+ /test/fixtures/vcr_cassettes/*.yml
11
11
  /test/version_tmp/
12
12
  /tmp/
13
13
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- steem-ruby (0.1.0)
4
+ steem-ruby (0.1.1)
5
5
  bitcoin-ruby (~> 0.0, >= 0.0.18)
6
6
  ffi (~> 1.9, >= 1.9.23)
7
7
  hashie (~> 3.5, >= 3.5.7)
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 Appbase
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.html Complete API Definitions}
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::ThreadSafeClient
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 Steem::ArgumentError, "#{rpc_method_name} expects arguments: #{expected_args_size}"
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.rpc_post(@api_name, m, rpc_args)
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
@@ -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::RemoteNodeError, error.message, JSON.pretty_generate(error)
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::RemoteNodeError, error.message, JSON.pretty_generate(error)
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 < BaseError; end
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 < BaseError; end
133
- class NonCanonicalSignatureError < BaseError; end
161
+ class DuplicateTransactionError < TransactionExpiredError; end
162
+ class NonCanonicalSignatureError < TransactionExpiredError; end
134
163
  class BlockTooOldError < BaseError; end
135
164
  class IrrelevantSignatureError < BaseError; end
136
- class MissingPostingAuthorityError < BaseError; end
137
- class MissingActiveAuthorityError < BaseError; end
138
- class MissingOwnerAuthorityError < BaseError; end
139
- class MissingOtherAuthorityError < BaseError; end
140
- class InvalidAccountError < BaseError; end
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
@@ -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 = 3000
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: [0..0]}, &block)
20
- block_range = options[:block_range] || [0..0]
19
+ def get_blocks(options = {block_range: (0..0)}, &block)
20
+ block_range = options[:block_range] || (0..0)
21
21
 
22
- if block_range.size > MAX_RANGE_SIZE
23
- raise Steem::ArgumentError, "Too many blocks requested: #{block_range.size}; maximum request size: #{MAX_RANGE_SIZE}."
22
+ if (start = block_range.first) < 1
23
+ raise Steem::ArgumentError, "Invalid starting block: #{start}"
24
24
  end
25
25
 
26
- request_body = []
27
-
28
- for i in block_range do
29
- @rpc_client.put(self.class.api_name, :get_block, block_num: i, request_body: request_body)
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
- if !!block
33
- @rpc_client.rpc_post(nil, nil, request_body: request_body) do |result, error, id|
34
- yield result.nil? ? nil : result.block, error, id
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
- @rpc_client.rpc_post(nil, nil, request_body: request_body) do |result, error, id|
40
- blocks << result
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
@@ -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
- metadata = (params[:metadata] rescue nil) || {}
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
- # * :json_metadata (String)
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
- # @option options [String] :json_metadata (optional)
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
- # * :json (String)
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 json)
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
- # * :json_meta (String)
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 json_meta)
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
- # * :json_metadata (String)
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 json_metadata)
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
- request_body = []
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
- request_body += methods.map do |method|
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
- @rpc_client.rpc_post(nil, nil, {request_body: request_body}) do |result, error, id|
81
- api, method = method_map[id]
82
- api = api.to_sym
83
- method = method.to_sym
84
-
85
- signatures[api] ||= {}
86
- signatures[api][method] = result
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
- if !!block
90
- signatures.each do |api, methods|
91
- yield api, methods
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, Errno::EBADF, Errno::ECONNREFUSED,
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
- POST_HEADERS = {
10
- 'Content-Type' => 'application/json; charset=utf-8',
11
- 'User-Agent' => Steem::AGENT_ID
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
- 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
-
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
- request_body = defined?(options.delete) ? options.delete(:request_body) : []
49
- request_body ||= []
40
+ request_object = defined?(options.delete) ? options.delete(:request_object) : []
41
+ request_object ||= []
50
42
 
51
- request_body << {
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
- request_body
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
- def http_request(request)
93
- http.request(request)
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
- def rpc_post(api_name = @api_name, api_method = nil, options = {}, &block)
97
- request = http_post
98
-
99
- request_body = if !!api_name && !!api_method
100
- put(api_name, api_method, options)
101
- elsif !!options && defined?(options.delete)
102
- options.delete(:request_body)
103
- end
138
+ # @private
139
+ def retry_timeout(context, cause = nil)
140
+ @timeout_retry_count += 1
104
141
 
105
- request.body = if request_body.size == 1
106
- request_body.first.to_json
107
- else
108
- request_body.to_json
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
- response = http_request(request)
150
+ backoff_timeout
112
151
 
113
- case response.code
114
- when '200'
115
- response = JSON[response.body]
116
- response = case response
117
- when Hash
118
- Hashie::Mash.new(response).tap do |r|
119
- evaluate_id(request: request_body.first, response: r, api_method: api_method)
120
- end
121
- when Array
122
- Hashie::Array.new(response).tap do |r|
123
- request_body.each_with_index do |req, index|
124
- evaluate_id(request: req, response: r[index], api_method: api_method)
125
- end
126
- end
127
- else; response
128
- end
129
-
130
- if !!block
131
- case response
132
- when Hashie::Mash then yield response.result, response.error, response.id
133
- when Hashie::Array
134
- response.each do |r|
135
- r = Hashie::Mash.new(r)
136
- yield r.result, r.error, r.id
137
- end
138
- else; yield response
139
- end
140
- else
141
- return response
142
- end
143
- else
144
- raise UnknownError, "#{api_name}.#{api_method}: #{response.body}"
145
- end
152
+ context
146
153
  end
147
154
 
148
- def rpc_id
149
- @rpc_id ||= 0
150
- @rpc_id += 1
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
@@ -1,4 +1,4 @@
1
1
  module Steem
2
- VERSION = '0.1.0'
2
+ VERSION = '0.1.1'
3
3
  AGENT_ID = "steem-ruby/#{VERSION}"
4
4
  end
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/thread_safe_client'
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.0
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-10 00:00:00.000000000 Z
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/thread_safe_client.rb
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