beowulf-ruby 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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