class Radiator::Api
Radiator::Api
allows you to call remote methods to interact with the STEEM blockchain. The `Api` class is a shortened name for `Radiator::CondenserApi`.
Examples:
api = Radiator::Api.new response = api.get_dynamic_global_properties virtual_supply = response.result.virtual_supply
… or …
api = Radiator::Api.new virtual_supply = api.get_dynamic_global_properties do |prop| prop.virtual_supply end
If you need access to the `error` property, they can be accessed as follows:
api = Radiator::Api.new response = api.get_dynamic_global_properties if response.result.nil? puts response.error exit end virtual_supply = response.result.virtual_supply
… or …
api = Radiator::Api.new virtual_supply = api.get_dynamic_global_properties do |prop, error| if prop.nil? puts error exis end prop.virtual_supply end
List of remote methods:
set_subscribe_callback set_pending_transaction_callback set_block_applied_callback cancel_all_subscriptions get_trending_tags get_tags_used_by_author get_post_discussions_by_payout get_comment_discussions_by_payout get_discussions_by_trending get_discussions_by_trending30 get_discussions_by_created get_discussions_by_active get_discussions_by_cashout get_discussions_by_payout get_discussions_by_votes get_discussions_by_children get_discussions_by_hot get_discussions_by_feed get_discussions_by_blog get_discussions_by_comments get_discussions_by_promoted get_block_header get_block get_ops_in_block get_state get_trending_categories get_best_categories get_active_categories get_recent_categories get_config get_dynamic_global_properties get_chain_properties get_feed_history get_current_median_history_price get_witness_schedule get_hardfork_version get_next_scheduled_hardfork get_accounts get_account_references lookup_account_names lookup_accounts get_account_count get_conversion_requests get_account_history get_owner_history get_recovery_request get_escrow get_withdraw_routes get_account_bandwidth get_savings_withdraw_from get_savings_withdraw_to get_order_book get_open_orders get_liquidity_queue get_transaction_hex get_transaction get_required_signatures get_potential_signatures verify_authority verify_account_authority get_active_votes get_account_votes get_content get_content_replies get_discussions_by_author_before_date get_replies_by_last_update get_witnesses get_witness_by_account get_witnesses_by_vote lookup_witness_accounts get_witness_count get_active_witnesses get_miner_queue get_reward_fund
These methods and their characteristics are copied directly from methods marked as `database_api` in `steem-js`:
raw.githubusercontent.com/steemit/steem-js/master/src/api/methods.js
Constants
- DEFAULT_HIVE_FAILOVER_URLS
- DEFAULT_HIVE_RESTFUL_URL
- DEFAULT_HIVE_URL
- DEFAULT_STEEM_FAILOVER_URLS
- DEFAULT_STEEM_RESTFUL_URL
- DEFAULT_STEEM_URL
- HEALTH_URI
@private
- POST_HEADERS
@private
Public Class Methods
# File lib/radiator/api.rb, line 192 def self.default_failover_urls(chain) case chain.to_sym when :steem, :hive begin _api = Radiator::Api.new(url: DEFAULT_HIVE_FAILOVER_URLS.sample, failover_urls: DEFAULT_HIVE_FAILOVER_URLS) default_failover_urls = _api.get_accounts(['fullnodeupdate']) do |accounts| fullnodeupdate = accounts.first metadata = (JSON[fullnodeupdate.json_metadata] rescue nil) || {} report = metadata.fetch('report', []) if report.any? report.map do |r| if chain.to_sym == :steem && !r.fetch('hive', false) r.fetch('node') elsif chain.to_sym == :hive && r.fetch('hive', false) r.fetch('node') end end.compact end end rescue => e puts e end else; raise ApiError, "Unsupported chain: #{chain}" end if !!default_failover_urls default_failover_urls else case chain.to_sym when :steem then DEFAULT_STEEM_FAILOVER_URLS when :hive then DEFAULT_HIVE_FAILOVER_URLS else; [] end end end
# File lib/radiator/api.rb, line 185 def self.default_restful_url(chain) case chain.to_sym when :steem then DEFAULT_STEEM_RESTFUL_URL when :hive then DEFAULT_HIVE_RESTFUL_URL end end
# File lib/radiator/api.rb, line 177 def self.default_url(chain) case chain.to_sym when :steem then DEFAULT_STEEM_URL when :hive then DEFAULT_HIVE_URL else; raise ApiError, "Unsupported chain: #{chain}" end end
# File lib/radiator/api.rb, line 230 def self.network_api(chain, api_name, options = {}) api = case chain.to_sym when :steem then Steem::Api.clone(freeze: true) rescue Api.clone when :hive then Hive::Api.clone(freeze: true) rescue Api.clone else; raise ApiError, "Unsupported chain: #{chain}" end api.api_name = api_name api.new(options) rescue nil end
Cretes a new instance of Radiator::Api
.
Examples:
api = Radiator::Api.new(url: 'https://api.example.com')
@param options [::Hash] The attributes to initialize the Radiator::Api
with. @option options [String] :url URL that points at a full node, like `api.steemit.com`. Default from DEFAULT_URL. @option options [::Array<String>] :failover_urls An array that contains one or more full nodes to fall back on. Default from DEFAULT_FAILOVER_URLS. @option options [Logger] :logger An instance of `Logger` to send debug messages to. @option options [Boolean] :recover_transactions_on_error Have Radiator
try to recover transactions that are accepted but could not be confirmed due to an error like network timeout. Default: `true` @option options [Integer] :max_requests Maximum number of requests on a connection before it is considered expired and automatically closed. @option options [Integer] :pool_size Maximum number of connections allowed. @option options [Boolean] :reuse_ssl_sessions Reuse a previously opened SSL session for a new connection. There's a slight performance improvement by enabling this, but at the expense of reliability during long execution. Default false. @option options [Boolean] :persist Enable or disable Persistent HTTP. Using Persistent HTTP keeps the connection alive between API calls. Default: `true`
# File lib/radiator/api.rb, line 256 def initialize(options = {}) @user = options[:user] @password = options[:password] @chain = (options[:chain] || 'hive').to_sym @url = options[:url] || Api::default_url(@chain) @restful_url = options[:restful_url] || Api::default_restful_url(@chain) @preferred_url = @url.dup @failover_urls = options[:failover_urls] @debug = !!options[:debug] @max_requests = options[:max_requests] || 30 @ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER @ssl_version = options[:ssl_version] @self_logger = false @logger = if options[:logger].nil? @self_logger = true Radiator.logger else options[:logger] end @self_hashie_logger = false @hashie_logger = if options[:hashie_logger].nil? @self_hashie_logger = true Logger.new(nil) else options[:hashie_logger] end if @failover_urls.nil? @failover_urls = Api::default_failover_urls(@chain) - [@url] end @failover_urls = [@failover_urls].flatten.compact @preferred_failover_urls = @failover_urls.dup unless @hashie_logger.respond_to? :warn @hashie_logger = Logger.new(@hashie_logger) end @recover_transactions_on_error = if options.keys.include? :recover_transactions_on_error options[:recover_transactions_on_error] else true end @persist_error_count = 0 @persist = if options.keys.include? :persist options[:persist] else true end @reuse_ssl_sessions = if options.keys.include? :reuse_ssl_sessions options[:reuse_ssl_sessions] else true end @use_condenser_namespace = if options.keys.include? :use_condenser_namespace options[:use_condenser_namespace] else true end if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE @pool_size = options[:pool_size] || Net::HTTP::Persistent::DEFAULT_POOL_SIZE end Hashie.logger = @hashie_logger @method_names = nil @uri = nil @http_id = nil @http_memo = {} @api_options = options.dup.merge(chain: @chain) @api = nil @block_api = nil @backoff_at = nil @jussi_supported = [] @network_api = Api::network_api(@chain, api_name, url: @url) end
Private Class Methods
# File lib/radiator/api.rb, line 659 def self.apply_http_defaults(http, ssl_verify_mode) http.read_timeout = 10 http.open_timeout = 10 http.verify_mode = ssl_verify_mode http.ssl_timeout = 30 if defined? http.ssl_timeout http end
# File lib/radiator/api.rb, line 985 def self.finalize(logger, hashie_logger) proc { if !!logger && defined?(logger.close) && !logger.closed? logger.close end if !!hashie_logger && defined?(hashie_logger.close) && !hashie_logger.closed? hashie_logger.close end } end
# File lib/radiator/api.rb, line 652 def self.methods(api_name) @methods ||= {} @methods[api_name] ||= JSON[File.read methods_json_path].map do |e| e if e['api'].to_sym == api_name end.compact.freeze end
# File lib/radiator/api.rb, line 648 def self.methods_json_path @methods_json_path ||= "#{File.dirname(__FILE__)}/methods.json" end
Public Instance Methods
@private
# File lib/radiator/api.rb, line 424 def api_name :condenser_api end
Get a specific block or range of blocks.
Example:
api = Radiator::Api.new blocks = api.get_blocks(10..20) transactions = blocks.flat_map(&:transactions)
… or …
api = Radiator::Api.new transactions = [] api.get_blocks(10..20) do |block| transactions += block.transactions end
@param block_number [Fixnum || ::Array<Fixnum>] @param block the block to execute for each result, optional. @return [::Array]
# File lib/radiator/api.rb, line 357 def get_blocks(block_number, &block) block_number = [*(block_number)].flatten if !!block block_number.each do |i| if use_condenser_namespace? yield api.get_block(i) else yield block_api.get_block(block_num: i).result, i end end else block_number.map do |i| if use_condenser_namespace? api.get_block(i) else block_api.get_block(block_num: i).result end end end end
# File lib/radiator/api.rb, line 614 def inspect properties = %w( chain url backoff_at max_requests ssl_verify_mode ssl_version persist recover_transactions_on_error reuse_ssl_sessions pool_size use_condenser_namespace ).map do |prop| if !!(v = instance_variable_get("@#{prop}")) "@#{prop}=#{v}" end end.compact.join(', ') "#<#{self.class.name} [#{properties}]>" end
@private
# File lib/radiator/api.rb, line 436 def method_missing(m, *args, &block) super unless respond_to_missing?(m) current_rpc_id = rpc_id method_name = [api_name, m].join('.') response = nil options = if api_name == :condenser_api { jsonrpc: "2.0", method: method_name, params: args, id: current_rpc_id, } else rpc_args = if args.empty? {} else args.first end { jsonrpc: "2.0", method: method_name, params: rpc_args, id: current_rpc_id, } end tries = 0 timestamp = Time.now.utc loop do tries += 1 if tries > 5 && flappy? && !check_file_open? raise ApiError, 'PANIC: Out of file resources' end begin if tries > 1 && @recover_transactions_on_error && api_name == :network_broadcast_api signatures, exp = extract_signatures(options) if !!signatures && signatures.any? offset = [(exp - timestamp).abs, 30].min if !!(response = recover_transaction(signatures, current_rpc_id, timestamp - offset)) response = Hashie::Mash.new(response) end end end @network_api ||= Api::network_api(@chain, api_name, url: @uri) if !!@network_api && @network_api.respond_to?(m) if !!block @network_api.send(m, *args) do |*r| return yield(*r) end else return @network_api.send(m, *args) end end if response.nil? response = request(options) response = if response.nil? error "No response, retrying ...", method_name elsif !response.kind_of? Net::HTTPSuccess warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name, true else detect_jussi(response) case response.code when '200' body = response.body response = JSON[body] if response['id'] != options[:id] debug_payload(options, body) if ENV['DEBUG'] == 'true' if !!response['id'] warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name, true else # The node has broken the jsonrpc spec. warning "Node did not provide jsonrpc id (expected: #{options[:id]}, got: nothing), retrying ...", method_name, true end if response.keys.include?('error') handle_error(response, options, method_name, tries) end elsif response.keys.include?('error') handle_error(response, options, method_name, tries) else Hashie::Mash.new(response) end when '400' then warning 'Code 400: Bad Request, retrying ...', method_name, true when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name, true when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name, true when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name, true when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name, true else warning "Unknown code #{response.code}, retrying ...", method_name, true warning response end end end rescue Net::HTTP::Persistent::Error => e warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name, true if e.cause.class == Net::HTTPMethodNotAllowed warning 'Node upstream is misconfigured.' drop_current_failover_url method_name end @persist_error_count += 1 rescue ConnectionPool::Error => e warning "Connection Pool Error (#{e.message}), retrying ...", method_name, true rescue Errno::ECONNREFUSED => e warning 'Connection refused, retrying ...', method_name, true rescue Errno::EADDRNOTAVAIL => e warning 'Node not available, retrying ...', method_name, true rescue Errno::ECONNRESET => e warning "Connection Reset (#{e.message}), retrying ...", method_name, true rescue Errno::EBUSY => e warning "Resource busy (#{e.message}), retrying ...", method_name, true rescue Errno::ENETDOWN => e warning "Network down (#{e.message}), retrying ...", method_name, true rescue Net::ReadTimeout => e warning 'Node read timeout, retrying ...', method_name, true rescue Net::OpenTimeout => e warning 'Node timeout, retrying ...', method_name, true rescue RangeError => e warning 'Range Error, retrying ...', method_name, true rescue OpenSSL::SSL::SSLError => e warning "SSL Error (#{e.message}), retrying ...", method_name, true rescue SocketError => e warning "Socket Error (#{e.message}), retrying ...", method_name, true rescue JSON::ParserError => e warning "JSON Parse Error (#{e.message}), retrying ...", method_name, true drop_current_failover_url method_name if tries > 5 response = nil rescue ApiError => e warning "ApiError (#{e.message}), retrying ...", method_name, true # rescue => e # warning "Unknown exception from request, retrying ...", method_name, true # warning e end # failover latch @network_api = nil if !!@network_api if !!response @persist_error_count = 0 if !!block if api_name == :condenser_api return yield(response.result, response.error, response.id) else if defined?(response.result.size) && response.result.size == 0 return yield(nil, response.error, response.id) elsif ( defined?(response.result.size) && response.result.size == 1 && defined?(response.result.values) ) return yield(response.result.values.first, response.error, response.id) else return yield(response.result, response.error, response.id) end end else return response end end backoff end # loop end
@private
# File lib/radiator/api.rb, line 414 def method_names return @method_names if !!@method_names return CondenserApi::METHOD_NAMES if api_name == :condenser_api @method_names = Radiator::Api.methods(api_name).map do |e| e['method'].to_sym end end
@private
# File lib/radiator/api.rb, line 429 def respond_to_missing?(m, include_private = false) return true if @network_api.respond_to? m.to_sym method_names.nil? ? false : method_names.include?(m.to_sym) end
Stops the persistant http connections.
# File lib/radiator/api.rb, line 381 def shutdown @uri = nil @http_id = nil @http_memo.each do |k| v = @http_memo.delete(k) if defined?(v.shutdown) debug "Shutting down instance #{k} (#{v})" v.shutdown end end @api.shutdown if !!@api && @api != self @api = nil @block_api.shutdown if !!@block_api && @block_api != self @block_api = nil if @self_logger if !!@logger && defined?(@logger.close) if defined?(@logger.closed?) @logger.close unless @logger.closed? end end end if @self_hashie_logger if !!@hashie_logger && defined?(@hashie_logger.close) if defined?(@hashie_logger.closed?) @hashie_logger.close unless @hashie_logger.closed? end end end end
# File lib/radiator/api.rb, line 628 def stopped? http_active = if @http_memo.nil? false else @http_memo.values.map do |http| if defined?(http.active?) http.active? else false end end.include?(true) end @uri.nil? && @http_id.nil? && !http_active && @api.nil? && @block_api.nil? end
# File lib/radiator/api.rb, line 644 def use_condenser_namespace? @use_condenser_namespace end
Private Instance Methods
# File lib/radiator/api.rb, line 671 def api @api ||= self.class == Api ? self : Api.new(api_options) end
# File lib/radiator/api.rb, line 667 def api_options @api_options.merge(failover_urls: @failover_urls, logger: @logger, hashie_logger: @hashie_logger) end
# File lib/radiator/api.rb, line 969 def backoff shutdown bump_failover if flappy? || !healthy?(uri) @backoff_at ||= Time.now.utc @backoff_sleep ||= 0.01 @backoff_sleep *= 2 GC.start sleep @backoff_sleep ensure if !!@backoff_at && Time.now.utc - @backoff_at > 300 @backoff_at = nil @backoff_sleep = nil end end
# File lib/radiator/api.rb, line 675 def block_api @block_api ||= self.class == BlockApi ? self : BlockApi.new(api_options) end
# File lib/radiator/api.rb, line 828 def bump_failover @uri = nil @url = pop_failover_url warning "Failing over to #{@url} ..." end
# File lib/radiator/api.rb, line 949 def check_file_open? File.exists?('.') rescue false end
# File lib/radiator/api.rb, line 955 def debug_payload(request, response) request = JSON.pretty_generate(request) response = JSON.parse(response) rescue response response = JSON.pretty_generate(response) rescue response puts '=' * 80 puts "Request:" puts request puts '=' * 80 puts "Response:" puts response puts '=' * 80 end
# File lib/radiator/api.rb, line 740 def detect_jussi(response) return if jussi_supported?(@url) jussi_response_id = response['x-jussi-response-id'] if !!jussi_response_id debug "Found a node that supports jussi: #{@url}" @jussi_supported << @url end end
Note, this methods only removes the uri.to_s if present but it does not call bump_failover
, in order to avoid a race condition.
# File lib/radiator/api.rb, line 840 def drop_current_failover_url(prefix) if @preferred_failover_urls.size == 1 warning "Node #{uri} appears to be misconfigured but no other node is available, retrying ...", prefix else warning "Removing misconfigured node from failover urls: #{uri}, retrying ...", prefix @preferred_failover_urls.delete(uri.to_s) @failover_urls.delete(uri.to_s) end end
# File lib/radiator/api.rb, line 834 def flappy? !!@backoff_at && Time.now.utc - @backoff_at < 300 end
# File lib/radiator/api.rb, line 850 def handle_error(response, request_options, method_name, tries) parser = ErrorParser.new(response) _signatures, exp = extract_signatures(request_options) if (!!exp && exp < Time.now.utc) || (tries > 2 && !parser.node_degraded?) # Whatever the error was, it is already expired or tried too much. No # need to try to recover. debug "Error code #{parser} but transaction already expired or too many tries, giving up (attempt: #{tries})." elsif parser.can_retry? drop_current_failover_url method_name if !!exp && parser.expiry? drop_current_failover_url method_name if parser.node_degraded? debug "Error code #{parser} (attempt: #{tries}), retrying ..." return nil end if !!parser.trx_id # Turns out, the ErrorParser found a transaction id. It might come in # handy, so let's append this to the result along with the error. response[:result] = { id: parser.trx_id, block_num: -1, trx_num: -1, expired: false } if @recover_transactions_on_error begin if !!@restful_url JSON[open("#{@restful_url}/account_history_api/get_transaction?id=#{parser.trx_id}").read].tap do |tx| response[:result][:block_num] = tx['block_num'] response[:result][:trx_num] = tx['transaction_num'] end else # Node operators often disable this operation. api.get_transaction(parser.trx_id) do |tx| if !!tx response[:result][:block_num] = tx.block_num response[:result][:trx_num] = tx.transaction_num end end end response[:recovered_by] = http_id response.delete('error') # no need for this, now rescue debug "Couldn't find block for trx_id: #{parser.trx_id}, giving up." end end end Hashie::Mash.new(response) end
# File lib/radiator/api.rb, line 905 def healthy?(url) begin # Note, not all nodes support the /health uri. But even if they don't, # they'll respond status code 200 OK, even if the body shows an error. # But if the node supports the /health uri, it will do additional # verifications on the block height. # See: https://github.com/steemit/steem/blob/master/contrib/healthcheck.sh # Also note, this check is done **without** net-http-persistent. response = open(url + HEALTH_URI) response = JSON[response.read] if !!response['error'] if !!response['error']['data'] if !!response['error']['data']['message'] error "#{url} error: #{response['error']['data']['message']}" end elsif !!response['error']['message'] error "#{url} error: #{response['error']['message']}" else error "#{url} error: #{response['error']}" end false elsif response['status'] == 'OK' true else error "#{url} status: #{response['status']}" false end rescue JSON::ParserError # No JSON, but also no HTTP error code, so we're OK. true rescue => e error "Health check failure for #{url}: #{e.inspect}" sleep 0.2 false end end
# File lib/radiator/api.rb, line 692 def http return @http_memo[http_id] if @http_memo.keys.include? http_id @http_memo[http_id] = if @persist && @persist_error_count < 10 idempotent = api_name != :network_broadcast_api http = if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size) else # net-http-persistent < 3.0 Net::HTTP::Persistent.new(http_id) end http.keep_alive = 30 http.idle_timeout = idempotent ? 10 : nil http.max_requests = @max_requests http.retry_change_requests = idempotent if defined? http.retry_change_requests http.reuse_ssl_sessions = @reuse_ssl_sessions http else http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' http end Api::apply_http_defaults(@http_memo[http_id], @ssl_verify_mode) end
# File lib/radiator/api.rb, line 688 def http_id @http_id ||= "radiator-#{Radiator::VERSION}-#{api_name}-#{SecureRandom.uuid}" end
# File lib/radiator/api.rb, line 736 def jussi_supported?(url = @url) @jussi_supported.include? url end
# File lib/radiator/api.rb, line 818 def pop_failover_url reset_failover if @failover_urls.none? until @failover_urls.none? || healthy?(url = @failover_urls.sample) @failover_urls.delete(url) end url || (uri || @url).to_s end
# File lib/radiator/api.rb, line 721 def post_request Net::HTTP::Post.new uri.request_uri, POST_HEADERS end
# File lib/radiator/api.rb, line 751 def recover_transaction(signatures, expected_rpc_id, after) debug "Looking for signatures: #{signatures.map{|s| s[0..5]}} since: #{after}" count = 0 start = Time.now.utc block_range = api.get_dynamic_global_properties do |properties| high = properties.head_block_number low = high - 100 [*(low..(high))].reverse end # It would be nice if Steemit, Inc. would add an API method like # `get_transaction`, call it `get_transaction_by_signature`, so we didn't # have to scan the latest blocks like this. At most, we read 100 blocks # but we also give up once the block time is before the `after` argument. api.get_blocks(block_range) do |block, block_num| unless defined? block.transaction_ids error "Blockchain does not provide transaction ids in blocks, giving up." return nil end count += 1 raise ApiError, "Race condition detected on remote node at: #{block_num}" if block.nil? # TODO Some blockchains (like Golos) do not have transaction_ids. In # the future, it would be better to decode the operation and signature # into the transaction id. # See: https://github.com/steemit/steem/issues/187 # See: https://github.com/GolosChain/golos/issues/281 unless defined? block.transaction_ids @recover_transactions_on_error = false return end timestamp = Time.parse(block.timestamp + 'Z') break if timestamp < after block.transactions.each_with_index do |tx, index| next unless ((tx['signatures'] || []) & signatures).any? debug "Found transaction #{count} block(s) ago; took #{(Time.now.utc - start)} seconds to scan." return { id: expected_rpc_id, recovered_by: http_id, result: { id: block.transaction_ids[index], block_num: block_num, trx_num: index, expired: false } } end end debug "Could not find transaction in #{count} block(s); took #{(Time.now.utc - start)} seconds to scan." return nil end
# File lib/radiator/api.rb, line 725 def request(options) request = post_request request.body = JSON[options] case http when Net::HTTP::Persistent then http.request(uri, request) when Net::HTTP then http.request(request) else; raise ApiError, "Unsuppored scheme: #{http.inspect}" end end
# File lib/radiator/api.rb, line 812 def reset_failover @url = @preferred_url.dup @failover_urls = @preferred_failover_urls.dup warning "Failover reset, going back to #{@url} ..." end
# File lib/radiator/api.rb, line 679 def rpc_id @rpc_id ||= 0 @rpc_id = @rpc_id + 1 end
# File lib/radiator/api.rb, line 684 def uri @uri ||= URI.parse(@url) end