steem-ruby 0.1.0 → 0.1.1

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