beowulf-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3ef4850cb0d869fc8cdd9650b2eaa97e4c632685170515e98ab272bf1e427b8d
4
+ data.tar.gz: db9a7bc76541043e0aacb559088e1a4cbc30673ce1ef6ad8d44c4458f8b82d0c
5
+ SHA512:
6
+ metadata.gz: f9dfd423d37d99eeb4af29e647a68ba949f7ba75122a652089ed31f05c33d87472226ad71c7535adb253a398e8a04b2235b4e52eaec2831547f2e7f3f1b14525
7
+ data.tar.gz: d5d2d7ea03c7d65b79079901fde8a86956db48fda59319bea17b070a5f58dde17d984b46b62024b3b224b2ffa1c3d8008e4d721cc2b7fd3c90445f961624a1c7
@@ -0,0 +1,58 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+ hashie.log
55
+ output.mp4
56
+
57
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
58
+ # .rubocop-https?--*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 beowulf-foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ # beowulf-ruby
2
+
3
+ beowulf-ruby is the official Beowulf library for Ruby.
4
+
5
+ ### Quick Start
6
+
7
+ Add the gem to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'beowulf-ruby'
11
+ ```
12
+
13
+ Then:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ If you don't have `bundler`, see the next section.
20
+
21
+ ### Prerequisites
22
+
23
+ `minimum ruby version: 2.2`
24
+
25
+ #### Linux
26
+
27
+ ```bash
28
+ $ sudo apt-get install ruby-full git openssl libssl1.0.0 libssl-dev
29
+ $ gem install bundler
30
+ ```
31
+
32
+ #### macOS
33
+
34
+ ```
35
+ $ gem install bundler
36
+ ```
37
+
38
+ ### Usage
39
+
40
+ ```ruby
41
+ require 'beowulf'
42
+
43
+ api = Beowulf::Api.new
44
+ api.get_dynamic_global_properties do |properties|
45
+ properties.head_block_number
46
+ end
47
+ => 2307597
48
+ ```
49
+
50
+ ... or ...
51
+
52
+ ```ruby
53
+ require 'beowulf'
54
+
55
+ api = Beowulf::Api.new
56
+ response = api.get_dynamic_global_properties
57
+ response.result.head_block_number
58
+ => 2307597
59
+ ```
60
+
61
+ ### Example
62
+ ```ruby
63
+ require 'beowulf'
64
+
65
+ # 0. Init
66
+ ## MainNet: https://bw.beowulfchain.com/rpc
67
+ ## TestNet: https://testnet-bw.beowulfchain.com/rpc
68
+ api = Beowulf::Api.new(url: 'http://localhost:8376/rpc') # Replace this url with your node url
69
+
70
+ # 1. get_version
71
+ version = api.get_version
72
+ puts version.to_json
73
+
74
+ # 2. get_block
75
+ bk = api.get_blocks(1)
76
+ puts bk.to_json
77
+
78
+ # 3. get_config
79
+ cfg = api.get_config
80
+ puts cfg.to_json
81
+ puts cfg.result.BEOWULF_INIT_SUPPLY
82
+
83
+ # 4. get_accounts
84
+ acc = api.get_accounts(["beowulf2", "beowulf3"])
85
+ puts acc.to_json
86
+
87
+ # 5. get_transaction
88
+ gtx = api.get_transaction("e725f75544dbeea7a017250bd0186ca247b24724")
89
+ puts gtx.to_json
90
+
91
+ # 6. Transfer native coin
92
+ ## 6.1. Transfer BWF from alice to bob
93
+ tx = Beowulf::Transaction.new(url: 'https://bw.beowulfchain.com/rpc', wif: 'alice Private-Key Here')
94
+ transfer = {
95
+ type: :transfer,
96
+ from: 'alice',
97
+ to: 'bob',
98
+ amount: '100.00000 BWF',
99
+ fee: '0.01000 W',
100
+ memo: 'alice to bob'
101
+ }
102
+ tx.operations << transfer
103
+ tx_resp = tx.process(true)
104
+ puts tx_resp.to_json
105
+ puts "tx_resp.result.id:", tx_resp.result.id
106
+
107
+ ## 6.2. Transfer W from alice to bob
108
+ tx = Beowulf::Transaction.new(url: 'https://bw.beowulfchain.com/rpc', wif: 'alice Private-Key Here')
109
+ transfer = {
110
+ type: :transfer,
111
+ from: 'alice',
112
+ to: 'bob',
113
+ amount: '1.00000 W',
114
+ fee: '0.01000 W',
115
+ memo: 'alice to bob'
116
+ }
117
+ tx.operations << transfer
118
+ tx_resp = tx.process(true)
119
+ puts tx_resp.to_json
120
+ puts "tx_resp.result.id:", tx_resp.result.id
121
+
122
+ # 7. Transfer token
123
+ ## Transfer token KNOW from alice to bob
124
+ tx = Beowulf::Transaction.new(url: 'https://bw.beowulfchain.com/rpc', wif: 'alice Private-Key Here')
125
+ transfer = {
126
+ type: :transfer,
127
+ from: 'alice',
128
+ to: 'bob',
129
+ amount: '100.00000 KNOW',
130
+ fee: '0.01000 W',
131
+ memo: 'alice to bob'
132
+ }
133
+ tx.operations << transfer
134
+ tx_resp = tx.process(true)
135
+ puts tx_resp.to_json
136
+ puts "tx_resp.result.id:", tx_resp.result.id
137
+
138
+
139
+ # 8. Create account
140
+ ## 8.1. GenKeys
141
+ wl = Beowulf::Wallet.new(name: "new-account-name")
142
+ wl.gen_keys
143
+ puts "wl.private_key:", wl.private_key
144
+ puts "wl.public_key:", wl.public_key
145
+
146
+ ## 8.2. AccountCreate
147
+ tx = Beowulf::Transaction.new(url: 'https://bw.beowulfchain.com/rpc', wif: 'creator Private-Key Here')
148
+ owner = {
149
+ weight_threshold: 1,
150
+ account_auths: [],
151
+ key_auths: [[wl.public_key, 1]]
152
+ }
153
+ account_create = {
154
+ type: :account_create,
155
+ fee: '0.10000 W',
156
+ creator: 'creator',
157
+ new_account_name: wl.name,
158
+ owner: owner,
159
+ json_metadata: ''
160
+ }
161
+ tx.operations << account_create
162
+ tx_resp = tx.process(true)
163
+ puts "tx_resp:", tx_resp.to_json
164
+ puts "tx_resp.result.id:", tx_resp.result.id
165
+
166
+ ## 8.3. Write file wallet.
167
+ wallet_path = "/path/to/folder/save/wallet"
168
+ password = "your_password"
169
+ wl.save_wallet_file(wallet_path, "", password)
170
+
171
+ ## 8.4. Load file wallet.
172
+ sleep(2)
173
+ wallet_path_file = File.join(wallet_path, "new-account-name-wallet.json")
174
+ wl2 = Beowulf::Wallet.new
175
+ wl2.read_wallet_file(wallet_path_file, password)
176
+ puts "wl2.private_key:", wl2.private_key
177
+ puts "wl2.public_key:", wl2.public_key
178
+ ```
179
+
180
+
181
+ ## Failover
182
+
183
+ Beowulf supports failover for situations where a node has, for example, become unresponsive. When creating a new instance of `::Api` and `::Transaction`, you may provide a list of alternative nodes, or leave them out to use the default list. For example:
184
+
185
+ ```ruby
186
+ options = {
187
+ url: 'https://api.yournodedomain.com',
188
+ failover_urls: [
189
+ 'https://api.otherdomain1.com',
190
+ 'https://api.otherdomain2.com'
191
+ ]
192
+ }
193
+
194
+ api = Beowulf::Api.new(options)
195
+ ```
196
+
197
+
198
+ ## License
199
+
200
+ MIT, see the `LICENSE` file.
@@ -0,0 +1,52 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'yard'
4
+ require 'beowulf'
5
+ require 'awesome_print'
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.test_files = FileList['test/**/*_test.rb']
11
+ t.ruby_opts << if ENV['HELL_ENABLED']
12
+ '-W2'
13
+ else
14
+ '-W1'
15
+ end
16
+ end
17
+
18
+ YARD::Rake::YardocTask.new do |t|
19
+ t.files = ['lib/**/*.rb']
20
+ end
21
+
22
+ task default: :test
23
+
24
+ namespace :clean do
25
+ desc 'Deletes test/fixtures/vcr_cassettes/*.yml so they can be rebuilt fresh.'
26
+ task :vcr do |t|
27
+ exec 'rm -v test/fixtures/vcr_cassettes/*.yml'
28
+ end
29
+ end
30
+
31
+ desc 'Ruby console with beowulf already required.'
32
+ task :console do
33
+ exec "irb -r beowulf -I ./lib"
34
+ end
35
+
36
+ desc 'Build a new version of the beowulf gem.'
37
+ task :build do
38
+ exec 'gem build beowulf.gemspec'
39
+ end
40
+
41
+ desc 'Publish the current version of the beowulf gem.'
42
+ task :push do
43
+ exec "gem push beowulf-ruby-#{Beowulf::VERSION}.gem"
44
+ end
45
+
46
+ # We're not going to yank on a regular basis, but this is how it's done if you
47
+ # really want a task for that for some reason.
48
+
49
+ # desc 'Yank the current version of the beowulf gem.'
50
+ # task :yank do
51
+ # exec "gem yank beowulf -v #{Beowulf::VERSION}"
52
+ # end
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'beowulf/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'beowulf-ruby'
8
+ spec.version = Beowulf::VERSION
9
+ spec.authors = ['NghiaTC']
10
+ spec.email = ['contact@beowulfchain.com']
11
+
12
+ spec.summary = %q{Beowulf RPC Ruby Client}
13
+ spec.description = %q{Beowulf-ruby is the official Beowulf library for Ruby.}
14
+ spec.homepage = 'https://github.com/beowulf-foundation/beowulf-ruby'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test)/}) }
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 2.0', '>= 2.0.1'
21
+ spec.add_development_dependency 'rake', '~> 12.1', '>= 12.1.0'
22
+ spec.add_development_dependency 'minitest', '~> 5.10', '>= 5.10.3'
23
+ spec.add_development_dependency 'minitest-line', '~> 0.6.3'
24
+ spec.add_development_dependency 'minitest-proveit', '~> 1.0', '>= 1.0.0'
25
+ spec.add_development_dependency 'webmock', '~> 3.6', '>= 3.6.0'
26
+ spec.add_development_dependency 'simplecov', '~> 0.17.0'
27
+ spec.add_development_dependency 'vcr', '~> 5.0', '>= 5.0.0'
28
+ spec.add_development_dependency 'yard', '~> 0.9.20'
29
+ spec.add_development_dependency 'pry', '~> 0.11', '>= 0.11.3'
30
+ spec.add_development_dependency 'rb-readline', '~> 0.5', '>= 0.5.5'
31
+ spec.add_development_dependency 'irb', '~> 1.0', '>= 1.0.0'
32
+
33
+ # net-http-persistent has an open-ended dependency because beowulf directly
34
+ # supports net-http-persistent-3.0.0 as well as net-http-persistent-2.5.2.
35
+ spec.add_dependency('net-http-persistent', '~> 2.5', '>= 2.5.2')
36
+ spec.add_dependency('json', '~> 2.0', '>= 2.0.2')
37
+ spec.add_dependency('logging', '~> 2.2', '>= 2.2.0')
38
+ spec.add_dependency('hashie', '~> 3.5', '>= 3.5.5')
39
+ spec.add_dependency('bitcoin-ruby', '~> 0.0', '>= 0.0.11')
40
+ spec.add_dependency('ffi', '~> 1.9', '>= 1.9.18')
41
+ spec.add_dependency('awesome_print', '~> 1.7', '>= 1.7.0')
42
+ spec.add_dependency 'base58', '~> 0.2', '>= 0.2.3'
43
+ end
@@ -0,0 +1,35 @@
1
+ require 'beowulf/version'
2
+ require 'json'
3
+ require 'awesome_print' if ENV['USE_AWESOME_PRINT'] == 'true'
4
+
5
+ module Beowulf
6
+ require 'beowulf/utils'
7
+ require 'beowulf/type/serializer'
8
+ require 'beowulf/type/amount'
9
+ require 'beowulf/type/u_int16'
10
+ require 'beowulf/type/u_int32'
11
+ require 'beowulf/type/point_in_time'
12
+ require 'beowulf/type/permission'
13
+ require 'beowulf/type/public_key'
14
+ require 'beowulf/type/array'
15
+ require 'beowulf/type/hash'
16
+ require 'beowulf/type/future'
17
+ require 'beowulf/type/authority'
18
+ require 'beowulf/type/authority_update'
19
+ require 'beowulf/logger'
20
+ require 'beowulf/chain_config'
21
+ require 'beowulf/api'
22
+ require 'beowulf/database_api'
23
+ require 'beowulf/network_broadcast_api'
24
+ require 'beowulf/account_history_api'
25
+ require 'beowulf/condenser_api'
26
+ require 'beowulf/block_api'
27
+ require 'beowulf/operation_ids'
28
+ require 'beowulf/operation_types'
29
+ require 'beowulf/operation'
30
+ require 'beowulf/transaction'
31
+ require 'beowulf/base_error'
32
+ require 'beowulf/error_parser'
33
+ require 'beowulf/wallet'
34
+ extend self
35
+ end
@@ -0,0 +1,15 @@
1
+ module Beowulf
2
+ class AccountHistoryApi < Api
3
+ def method_names
4
+ @method_names ||= [
5
+ # :get_account_history,
6
+ # :get_ops_in_block,
7
+ :get_transaction
8
+ ].freeze
9
+ end
10
+
11
+ def api_name
12
+ :account_history_api
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,845 @@
1
+ require 'uri'
2
+ require 'base64'
3
+ require 'hashie'
4
+ require 'hashie/logger'
5
+ require 'openssl'
6
+ require 'open-uri'
7
+ require 'net/http/persistent'
8
+
9
+ module Beowulf
10
+ # Beowulf::Api allows you to call remote methods to interact with the Beowulf blockchain.
11
+ # The `Api` class is a shortened name for
12
+ # `Beowulf::CondenserApi`.
13
+ #
14
+ # Examples:
15
+ #
16
+ # api = Beowulf::Api.new
17
+ # response = api.get_dynamic_global_properties
18
+ # virtual_supply = response.result.virtual_supply
19
+ #
20
+ # ... or ...
21
+ #
22
+ # api = Beowulf::Api.new
23
+ # virtual_supply = api.get_dynamic_global_properties do |prop|
24
+ # prop.virtual_supply
25
+ # end
26
+ #
27
+ # If you need access to the `error` property, they can be accessed as follows:
28
+ #
29
+ # api = Beowulf::Api.new
30
+ # response = api.get_dynamic_global_properties
31
+ # if response.result.nil?
32
+ # puts response.error
33
+ # exit
34
+ # end
35
+ #
36
+ # virtual_supply = response.result.virtual_supply
37
+ #
38
+ # ... or ...
39
+ #
40
+ # api = Beowulf::Api.new
41
+ # virtual_supply = api.get_dynamic_global_properties do |prop, error|
42
+ # if prop.nil?
43
+ # puts error
44
+ # exis
45
+ # end
46
+ #
47
+ # prop.virtual_supply
48
+ # end
49
+ #
50
+ # List of remote methods:
51
+ #
52
+ # get_version
53
+ # get_block_header
54
+ # get_block
55
+ # get_config
56
+ # get_dynamic_global_properties
57
+ # get_supernode_schedule
58
+ # get_hardfork_version
59
+ # get_next_scheduled_hardfork
60
+ # get_accounts
61
+ # lookup_account_names
62
+ # lookup_accounts
63
+ # get_account_count
64
+ # get_owner_history
65
+ # get_transaction_hex
66
+ # get_transaction
67
+ # get_transaction_with_status
68
+ # get_pending_transaction_count
69
+ # get_required_signatures
70
+ # get_potential_signatures
71
+ # verify_authority
72
+ # get_supernodes
73
+ # get_supernode_by_accounts
74
+ # get_supernodes_by_vote
75
+ # lookup_supernode_accounts
76
+ # get_supernode_count
77
+ # get_active_supernodes
78
+ #
79
+ class Api
80
+ include Utils
81
+
82
+ DEFAULT_BEOWULF_URL = 'https://bw.beowulfchain.com/rpc'
83
+
84
+ DEFAULT_BEOWULF_FAILOVER_URLS = [
85
+ DEFAULT_BEOWULF_URL
86
+ ]
87
+
88
+ DEFAULT_RESTFUL_URL = 'https://bw.beowulfchain.com/rpc'
89
+
90
+ # @private
91
+ POST_HEADERS = {
92
+ 'Content-Type' => 'application/json',
93
+ 'User-Agent' => Beowulf::AGENT_ID
94
+ }
95
+
96
+ # @private
97
+ HEALTH_URI = '/health'
98
+
99
+ def self.default_url(chain)
100
+ case chain.to_sym
101
+ when :beowulf then DEFAULT_BEOWULF_URL
102
+ else; raise ApiError, "Unsupported chain: #{chain}"
103
+ end
104
+ end
105
+
106
+ def self.default_restful_url(chain)
107
+ case chain.to_sym
108
+ when :beowulf then DEFAULT_RESTFUL_URL
109
+ else; raise ApiError, "Unsupported chain: #{chain}"
110
+ end
111
+ end
112
+
113
+ def self.default_failover_urls(chain)
114
+ case chain.to_sym
115
+ when :beowulf then DEFAULT_BEOWULF_FAILOVER_URLS
116
+ else; raise ApiError, "Unsupported chain: #{chain}"
117
+ end
118
+ end
119
+
120
+ # Create a new instance of Beowulf::Api.
121
+ #
122
+ # Examples:
123
+ #
124
+ # api = Beowulf::Api.new(url: 'https://api.example.com')
125
+ #
126
+ # @param options [::Hash] The attributes to initialize the Beowulf::Api with.
127
+ # @option options [String] :url URL that points at a full node, like `https://bw.beowulfchain.com/rpc`. Default from DEFAULT_URL.
128
+ # @option options [::Array<String>] :failover_urls An array that contains one or more full nodes to fall back on. Default from DEFAULT_FAILOVER_URLS.
129
+ # @option options [Logger] :logger An instance of `Logger` to send debug messages to.
130
+ # @option options [Boolean] :recover_transactions_on_error Have Beowulf try to recover transactions that are accepted but could not be confirmed due to an error like network timeout. Default: `true`
131
+ # @option options [Integer] :max_requests Maximum number of requests on a connection before it is considered expired and automatically closed.
132
+ # @option options [Integer] :pool_size Maximum number of connections allowed.
133
+ # @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.
134
+ # @option options [Boolean] :persist Enable or disable Persistent HTTP. Using Persistent HTTP keeps the connection alive between API calls. Default: `true`
135
+ def initialize(options = {})
136
+ @user = options[:user]
137
+ @password = options[:password]
138
+ @chain = options[:chain] || :beowulf
139
+ @url = options[:url] || Api::default_url(@chain)
140
+ @restful_url = options[:restful_url] || Api::default_restful_url(@chain)
141
+ @preferred_url = @url.dup
142
+ @failover_urls = options[:failover_urls]
143
+ @debug = !!options[:debug]
144
+ @max_requests = options[:max_requests] || 30
145
+ @ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
146
+ @ssl_version = options[:ssl_version]
147
+
148
+ @self_logger = false
149
+ @logger = if options[:logger].nil?
150
+ @self_logger = true
151
+ Beowulf.logger
152
+ else
153
+ options[:logger]
154
+ end
155
+
156
+ @self_hashie_logger = false
157
+ @hashie_logger = if options[:hashie_logger].nil?
158
+ @self_hashie_logger = true
159
+ Logger.new(nil)
160
+ else
161
+ options[:hashie_logger]
162
+ end
163
+
164
+ if @failover_urls.nil?
165
+ @failover_urls = Api::default_failover_urls(@chain) - [@url]
166
+ end
167
+
168
+ @failover_urls = [@failover_urls].flatten.compact
169
+ @preferred_failover_urls = @failover_urls.dup
170
+
171
+ unless @hashie_logger.respond_to? :warn
172
+ @hashie_logger = Logger.new(@hashie_logger)
173
+ end
174
+
175
+ @recover_transactions_on_error = if options.keys.include? :recover_transactions_on_error
176
+ options[:recover_transactions_on_error]
177
+ else
178
+ true
179
+ end
180
+
181
+ @persist_error_count = 0
182
+ @persist = if options.keys.include? :persist
183
+ options[:persist]
184
+ else
185
+ true
186
+ end
187
+
188
+ @reuse_ssl_sessions = if options.keys.include? :reuse_ssl_sessions
189
+ options[:reuse_ssl_sessions]
190
+ else
191
+ true
192
+ end
193
+
194
+ @use_condenser_namespace = if options.keys.include? :use_condenser_namespace
195
+ options[:use_condenser_namespace]
196
+ else
197
+ true
198
+ end
199
+
200
+ if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
201
+ @pool_size = options[:pool_size] || Net::HTTP::Persistent::DEFAULT_POOL_SIZE
202
+ end
203
+
204
+ Hashie.logger = @hashie_logger
205
+ @method_names = nil
206
+ @uri = nil
207
+ @http_id = nil
208
+ @http_memo = {}
209
+ @api_options = options.dup.merge(chain: @chain)
210
+ @api = nil
211
+ @block_api = nil
212
+ @backoff_at = nil
213
+ @jussi_supported = []
214
+ end
215
+
216
+ # Get a specific block or range of blocks.
217
+ #
218
+ # Example:
219
+ #
220
+ # api = Beowulf::Api.new
221
+ # blocks = api.get_blocks(10..20)
222
+ # transactions = blocks.flat_map(&:transactions)
223
+ #
224
+ # ... or ...
225
+ #
226
+ # api = Beowulf::Api.new
227
+ # transactions = []
228
+ # api.get_blocks(10..20) do |block|
229
+ # transactions += block.transactions
230
+ # end
231
+ #
232
+ # @param block_number [Fixnum || ::Array<Fixnum>]
233
+ # @param block the block to execute for each result, optional.
234
+ # @return [::Array]
235
+ def get_blocks(block_number, &block)
236
+ block_number = [*(block_number)].flatten
237
+
238
+ if !!block
239
+ block_number.each do |i|
240
+ if use_condenser_namespace?
241
+ yield api.get_block(i)
242
+ else
243
+ yield block_api.get_block(block_num: i).result, i
244
+ end
245
+ end
246
+ else
247
+ block_number.map do |i|
248
+ if use_condenser_namespace?
249
+ api.get_block(i)
250
+ else
251
+ block_api.get_block(block_num: i).result
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ # Stops the persistent http connections.
258
+ #
259
+ def shutdown
260
+ @uri = nil
261
+ @http_id = nil
262
+ @http_memo.each do |k|
263
+ v = @http_memo.delete(k)
264
+ if defined?(v.shutdown)
265
+ debug "Shutting down instance #{k} (#{v})"
266
+ v.shutdown
267
+ end
268
+ end
269
+ @api.shutdown if !!@api && @api != self
270
+ @api = nil
271
+ @block_api.shutdown if !!@block_api && @block_api != self
272
+ @block_api = nil
273
+
274
+ if @self_logger
275
+ if !!@logger && defined?(@logger.close)
276
+ if defined?(@logger.closed?)
277
+ @logger.close unless @logger.closed?
278
+ end
279
+ end
280
+ end
281
+
282
+ if @self_hashie_logger
283
+ if !!@hashie_logger && defined?(@hashie_logger.close)
284
+ if defined?(@hashie_logger.closed?)
285
+ @hashie_logger.close unless @hashie_logger.closed?
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ # @private
292
+ def method_names
293
+ return @method_names if !!@method_names
294
+ return CondenserApi::METHOD_NAMES if api_name == :condenser_api
295
+
296
+ @method_names = Beowulf::Api.methods(api_name).map do |e|
297
+ e['method'].to_sym
298
+ end
299
+ end
300
+
301
+ # @private
302
+ def api_name
303
+ :condenser_api
304
+ end
305
+
306
+ # @private
307
+ def respond_to_missing?(m, include_private = false)
308
+ method_names.nil? ? false : method_names.include?(m.to_sym)
309
+ end
310
+
311
+ # @private
312
+ def method_missing(m, *args, &block)
313
+ super unless respond_to_missing?(m)
314
+
315
+ current_rpc_id = rpc_id
316
+ method_name = [api_name, m].join('.')
317
+ response = nil
318
+ options = if api_name == :condenser_api
319
+ {
320
+ jsonrpc: "2.0",
321
+ method: method_name,
322
+ params: args,
323
+ id: current_rpc_id,
324
+ }
325
+ else
326
+ rpc_args = if args.empty?
327
+ {}
328
+ else
329
+ args.first
330
+ end
331
+ {
332
+ jsonrpc: "2.0",
333
+ method: method_name,
334
+ params: rpc_args,
335
+ id: current_rpc_id,
336
+ }
337
+ end
338
+
339
+ tries = 0
340
+ timestamp = Time.now.utc
341
+
342
+ loop do
343
+ tries += 1
344
+
345
+ if tries > 5 && flappy? && !check_file_open?
346
+ raise ApiError, 'PANIC: Out of file resources'
347
+ end
348
+
349
+ begin
350
+ if tries > 1 && @recover_transactions_on_error && api_name == :network_broadcast_api
351
+ signatures, exp = extract_signatures(options)
352
+
353
+ if !!signatures && signatures.any?
354
+ offset = [(exp - timestamp).abs, 30].min
355
+
356
+ if !!(response = recover_transaction(signatures, current_rpc_id, timestamp - offset))
357
+ response = Hashie::Mash.new(response)
358
+ end
359
+ end
360
+ end
361
+
362
+ if response.nil?
363
+ response = request(options)
364
+
365
+ response = if response.nil?
366
+ error "No response, retrying ...", method_name
367
+ elsif !response.kind_of? Net::HTTPSuccess
368
+ warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name, true
369
+ else
370
+ detect_jussi(response)
371
+
372
+ case response.code
373
+ when '200'
374
+ body = response.body
375
+ response = JSON[body]
376
+
377
+ if response['id'] != options[:id]
378
+ debug_payload(options, body) if ENV['DEBUG'] == 'true'
379
+
380
+ if !!response['id']
381
+ warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name, true
382
+ else
383
+ # The node has broken the jsonrpc spec.
384
+ warning "Node did not provide jsonrpc id (expected: #{options[:id]}, got: nothing), retrying ...", method_name, true
385
+ end
386
+
387
+ if response.keys.include?('error')
388
+ handle_error(response, options, method_name, tries)
389
+ end
390
+ elsif response.keys.include?('error')
391
+ handle_error(response, options, method_name, tries)
392
+ else
393
+ Hashie::Mash.new(response)
394
+ end
395
+ when '400' then warning 'Code 400: Bad Request, retrying ...', method_name, true
396
+ when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name, true
397
+ when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name, true
398
+ when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name, true
399
+ when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name, true
400
+ else
401
+ warning "Unknown code #{response.code}, retrying ...", method_name, true
402
+ warning response
403
+ end
404
+ end
405
+ end
406
+ rescue Net::HTTP::Persistent::Error => e
407
+ warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name, true
408
+ if e.cause.class == Net::HTTPMethodNotAllowed
409
+ warning 'Node upstream is misconfigured.'
410
+ drop_current_failover_url method_name
411
+ end
412
+
413
+ @persist_error_count += 1
414
+ rescue ConnectionPool::Error => e
415
+ warning "Connection Pool Error (#{e.message}), retrying ...", method_name, true
416
+ rescue Errno::ECONNREFUSED => e
417
+ warning 'Connection refused, retrying ...', method_name, true
418
+ rescue Errno::EADDRNOTAVAIL => e
419
+ warning 'Node not available, retrying ...', method_name, true
420
+ rescue Errno::ECONNRESET => e
421
+ warning "Connection Reset (#{e.message}), retrying ...", method_name, true
422
+ rescue Errno::EBUSY => e
423
+ warning "Resource busy (#{e.message}), retrying ...", method_name, true
424
+ rescue Errno::ENETDOWN => e
425
+ warning "Network down (#{e.message}), retrying ...", method_name, true
426
+ rescue Net::ReadTimeout => e
427
+ warning 'Node read timeout, retrying ...', method_name, true
428
+ rescue Net::OpenTimeout => e
429
+ warning 'Node timeout, retrying ...', method_name, true
430
+ rescue RangeError => e
431
+ warning 'Range Error, retrying ...', method_name, true
432
+ rescue OpenSSL::SSL::SSLError => e
433
+ warning "SSL Error (#{e.message}), retrying ...", method_name, true
434
+ rescue SocketError => e
435
+ warning "Socket Error (#{e.message}), retrying ...", method_name, true
436
+ rescue JSON::ParserError => e
437
+ warning "JSON Parse Error (#{e.message}), retrying ...", method_name, true
438
+ drop_current_failover_url method_name if tries > 5
439
+ response = nil
440
+ rescue ApiError => e
441
+ warning "ApiError (#{e.message}), retrying ...", method_name, true
442
+ # rescue => e
443
+ # warning "Unknown exception from request, retrying ...", method_name, true
444
+ # warning e
445
+ end
446
+
447
+ if !!response
448
+ @persist_error_count = 0
449
+
450
+ if !!block
451
+ if api_name == :condenser_api
452
+ return yield(response.result, response.error, response.id)
453
+ else
454
+ if defined?(response.result.size) && response.result.size == 0
455
+ return yield(nil, response.error, response.id)
456
+ elsif (defined?(response.result.size) && response.result.size == 1 && defined?(response.result.values))
457
+ return yield(response.result.values.first, response.error, response.id)
458
+ else
459
+ return yield(response.result, response.error, response.id)
460
+ end
461
+ end
462
+ else
463
+ return response
464
+ end
465
+ end
466
+
467
+ backoff
468
+ end # loop
469
+ end
470
+
471
+ def inspect
472
+ properties = %w(
473
+ chain url backoff_at max_requests ssl_verify_mode ssl_version persist
474
+ recover_transactions_on_error reuse_ssl_sessions pool_size
475
+ use_condenser_namespace
476
+ ).map do |prop|
477
+ if !!(v = instance_variable_get("@#{prop}"))
478
+ "@#{prop}=#{v}"
479
+ end
480
+ end.compact.join(', ')
481
+
482
+ "#<#{self.class.name} [#{properties}]>"
483
+ end
484
+
485
+ def stopped?
486
+ http_active = if @http_memo.nil?
487
+ false
488
+ else
489
+ @http_memo.values.map do |http|
490
+ if defined?(http.active?)
491
+ http.active?
492
+ else
493
+ false
494
+ end
495
+ end.include?(true)
496
+ end
497
+
498
+ @uri.nil? && @http_id.nil? && !http_active && @api.nil? && @block_api.nil?
499
+ end
500
+
501
+ def use_condenser_namespace?
502
+ @use_condenser_namespace
503
+ end
504
+ private
505
+ def self.methods_json_path
506
+ @methods_json_path ||= "#{File.dirname(__FILE__)}/methods.json"
507
+ end
508
+
509
+ def self.methods(api_name)
510
+ @methods ||= {}
511
+ @methods[api_name] ||= JSON[File.read methods_json_path].map do |e|
512
+ e if e['api'].to_sym == api_name
513
+ end.compact.freeze
514
+ end
515
+
516
+ def self.apply_http_defaults(http, ssl_verify_mode)
517
+ http.read_timeout = 10
518
+ http.open_timeout = 10
519
+ http.verify_mode = ssl_verify_mode
520
+ http.ssl_timeout = 30 if defined? http.ssl_timeout
521
+ http
522
+ end
523
+
524
+ def api_options
525
+ @api_options.merge(failover_urls: @failover_urls, logger: @logger, hashie_logger: @hashie_logger)
526
+ end
527
+
528
+ def api
529
+ @api ||= self.class == Api ? self : Api.new(api_options)
530
+ end
531
+
532
+ def block_api
533
+ @block_api ||= self.class == BlockApi ? self : BlockApi.new(api_options)
534
+ end
535
+
536
+ def rpc_id
537
+ @rpc_id ||= 0
538
+ @rpc_id = @rpc_id + 1
539
+ end
540
+
541
+ def uri
542
+ @uri ||= URI.parse(@url)
543
+ end
544
+
545
+ def http_id
546
+ @http_id ||= "beowulf-#{Beowulf::VERSION}-#{api_name}-#{SecureRandom.uuid}"
547
+ end
548
+
549
+ def http
550
+ return @http_memo[http_id] if @http_memo.keys.include? http_id
551
+
552
+ @http_memo[http_id] = if @persist && @persist_error_count < 10
553
+ idempotent = api_name != :network_broadcast_api
554
+
555
+ http = if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
556
+ Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size)
557
+ else
558
+ # net-http-persistent < 3.0
559
+ Net::HTTP::Persistent.new(http_id)
560
+ end
561
+
562
+ http.keep_alive = 30
563
+ http.idle_timeout = idempotent ? 10 : nil
564
+ http.max_requests = @max_requests
565
+ http.retry_change_requests = idempotent
566
+ http.reuse_ssl_sessions = @reuse_ssl_sessions
567
+
568
+ http
569
+ else
570
+ http = Net::HTTP.new(uri.host, uri.port)
571
+ http.use_ssl = uri.scheme == 'https'
572
+ http
573
+ end
574
+
575
+ Api::apply_http_defaults(@http_memo[http_id], @ssl_verify_mode)
576
+ end
577
+
578
+ def post_request
579
+ Net::HTTP::Post.new uri.request_uri, POST_HEADERS
580
+ end
581
+
582
+ def request(options)
583
+ request = post_request
584
+ request.body = JSON[options]
585
+
586
+ case http
587
+ when Net::HTTP::Persistent then http.request(uri, request)
588
+ when Net::HTTP then http.request(request)
589
+ else; raise ApiError, "Unsuppored scheme: #{http.inspect}"
590
+ end
591
+ end
592
+
593
+ def jussi_supported?(url = @url)
594
+ @jussi_supported.include? url
595
+ end
596
+
597
+ def detect_jussi(response)
598
+ return if jussi_supported?(@url)
599
+
600
+ jussi_response_id = response['x-jussi-response-id']
601
+
602
+ if !!jussi_response_id
603
+ debug "Found a node that supports jussi: #{@url}"
604
+ @jussi_supported << @url
605
+ end
606
+ end
607
+
608
+ def recover_transaction(signatures, expected_rpc_id, after)
609
+ debug "Looking for signatures: #{signatures.map{|s| s[0..5]}} since: #{after}"
610
+
611
+ count = 0
612
+ start = Time.now.utc
613
+ block_range = api.get_dynamic_global_properties do |properties|
614
+ high = properties.head_block_number
615
+ low = high - 100
616
+ [*(low..(high))].reverse
617
+ end
618
+
619
+ # At most, we read 100 blocks
620
+ # but we also give up once the block time is before the `after` argument.
621
+ api.get_blocks(block_range) do |block, block_num|
622
+ unless defined? block.transaction_ids
623
+ error "Blockchain does not provide transaction ids in blocks, giving up."
624
+ return nil
625
+ end
626
+
627
+ count += 1
628
+ raise ApiError, "Race condition detected on remote node at: #{block_num}" if block.nil?
629
+
630
+ # In the future, it would be better to decode the operation and signature
631
+ # into the transaction id.
632
+ unless defined? block.transaction_ids
633
+ @recover_transactions_on_error = false
634
+ return
635
+ end
636
+
637
+ timestamp = Time.parse(block.timestamp + 'Z')
638
+ break if timestamp < after
639
+
640
+ block.transactions.each_with_index do |tx, index|
641
+ next unless ((tx['signatures'] || []) & signatures).any?
642
+
643
+ debug "Found transaction #{count} block(s) ago; took #{(Time.now.utc - start)} seconds to scan."
644
+
645
+ return {
646
+ id: expected_rpc_id,
647
+ recovered_by: http_id,
648
+ result: {
649
+ id: block.transaction_ids[index],
650
+ block_num: block_num,
651
+ trx_num: index,
652
+ expired: false
653
+ }
654
+ }
655
+ end
656
+ end
657
+
658
+ debug "Could not find transaction in #{count} block(s); took #{(Time.now.utc - start)} seconds to scan."
659
+
660
+ return nil
661
+ end
662
+
663
+ def reset_failover
664
+ @url = @preferred_url.dup
665
+ @failover_urls = @preferred_failover_urls.dup
666
+ warning "Failover reset, going back to #{@url} ..."
667
+ end
668
+
669
+ def pop_failover_url
670
+ reset_failover if @failover_urls.none?
671
+
672
+ until @failover_urls.none? || healthy?(url = @failover_urls.sample)
673
+ @failover_urls.delete(url)
674
+ end
675
+
676
+ url || (uri || @url).to_s
677
+ end
678
+
679
+ def bump_failover
680
+ @uri = nil
681
+ @url = pop_failover_url
682
+ warning "Failing over to #{@url} ..."
683
+ end
684
+
685
+ def flappy?
686
+ !!@backoff_at && Time.now.utc - @backoff_at < 300
687
+ end
688
+
689
+ # Note, this methods only removes the uri.to_s if present but it does not
690
+ # call bump_failover, in order to avoid a race condition.
691
+ def drop_current_failover_url(prefix)
692
+ if @preferred_failover_urls.size == 1
693
+ warning "Node #{uri} appears to be misconfigured but no other node is available, retrying ...", prefix
694
+ else
695
+ warning "Removing misconfigured node from failover urls: #{uri}, retrying ...", prefix
696
+ @preferred_failover_urls.delete(uri.to_s)
697
+ @failover_urls.delete(uri.to_s)
698
+ end
699
+ end
700
+
701
+ def handle_error(response, request_options, method_name, tries)
702
+ parser = ErrorParser.new(response)
703
+ _signatures, exp = extract_signatures(request_options)
704
+
705
+ if (!!exp && exp < Time.now.utc) || (tries > 2 && !parser.node_degraded?)
706
+ # Whatever the error was, it is already expired or tried too much. No need to try to recover.
707
+ debug "Error code #{parser} but transaction already expired or too many tries, giving up (attempt: #{tries})."
708
+ elsif parser.can_retry?
709
+ drop_current_failover_url method_name if !!exp && parser.expiry?
710
+ drop_current_failover_url method_name if parser.node_degraded?
711
+ debug "Error code #{parser} (attempt: #{tries}), retrying ..."
712
+ return nil
713
+ end
714
+
715
+ if !!parser.trx_id
716
+ # Turns out, the ErrorParser found a transaction id. It might come in
717
+ # handy, so let's append this to the result along with the error.
718
+
719
+ response[:result] = {
720
+ id: parser.trx_id,
721
+ block_num: -1,
722
+ trx_num: -1,
723
+ expired: false
724
+ }
725
+
726
+ if @recover_transactions_on_error
727
+ begin
728
+ if !!@restful_url
729
+ JSON[open("#{@restful_url}/account_history_api/get_transaction?id=#{parser.trx_id}").read].tap do |tx|
730
+ response[:result][:block_num] = tx['block_num']
731
+ response[:result][:trx_num] = tx['transaction_num']
732
+ end
733
+ else
734
+ # Node operators often disable this operation.
735
+ api.get_transaction(parser.trx_id) do |tx|
736
+ if !!tx
737
+ response[:result][:block_num] = tx.block_num
738
+ response[:result][:trx_num] = tx.transaction_num
739
+ end
740
+ end
741
+ end
742
+
743
+ response[:recovered_by] = http_id
744
+ response.delete('error') # no need for this, now
745
+ rescue
746
+ debug "Couldn't find block for trx_id: #{parser.trx_id}, giving up."
747
+ end
748
+ end
749
+ end
750
+
751
+ Hashie::Mash.new(response)
752
+ end
753
+
754
+ def healthy?(url)
755
+ begin
756
+ # Note, not all nodes support the /health uri. But even if they don't,
757
+ # they'll respond status code 200 OK, even if the body shows an error.
758
+ #
759
+ # But if the node supports the /health uri, it will do additional
760
+ # verifications on the block height.
761
+ #
762
+ # Also note, this check is done **without** net-http-persistent.
763
+
764
+ response = open(url + HEALTH_URI)
765
+ response = JSON[response.read]
766
+
767
+ if !!response['error']
768
+ if !!response['error']['data']
769
+ if !!response['error']['data']['message']
770
+ error "#{url} error: #{response['error']['data']['message']}"
771
+ end
772
+ elsif !!response['error']['message']
773
+ error "#{url} error: #{response['error']['message']}"
774
+ else
775
+ error "#{url} error: #{response['error']}"
776
+ end
777
+
778
+ false
779
+ elsif response['status'] == 'OK'
780
+ true
781
+ else
782
+ error "#{url} status: #{response['status']}"
783
+
784
+ false
785
+ end
786
+ rescue JSON::ParserError
787
+ # No JSON, but also no HTTP error code, so we're OK.
788
+
789
+ true
790
+ rescue => e
791
+ error "Health check failure for #{url}: #{e.inspect}"
792
+ sleep 0.2
793
+ false
794
+ end
795
+ end
796
+
797
+ def check_file_open?
798
+ File.exists?('.')
799
+ rescue
800
+ false
801
+ end
802
+
803
+ def debug_payload(request, response)
804
+ request = JSON.pretty_generate(request)
805
+ response = JSON.parse(response) rescue response
806
+ response = JSON.pretty_generate(response) rescue response
807
+
808
+ puts '=' * 80
809
+ puts "Request:"
810
+ puts request
811
+ puts '=' * 80
812
+ puts "Response:"
813
+ puts response
814
+ puts '=' * 80
815
+ end
816
+
817
+ def backoff
818
+ shutdown
819
+ bump_failover if flappy? || !healthy?(uri)
820
+ @backoff_at ||= Time.now.utc
821
+ @backoff_sleep ||= 0.01
822
+
823
+ @backoff_sleep *= 2
824
+ GC.start
825
+ sleep @backoff_sleep
826
+ ensure
827
+ if !!@backoff_at && Time.now.utc - @backoff_at > 300
828
+ @backoff_at = nil
829
+ @backoff_sleep = nil
830
+ end
831
+ end
832
+
833
+ def self.finalize(logger, hashie_logger)
834
+ proc {
835
+ if !!logger && defined?(logger.close) && !logger.closed?
836
+ logger.close
837
+ end
838
+
839
+ if !!hashie_logger && defined?(hashie_logger.close) && !hashie_logger.closed?
840
+ hashie_logger.close
841
+ end
842
+ }
843
+ end
844
+ end
845
+ end