radiator 0.3.9 → 0.3.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/Rakefile +11 -4
- data/lib/golos.rb +8 -0
- data/lib/radiator/api.rb +91 -29
- data/lib/radiator/base_error.rb +1 -0
- data/lib/radiator/chain.rb +335 -0
- data/lib/radiator/error_parser.rb +81 -42
- data/lib/radiator/stream.rb +54 -12
- data/lib/radiator/transaction.rb +6 -3
- data/lib/radiator/utils.rb +1 -1
- data/lib/radiator/version.rb +2 -1
- data/lib/radiator.rb +3 -0
- data/lib/steem.rb +8 -0
- data/radiator.gemspec +4 -2
- metadata +5 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4ff8172c52320522f9a22b4a710a1051837aa9d
|
4
|
+
data.tar.gz: 827457f802605807383f2c42350f671754fad7c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f79ed7cbb0e5a64a7a21c17e6b25bc254591b9e31b24ab733ffc93872de6949a994339844f4479780c86e34ac56546dbfb91d391875c861fc03ea4818f83c9a1
|
7
|
+
data.tar.gz: 53c4aa2777f751221c0e0bee02b2dc47abbed64dde1ae92d66db77ae57a4363d5554d552012f1372b480bd928c6f17ea07b0595f59d104397a3562e66de5a09b
|
data/Gemfile.lock
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
radiator (0.3.
|
4
|
+
radiator (0.3.10)
|
5
5
|
awesome_print (~> 1.7, >= 1.7.0)
|
6
6
|
bitcoin-ruby (~> 0.0, >= 0.0.11)
|
7
7
|
ffi (~> 1.9, >= 1.9.18)
|
8
8
|
hashie (~> 3.5, >= 3.5.5)
|
9
9
|
json (~> 2.0, >= 2.0.2)
|
10
10
|
logging (~> 2.2, >= 2.2.0)
|
11
|
-
net-http-persistent (
|
11
|
+
net-http-persistent (>= 2.5.2)
|
12
12
|
|
13
13
|
GEM
|
14
14
|
remote: https://rubygems.org/
|
data/Rakefile
CHANGED
@@ -52,16 +52,21 @@ task :test_live_broadcast, [:account, :wif, :chain] do |t, args|
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
-
desc 'Tests the ability to stream live data.'
|
56
|
-
task :test_live_stream, :chain do |t, args|
|
55
|
+
desc 'Tests the ability to stream live data. defaults: chain = steem; persist = true.'
|
56
|
+
task :test_live_stream, [:chain, :persist] do |t, args|
|
57
57
|
chain = args[:chain] || 'steem'
|
58
|
+
persist = (args[:persist] || 'true') == 'true'
|
58
59
|
last_block_number = 0
|
59
|
-
options = {chain: chain}
|
60
|
+
options = {chain: chain, persist: persist}
|
60
61
|
api = Radiator::Api.new(options)
|
61
62
|
total_ops = 0.0
|
62
63
|
total_vops = 0.0
|
64
|
+
elapsed = 0
|
65
|
+
count = 0
|
63
66
|
|
64
67
|
Radiator::Stream.new(options).blocks do |b, n|
|
68
|
+
start = Time.now.utc
|
69
|
+
|
65
70
|
if last_block_number == 0
|
66
71
|
# skip test
|
67
72
|
elsif last_block_number + 1 == n
|
@@ -80,7 +85,9 @@ task :test_live_stream, :chain do |t, args|
|
|
80
85
|
0
|
81
86
|
end
|
82
87
|
|
83
|
-
|
88
|
+
elapsed += Time.now.utc - start
|
89
|
+
count += 1
|
90
|
+
puts "#{n}: #{b.witness}; trx: #{t_size}; op: #{op_size}, vop: #{vop_size} (cumulative vop ratio: #{('%.2f' % (vop_ratio * 100))} %; average #{((elapsed / count) * 1000).to_i}ms)"
|
84
91
|
end
|
85
92
|
else
|
86
93
|
# This should not happen. If it does, there's likely a bug in Radiator.
|
data/lib/golos.rb
ADDED
data/lib/radiator/api.rb
CHANGED
@@ -150,12 +150,17 @@ module Radiator
|
|
150
150
|
|
151
151
|
DEFAULT_GOLOS_FAILOVER_URLS = [
|
152
152
|
DEFAULT_GOLOS_URL,
|
153
|
-
'https://api.golos.cf'
|
153
|
+
'https://api.golos.cf',
|
154
|
+
# not recommended:
|
155
|
+
# 'http://golos-seed.arcange.eu',
|
156
|
+
# not recommended, requires option ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE
|
157
|
+
# 'https://golos-seed.arcange.eu',
|
154
158
|
]
|
155
159
|
|
156
160
|
# @private
|
157
161
|
POST_HEADERS = {
|
158
|
-
'Content-Type' => 'application/json'
|
162
|
+
'Content-Type' => 'application/json',
|
163
|
+
'User-Agent' => Radiator::AGENT_ID
|
159
164
|
}
|
160
165
|
|
161
166
|
# @private
|
@@ -203,7 +208,6 @@ module Radiator
|
|
203
208
|
@hashie_logger = options[:hashie_logger] || Logger.new(nil)
|
204
209
|
@max_requests = options[:max_requests] || 30
|
205
210
|
@ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
|
206
|
-
@reuse_ssl_sessions = !!options[:reuse_ssl_sessions]
|
207
211
|
@ssl_version = options[:ssl_version]
|
208
212
|
|
209
213
|
if @failover_urls.nil?
|
@@ -223,17 +227,31 @@ module Radiator
|
|
223
227
|
true
|
224
228
|
end
|
225
229
|
|
230
|
+
@persist = if options.keys.include? :persist
|
231
|
+
options[:persist]
|
232
|
+
else
|
233
|
+
true
|
234
|
+
end
|
235
|
+
|
236
|
+
@reuse_ssl_sessions = if options.keys.include? :reuse_ssl_sessions
|
237
|
+
options[:reuse_ssl_sessions]
|
238
|
+
else
|
239
|
+
true
|
240
|
+
end
|
241
|
+
|
226
242
|
if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
|
227
243
|
@pool_size = options[:pool_size] || Net::HTTP::Persistent::DEFAULT_POOL_SIZE
|
228
244
|
end
|
229
245
|
|
230
246
|
Hashie.logger = @hashie_logger
|
231
247
|
@method_names = nil
|
232
|
-
@
|
248
|
+
@http_memo = {}
|
233
249
|
@api_options = options.dup.merge(chain: @chain)
|
234
250
|
@api = nil
|
235
251
|
@block_api = nil
|
236
252
|
@backoff_at = nil
|
253
|
+
|
254
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(self))
|
237
255
|
end
|
238
256
|
|
239
257
|
# Get a specific block or range of blocks.
|
@@ -355,8 +373,13 @@ module Radiator
|
|
355
373
|
def shutdown
|
356
374
|
@uri = nil
|
357
375
|
@http_id = nil
|
358
|
-
@
|
359
|
-
|
376
|
+
@http_memo.each do |k|
|
377
|
+
v = @http_memo.delete(k)
|
378
|
+
if defined?(v.shutdown)
|
379
|
+
debug "Shutting down instance #{k} (#{v})"
|
380
|
+
v.shutdown
|
381
|
+
end
|
382
|
+
end
|
360
383
|
@api.shutdown if !!@api && @api != self
|
361
384
|
@api = nil
|
362
385
|
@block_api.shutdown if !!@block_api && @block_api != self
|
@@ -485,6 +508,19 @@ module Radiator
|
|
485
508
|
backoff
|
486
509
|
end # loop
|
487
510
|
end
|
511
|
+
|
512
|
+
def inspect
|
513
|
+
properties = %w(
|
514
|
+
chain url backoff_at max_requests ssl_verify_mode ssl_version persist
|
515
|
+
recover_transactions_on_error reuse_ssl_sessions pool_size
|
516
|
+
).map do |prop|
|
517
|
+
if !!(v = instance_variable_get("@#{prop}"))
|
518
|
+
"@#{prop}=#{v}"
|
519
|
+
end
|
520
|
+
end.compact.join(', ')
|
521
|
+
|
522
|
+
"#<#{self.class.name} [#{properties}]>"
|
523
|
+
end
|
488
524
|
private
|
489
525
|
def self.methods_json_path
|
490
526
|
@methods_json_path ||= "#{File.dirname(__FILE__)}/methods.json"
|
@@ -497,6 +533,14 @@ module Radiator
|
|
497
533
|
end.compact.freeze
|
498
534
|
end
|
499
535
|
|
536
|
+
def self.apply_http_defaults(http, ssl_verify_mode)
|
537
|
+
http.read_timeout = 10
|
538
|
+
http.open_timeout = 10
|
539
|
+
http.verify_mode = ssl_verify_mode
|
540
|
+
http.ssl_timeout = 30
|
541
|
+
http
|
542
|
+
end
|
543
|
+
|
500
544
|
def api
|
501
545
|
@api ||= self.class == Api ? self : Api.new(@api_options)
|
502
546
|
end
|
@@ -519,26 +563,32 @@ module Radiator
|
|
519
563
|
end
|
520
564
|
|
521
565
|
def http
|
522
|
-
|
566
|
+
return @http_memo[http_id] if @http_memo.keys.include? http_id
|
567
|
+
|
568
|
+
@http_memo[http_id] = if @persist
|
569
|
+
idempotent = api_name != :network_broadcast_api
|
570
|
+
|
571
|
+
http = if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
|
572
|
+
Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size)
|
573
|
+
else
|
574
|
+
# net-http-persistent < 3.0
|
575
|
+
Net::HTTP::Persistent.new(http_id)
|
576
|
+
end
|
577
|
+
|
578
|
+
http.keep_alive = 30
|
579
|
+
http.idle_timeout = idempotent ? 10 : nil
|
580
|
+
http.max_requests = @max_requests
|
581
|
+
http.retry_change_requests = idempotent
|
582
|
+
http.reuse_ssl_sessions = @reuse_ssl_sessions
|
523
583
|
|
524
|
-
|
525
|
-
Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size)
|
584
|
+
http
|
526
585
|
else
|
527
|
-
|
528
|
-
|
586
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
587
|
+
http.use_ssl = uri.scheme == 'https'
|
588
|
+
http
|
529
589
|
end
|
530
590
|
|
531
|
-
@
|
532
|
-
@http.read_timeout = 10
|
533
|
-
@http.open_timeout = 10
|
534
|
-
@http.idle_timeout = idempotent ? 10 : nil
|
535
|
-
@http.max_requests = @max_requests
|
536
|
-
@http.retry_change_requests = idempotent
|
537
|
-
@http.verify_mode = @ssl_verify_mode
|
538
|
-
@http.reuse_ssl_sessions = @reuse_ssl_sessions
|
539
|
-
@http.ssl_version = @ssl_version
|
540
|
-
|
541
|
-
@http
|
591
|
+
Api::apply_http_defaults(@http_memo[http_id], @ssl_verify_mode)
|
542
592
|
end
|
543
593
|
|
544
594
|
def post_request
|
@@ -548,7 +598,12 @@ module Radiator
|
|
548
598
|
def request(options)
|
549
599
|
request = post_request
|
550
600
|
request.body = JSON[options]
|
551
|
-
|
601
|
+
|
602
|
+
case http
|
603
|
+
when Net::HTTP::Persistent then http.request(uri, request)
|
604
|
+
when Net::HTTP then http.request(request)
|
605
|
+
else; raise ApiError, "Unsuppored scheme: #{http.inspect}"
|
606
|
+
end
|
552
607
|
end
|
553
608
|
|
554
609
|
def recover_transaction(signatures, expected_rpc_id, after)
|
@@ -610,7 +665,7 @@ module Radiator
|
|
610
665
|
@failover_urls.delete(url)
|
611
666
|
end
|
612
667
|
|
613
|
-
url || @url
|
668
|
+
url || (uri || @url).to_s
|
614
669
|
end
|
615
670
|
|
616
671
|
def bump_failover
|
@@ -625,11 +680,11 @@ module Radiator
|
|
625
680
|
|
626
681
|
def drop_current_failover_url(prefix)
|
627
682
|
if @preferred_failover_urls.size == 1
|
628
|
-
warning "Node #{
|
683
|
+
warning "Node #{uri} appears to be misconfigured but no other node is available, retrying ...", prefix
|
629
684
|
else
|
630
|
-
warning "Removing misconfigured node from failover urls: #{
|
631
|
-
@preferred_failover_urls.delete(
|
632
|
-
@failover_urls.delete(
|
685
|
+
warning "Removing misconfigured node from failover urls: #{uri}, retrying ...", prefix
|
686
|
+
@preferred_failover_urls.delete(uri)
|
687
|
+
@failover_urls.delete(uri)
|
633
688
|
end
|
634
689
|
end
|
635
690
|
|
@@ -700,7 +755,7 @@ module Radiator
|
|
700
755
|
|
701
756
|
def backoff
|
702
757
|
shutdown
|
703
|
-
bump_failover if flappy? || !healthy?(
|
758
|
+
bump_failover if flappy? || !healthy?(uri)
|
704
759
|
@backoff_at ||= Time.now.utc
|
705
760
|
@backoff_sleep ||= 0.01
|
706
761
|
|
@@ -712,5 +767,12 @@ module Radiator
|
|
712
767
|
@backoff_sleep = nil
|
713
768
|
end
|
714
769
|
end
|
770
|
+
|
771
|
+
def self.finalize(obj)
|
772
|
+
proc {
|
773
|
+
puts "DESTROY OBJECT #{obj.inspect}" if ENV['LOG'] == 'TRACE'
|
774
|
+
obj.shutdown
|
775
|
+
}
|
776
|
+
end
|
715
777
|
end
|
716
778
|
end
|
data/lib/radiator/base_error.rb
CHANGED
@@ -20,3 +20,4 @@ module Radiator; class StreamError < BaseError; end; end
|
|
20
20
|
module Radiator; class TypeError < BaseError; end; end
|
21
21
|
module Radiator; class OperationError < BaseError; end; end
|
22
22
|
module Radiator; class TransactionError < BaseError; end; end
|
23
|
+
module Radiator; class ChainError < BaseError; end; end
|
@@ -0,0 +1,335 @@
|
|
1
|
+
module Radiator
|
2
|
+
# Examples ...
|
3
|
+
#
|
4
|
+
# To vote on a post/comment:
|
5
|
+
#
|
6
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
7
|
+
# steem.vote!(10000, 'author', 'post-or-comment-permlink')
|
8
|
+
#
|
9
|
+
# To post and vote in the same transaction:
|
10
|
+
#
|
11
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
12
|
+
# steem.post!(title: 'title of my post', body: 'body of my post', tags: ['tag'], self_upvote: 10000)
|
13
|
+
#
|
14
|
+
# To post and vote with declined payout:
|
15
|
+
#
|
16
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
17
|
+
#
|
18
|
+
# options = {
|
19
|
+
# title: 'title of my post',
|
20
|
+
# body: 'body of my post',
|
21
|
+
# tags: ['tag'],
|
22
|
+
# self_upvote: 10000,
|
23
|
+
# percent_steem_dollars: 0
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
# steem.post!(options)
|
27
|
+
#
|
28
|
+
class Chain
|
29
|
+
VALID_OPTIONS = %w(
|
30
|
+
chain account_name wif
|
31
|
+
).map(&:to_sym)
|
32
|
+
VALID_OPTIONS.each { |option| attr_accessor option }
|
33
|
+
|
34
|
+
def initialize(options = {})
|
35
|
+
options = options.dup
|
36
|
+
options.each do |k, v|
|
37
|
+
k = k.to_sym
|
38
|
+
if VALID_OPTIONS.include?(k.to_sym)
|
39
|
+
options.delete(k)
|
40
|
+
send("#{k}=", v)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@account_name ||= ENV['ACCOUNT_NAME']
|
45
|
+
@wif ||= ENV['WIF']
|
46
|
+
|
47
|
+
raise ChainError, "Required option: chain" if @chain.nil?
|
48
|
+
raise ChainError, "Required option: account_name, wif" if @account_name.nil? || @wif.nil?
|
49
|
+
|
50
|
+
reset
|
51
|
+
end
|
52
|
+
|
53
|
+
# Clears out queued operations.
|
54
|
+
def reset
|
55
|
+
@operations = []
|
56
|
+
end
|
57
|
+
|
58
|
+
# Broadcast queued operations.
|
59
|
+
#
|
60
|
+
# @param auto_reset [boolean] clears operations no matter what, even if there's an error.
|
61
|
+
def broadcast!(auto_reset = false)
|
62
|
+
begin
|
63
|
+
transaction = Radiator::Transaction.new(build_options)
|
64
|
+
transaction.operations = @operations
|
65
|
+
response = transaction.process(true)
|
66
|
+
rescue => e
|
67
|
+
reset if auto_reset
|
68
|
+
raise e
|
69
|
+
end
|
70
|
+
|
71
|
+
if !!response.result
|
72
|
+
reset
|
73
|
+
response
|
74
|
+
else
|
75
|
+
reset if auto_reset
|
76
|
+
ErrorParser.new(response)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Create a vote operation.
|
81
|
+
#
|
82
|
+
# Examples:
|
83
|
+
#
|
84
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
85
|
+
# steem.vote(10000, 'author', 'permlink')
|
86
|
+
# steem.broadcast!
|
87
|
+
#
|
88
|
+
# ... or ...
|
89
|
+
#
|
90
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
91
|
+
# steem.vote(10000, '@author/permlink')
|
92
|
+
# steem.broadcast!
|
93
|
+
#
|
94
|
+
# @param weight [Integer] value between -10000 and 10000.
|
95
|
+
# @param args [author, permlink || slug] pass either `author` and `permlink` or string containing both like `@author/permlink`.
|
96
|
+
def vote(weight, *args)
|
97
|
+
author, permlink = if args.size == 1
|
98
|
+
author, permlink = parse_slug(args[0])
|
99
|
+
else
|
100
|
+
author, permlink = args
|
101
|
+
end
|
102
|
+
|
103
|
+
@operations << {
|
104
|
+
type: :vote,
|
105
|
+
voter: account_name,
|
106
|
+
author: author,
|
107
|
+
permlink: permlink,
|
108
|
+
weight: weight
|
109
|
+
}
|
110
|
+
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create a vote operation and broadcasts it right away.
|
115
|
+
#
|
116
|
+
# Examples:
|
117
|
+
#
|
118
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
119
|
+
# steem.vote!(10000, 'author', 'permlink')
|
120
|
+
#
|
121
|
+
# ... or ...
|
122
|
+
#
|
123
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
124
|
+
# steem.vote!(10000, '@author/permlink')
|
125
|
+
#
|
126
|
+
# @see vote
|
127
|
+
def vote!(weight, *args); vote(weight, *args).broadcast!(true); end
|
128
|
+
|
129
|
+
# Creates a post operation.
|
130
|
+
#
|
131
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
132
|
+
# options = {
|
133
|
+
# title: 'This is my fancy post title.',
|
134
|
+
# body: 'This is my fancy post body.',
|
135
|
+
# tags: %w(thess are my fancy tags)
|
136
|
+
# }
|
137
|
+
# steem.post(options)
|
138
|
+
# steem.broadcast!
|
139
|
+
#
|
140
|
+
# @param options [Hash] options
|
141
|
+
# @option options [String] :title Title of the post.
|
142
|
+
# @option options [String] :body Body of the post.
|
143
|
+
# @option options [Array<String>] :tags Tags of the post.
|
144
|
+
# @option options [String] :permlink (automatic) Permlink of the post, defaults to formatted title.
|
145
|
+
# @option options [String] :parent_permlink (automatic) Parent permlink of the post, defaults to first tag.
|
146
|
+
# @option options [String] :parent_author (optional) Parent author of the post (only used if reply).
|
147
|
+
# @option options [String] :max_accepted_payout (1000000.000 SBD) Maximum accepted payout, set to '0.000 SBD' to deline payout
|
148
|
+
# @option options [Integer] :percent_steem_dollars (5000) Percent STEEM Dollars is used to set 50/50 or 100% STEEM Power
|
149
|
+
# @option options [Integer] :allow_votes (true) Allow votes for this post.
|
150
|
+
# @option options [Integer] :allow_curation_rewards (true) Allow curation rewards for this post.
|
151
|
+
def post(options = {})
|
152
|
+
tags = [options[:tags] || []].flatten
|
153
|
+
title = options[:title].to_s
|
154
|
+
permlink = options[:permlink] || title.downcase.gsub(/[^a-z0-9\-]+/, '-')
|
155
|
+
parent_permlink = options[:parent_permlink] || tags[0]
|
156
|
+
|
157
|
+
raise ChainError, 'At least one tag is required or set the parent_permlink directy.' if parent_permlink.nil?
|
158
|
+
|
159
|
+
body = options[:body]
|
160
|
+
parent_author = options[:parent_author] || ''
|
161
|
+
max_accepted_payout = options[:max_accepted_payout] || default_max_acepted_payout
|
162
|
+
percent_steem_dollars = options[:percent_steem_dollars]
|
163
|
+
allow_votes = options[:allow_votes] || true
|
164
|
+
allow_curation_rewards = options[:allow_curation_rewards] || true
|
165
|
+
self_vote = options[:self_vote]
|
166
|
+
|
167
|
+
tags.insert(0, parent_permlink)
|
168
|
+
tags = tags.compact.uniq
|
169
|
+
|
170
|
+
metadata = {
|
171
|
+
app: Radiator::AGENT_ID
|
172
|
+
}
|
173
|
+
metadata[:tags] = tags if tags.any?
|
174
|
+
|
175
|
+
@operations << {
|
176
|
+
type: :comment,
|
177
|
+
parent_permlink: parent_permlink,
|
178
|
+
author: account_name,
|
179
|
+
permlink: permlink,
|
180
|
+
title: title,
|
181
|
+
body: body,
|
182
|
+
json_metadata: metadata.to_json,
|
183
|
+
parent_author: parent_author
|
184
|
+
}
|
185
|
+
|
186
|
+
if (!!max_accepted_payout &&
|
187
|
+
max_accepted_payout != default_max_acepted_payout
|
188
|
+
) || !!percent_steem_dollars || !allow_votes || !allow_curation_rewards
|
189
|
+
@operations << {
|
190
|
+
type: :comment_options,
|
191
|
+
author: account_name,
|
192
|
+
permlink: permlink,
|
193
|
+
max_accepted_payout: max_accepted_payout,
|
194
|
+
percent_steem_dollars: percent_steem_dollars,
|
195
|
+
allow_votes: allow_votes,
|
196
|
+
allow_curation_rewards: allow_curation_rewards,
|
197
|
+
extensions: []
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
vote(self_vote, account_name, permlink) if !!self_vote
|
202
|
+
|
203
|
+
self
|
204
|
+
end
|
205
|
+
|
206
|
+
# Create a vote operation and broadcasts it right away.
|
207
|
+
#
|
208
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
209
|
+
# options = {
|
210
|
+
# title: 'This is my fancy post title.',
|
211
|
+
# body: 'This is my fancy post body.',
|
212
|
+
# tags: %w(thess are my fancy tags)
|
213
|
+
# }
|
214
|
+
# steem.post!(options)
|
215
|
+
#
|
216
|
+
# @see post
|
217
|
+
def post!(options = {}); post(options).broadcast!(true); end
|
218
|
+
|
219
|
+
# Create a delete_comment operation.
|
220
|
+
#
|
221
|
+
# Examples:
|
222
|
+
#
|
223
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
224
|
+
# steem.delete_comment('permlink')
|
225
|
+
# steem.broadcast!
|
226
|
+
#
|
227
|
+
# @param permlink
|
228
|
+
def delete_comment(permlink)
|
229
|
+
@operations << {
|
230
|
+
type: :delete_comment,
|
231
|
+
author: account_name,
|
232
|
+
permlink: permlink
|
233
|
+
}
|
234
|
+
|
235
|
+
self
|
236
|
+
end
|
237
|
+
|
238
|
+
# Create a delete_comment operation and broadcasts it right away.
|
239
|
+
#
|
240
|
+
# Examples:
|
241
|
+
#
|
242
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
243
|
+
# steem.delete_comment!('permlink')
|
244
|
+
#
|
245
|
+
# @see delete_comment
|
246
|
+
def delete_comment!(permlink); delete_comment(permlink).broadcast!(true); end
|
247
|
+
|
248
|
+
# Create a claim_reward_balance operation.
|
249
|
+
#
|
250
|
+
# Examples:
|
251
|
+
#
|
252
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
253
|
+
# steem.claim_reward_balance(reward_sbd: '100.000 SBD')
|
254
|
+
# steem.broadcast!
|
255
|
+
#
|
256
|
+
# @param options [Hash] options
|
257
|
+
# @option options [String] :reward_steem The amount of STEEM to claim, like: `100.000 STEEM`
|
258
|
+
# @option options [String] :reward_sbd The amount of SBD to claim, like: `100.000 SBD`
|
259
|
+
# @option options [String] :reward_vests The amount of VESTS to claim, like: `100.000000 VESTS`
|
260
|
+
def claim_reward_balance(options)
|
261
|
+
reward_steem = options[:reward_steem] || '0.000 STEEM'
|
262
|
+
reward_sbd = options[:reward_sbd] || '0.000 SBD'
|
263
|
+
reward_vests = options[:reward_vests] || '0.000000 VESTS'
|
264
|
+
|
265
|
+
@operations << {
|
266
|
+
type: :claim_reward_balance,
|
267
|
+
account: account_name,
|
268
|
+
reward_steem: reward_steem,
|
269
|
+
reward_sbd: reward_sbd,
|
270
|
+
reward_vests: reward_vests
|
271
|
+
}
|
272
|
+
|
273
|
+
self
|
274
|
+
end
|
275
|
+
|
276
|
+
# Create a claim_reward_balance operation and broadcasts it right away.
|
277
|
+
#
|
278
|
+
# Examples:
|
279
|
+
#
|
280
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
281
|
+
# steem.claim_reward_balance!(reward_sbd: '100.000 SBD')
|
282
|
+
#
|
283
|
+
# @see claim_reward_balance
|
284
|
+
def claim_reward_balance!(permlink); claim_reward_balance(permlink).broadcast!(true); end
|
285
|
+
|
286
|
+
# Create a transfer operation.
|
287
|
+
#
|
288
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your active wif')
|
289
|
+
# steem.transfer(amount: '1.000 SBD', to: 'account name', memo: 'this is a memo')
|
290
|
+
# steem.broadcast!
|
291
|
+
#
|
292
|
+
# @param options [Hash] options
|
293
|
+
# @option options [String] :amount The amount to transfer, like: `100.000 STEEM`
|
294
|
+
# @option options [String] :to The account receiving the transfer.
|
295
|
+
# @option options [String] :memo ('') The memo for the transfer.
|
296
|
+
def transfer(options = {})
|
297
|
+
@operations << options.merge(type: :transfer, from: account_name)
|
298
|
+
|
299
|
+
self
|
300
|
+
end
|
301
|
+
|
302
|
+
# Create a transfer operation and broadcasts it right away.
|
303
|
+
#
|
304
|
+
# steem = Steem.new(account_name: 'your account name', wif: 'your wif')
|
305
|
+
# steem.transfer!(amount: '1.000 SBD', to: 'account name', memo: 'this is a memo')
|
306
|
+
#
|
307
|
+
# @see transfer
|
308
|
+
def transfer!(options = {}); transfer(options).broadcast!(true); end
|
309
|
+
private
|
310
|
+
def build_options
|
311
|
+
{
|
312
|
+
chain: chain,
|
313
|
+
wif: wif
|
314
|
+
}
|
315
|
+
end
|
316
|
+
|
317
|
+
def parse_slug(slug)
|
318
|
+
slug = slug.split('@').last
|
319
|
+
author = slug.split('/')[0]
|
320
|
+
[author, slug.split('/')[1..-1].join('/')]
|
321
|
+
end
|
322
|
+
|
323
|
+
def default_max_acepted_payout
|
324
|
+
"1000000.000 #{default_debt_asset}"
|
325
|
+
end
|
326
|
+
|
327
|
+
def default_debt_asset
|
328
|
+
case chain
|
329
|
+
when :steem then 'SBD'
|
330
|
+
when :golos then 'GBG'
|
331
|
+
else; raise ChainError, "Unknown chain: #{chain}"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
require 'awesome_print'
|
2
|
+
|
1
3
|
module Radiator
|
2
4
|
class ErrorParser
|
3
5
|
include Utils
|
4
6
|
|
5
|
-
attr_reader :response, :error_code, :error_message,
|
7
|
+
attr_reader :response, :error, :error_code, :error_message,
|
6
8
|
:api_name, :api_method, :api_params,
|
7
9
|
:expiry, :can_retry, :can_reprepare, :trx_id, :debug
|
8
10
|
|
@@ -22,6 +24,7 @@ module Radiator
|
|
22
24
|
def initialize(response)
|
23
25
|
@response = response
|
24
26
|
|
27
|
+
@error = nil
|
25
28
|
@error_code = nil
|
26
29
|
@error_message = nil
|
27
30
|
@api_name = nil
|
@@ -38,53 +41,85 @@ module Radiator
|
|
38
41
|
end
|
39
42
|
|
40
43
|
def parse_error_response
|
41
|
-
|
42
|
-
|
43
|
-
@error_code = response['error']['data']['code']
|
44
|
-
stacks = response['error']['data']['stack']
|
45
|
-
stack_formats = stacks.map { |s| s['format'] }
|
46
|
-
stack_datum = stacks.map { |s| s['data'] }
|
47
|
-
data_call_method = stack_datum.find { |data| data['call.method'] == 'call' }
|
48
|
-
|
49
|
-
@error_message = stack_formats.reject(&:empty?).join('; ')
|
50
|
-
|
51
|
-
@api_name, @api_method, @api_params = if !!data_call_method
|
52
|
-
@api_name = data_call_method['call.params']
|
53
|
-
end
|
54
|
-
|
55
|
-
# See if we can recover a transaction id out of this hot mess.
|
56
|
-
data_trx_ix = stack_datum.find { |data| !!data['trx_ix'] }
|
57
|
-
@trx_id = data_trx_ix['trx_ix'] if !!data_trx_ix
|
58
|
-
|
59
|
-
case @error_code
|
60
|
-
when 10
|
44
|
+
if response.nil?
|
61
45
|
@expiry = false
|
62
46
|
@can_retry = false
|
63
|
-
@can_reprepare =
|
64
|
-
|
65
|
-
|
66
|
-
|
47
|
+
@can_reprepare = false
|
48
|
+
|
49
|
+
return
|
50
|
+
end
|
51
|
+
|
52
|
+
@response = JSON[response] if response.class == String
|
53
|
+
|
54
|
+
@error = if !!@response['error']
|
55
|
+
response['error']
|
56
|
+
else
|
57
|
+
response
|
58
|
+
end
|
59
|
+
|
60
|
+
begin
|
61
|
+
@error_code = @error['data']['code']
|
62
|
+
stacks = @error['data']['stack']
|
63
|
+
stack_formats = stacks.map { |s| s['format'] }
|
64
|
+
stack_datum = stacks.map { |s| s['data'] }
|
65
|
+
data_call_method = stack_datum.find { |data| data['call.method'] == 'call' }
|
66
|
+
|
67
|
+
@error_message = stack_formats.reject(&:empty?).join('; ')
|
68
|
+
|
69
|
+
@api_name, @api_method, @api_params = if !!data_call_method
|
70
|
+
@api_name = data_call_method['call.params']
|
67
71
|
end
|
68
|
-
when 4030100
|
69
|
-
# Code 4030100 is "transaction_expiration_exception: transaction
|
70
|
-
# expiration exception". If we assume the expiration was valid, the
|
71
|
-
# node might be bad and needs to be dropped.
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
@
|
76
|
-
when 4030200
|
77
|
-
# Code 4030200 is "transaction tapos exception". They are recoverable
|
78
|
-
# if the transaction hasn't expired yet. A tapos exception can be
|
79
|
-
# retried in situations where the node is behind and the tapos is
|
80
|
-
# based on a block the node doesn't know about yet.
|
73
|
+
# See if we can recover a transaction id out of this hot mess.
|
74
|
+
data_trx_ix = stack_datum.find { |data| !!data['trx_ix'] }
|
75
|
+
@trx_id = data_trx_ix['trx_ix'] if !!data_trx_ix
|
81
76
|
|
82
|
-
@
|
83
|
-
|
77
|
+
case @error_code
|
78
|
+
when 10
|
79
|
+
@expiry = false
|
80
|
+
@can_retry = false
|
81
|
+
@can_reprepare = if @api_name == 'network_broadcast_api'
|
82
|
+
(stack_formats & REPREPARE_WHITELIST).any?
|
83
|
+
else
|
84
|
+
false
|
85
|
+
end
|
86
|
+
when 13
|
87
|
+
@error_message = @error['data']['message']
|
88
|
+
@expiry = false
|
89
|
+
@can_retry = false
|
90
|
+
@can_reprepare = false
|
91
|
+
when 3030000
|
92
|
+
@error_message = @error['data']['message']
|
93
|
+
@expiry = false
|
94
|
+
@can_retry = false
|
95
|
+
@can_reprepare = false
|
96
|
+
when 4030100
|
97
|
+
# Code 4030100 is "transaction_expiration_exception: transaction
|
98
|
+
# expiration exception". If we assume the expiration was valid, the
|
99
|
+
# node might be bad and needs to be dropped.
|
100
|
+
|
101
|
+
@expiry = true
|
102
|
+
@can_retry = true
|
103
|
+
@can_reprepare = false
|
104
|
+
when 4030200
|
105
|
+
# Code 4030200 is "transaction tapos exception". They are recoverable
|
106
|
+
# if the transaction hasn't expired yet. A tapos exception can be
|
107
|
+
# retried in situations where the node is behind and the tapos is
|
108
|
+
# based on a block the node doesn't know about yet.
|
109
|
+
|
110
|
+
@expiry = false
|
111
|
+
@can_retry = true
|
112
|
+
|
113
|
+
# Allow fall back to reprepare if retry fails.
|
114
|
+
@can_reprepare = true
|
115
|
+
else
|
116
|
+
@expiry = false
|
117
|
+
@can_retry = false
|
118
|
+
@can_reprepare = false
|
119
|
+
end
|
120
|
+
rescue => e
|
121
|
+
ap error_perser_exception: e, original_response: response
|
84
122
|
|
85
|
-
# Allow fall back to reprepare if retry fails.
|
86
|
-
@can_reprepare = true
|
87
|
-
else
|
88
123
|
@expiry = false
|
89
124
|
@can_retry = false
|
90
125
|
@can_reprepare = false
|
@@ -98,5 +133,9 @@ module Radiator
|
|
98
133
|
error_code.to_s
|
99
134
|
end
|
100
135
|
end
|
136
|
+
|
137
|
+
def inspect
|
138
|
+
"#<#{self.class.name} [#{to_s}]>"
|
139
|
+
end
|
101
140
|
end
|
102
141
|
end
|
data/lib/radiator/stream.rb
CHANGED
@@ -23,6 +23,8 @@ module Radiator
|
|
23
23
|
# @private
|
24
24
|
MAX_BLOCKS_PER_NODE = 1000
|
25
25
|
|
26
|
+
RANGE_BEHIND_WARNING = 400
|
27
|
+
|
26
28
|
def initialize(options = {})
|
27
29
|
super
|
28
30
|
end
|
@@ -212,17 +214,24 @@ module Radiator
|
|
212
214
|
# @param mode we have the choice between
|
213
215
|
# * :head the last block
|
214
216
|
# * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
|
215
|
-
#
|
217
|
+
# @param max_blocks_per_node the number of blocks to read before trying a new node
|
216
218
|
# @param block the block to execute for each result, optional.
|
217
219
|
# @return [Hash]
|
218
220
|
def blocks(start = nil, mode = :irreversible, max_blocks_per_node = MAX_BLOCKS_PER_NODE, &block)
|
221
|
+
reset_api
|
222
|
+
|
223
|
+
replay = !!start
|
219
224
|
counter = 0
|
220
225
|
latest_block_number = -1
|
221
226
|
@api_options[:max_requests] = [max_blocks_per_node * 2, @api_options[:max_requests].to_i].max
|
222
227
|
|
223
228
|
loop do
|
229
|
+
break if stop?
|
230
|
+
|
224
231
|
catch :sequence do; begin
|
225
232
|
head_block = api.get_dynamic_global_properties do |properties|
|
233
|
+
break if stop?
|
234
|
+
|
226
235
|
if properties.head_block_number.nil?
|
227
236
|
# This can happen if a reverse proxy is acting up.
|
228
237
|
standby "Bad block sequence after height: #{latest_block_number}", {
|
@@ -251,18 +260,27 @@ module Radiator
|
|
251
260
|
start ||= head_block
|
252
261
|
range = (start..head_block)
|
253
262
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
standby "Stream behind by #{range.size} blocks (about #{(range.size * 3) / 60.0} minutes)."
|
258
|
-
end
|
259
|
-
|
260
|
-
[*range].each do |n|
|
263
|
+
for n in range
|
264
|
+
break if stop?
|
265
|
+
|
261
266
|
if (counter += 1) > max_blocks_per_node
|
262
|
-
|
267
|
+
reset_api
|
263
268
|
counter = 0
|
264
269
|
end
|
265
|
-
|
270
|
+
|
271
|
+
if !replay && range.size > RANGE_BEHIND_WARNING
|
272
|
+
# When the range is above RANGE_BEHIND_WARNING blocks, it's time
|
273
|
+
# to warn, unless we're replaying.
|
274
|
+
|
275
|
+
r = [*range]
|
276
|
+
index = r.index(n)
|
277
|
+
current_range = r[index..-1]
|
278
|
+
|
279
|
+
if current_range.size % RANGE_BEHIND_WARNING == 0
|
280
|
+
standby "Stream behind by #{current_range.size} blocks (about #{(current_range.size * 3) / 60.0} minutes)."
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
266
284
|
block_api.get_block(n) do |current_block, error|
|
267
285
|
if current_block.nil?
|
268
286
|
standby "Node responded with: empty block, retrying ...", {
|
@@ -295,13 +313,24 @@ module Radiator
|
|
295
313
|
# Stops the persistant http connections.
|
296
314
|
#
|
297
315
|
def shutdown
|
316
|
+
flappy = false
|
317
|
+
|
298
318
|
begin
|
299
|
-
@api.
|
319
|
+
unless @api.nil?
|
320
|
+
flappy = @api.send(:flappy?)
|
321
|
+
@api.shutdown
|
322
|
+
end
|
323
|
+
|
324
|
+
unless @block_api.nil?
|
325
|
+
flappy = @block_api.send(:flappy?) unless flappy
|
326
|
+
@block_api.shutdown
|
327
|
+
end
|
300
328
|
rescue => e
|
301
329
|
warning("Unable to shut down: #{e}")
|
302
330
|
end
|
303
331
|
|
304
332
|
@api = nil
|
333
|
+
@block_api = nil
|
305
334
|
end
|
306
335
|
|
307
336
|
# @private
|
@@ -353,6 +382,8 @@ module Radiator
|
|
353
382
|
@latest_values ||= []
|
354
383
|
@latest_values.shift(5) if @latest_values.size > 20
|
355
384
|
loop do
|
385
|
+
break if stop?
|
386
|
+
|
356
387
|
value = if (n = method_params(m)).nil?
|
357
388
|
key_value = api.get_dynamic_global_properties.result[m]
|
358
389
|
else
|
@@ -362,12 +393,14 @@ module Radiator
|
|
362
393
|
key_value = param = r[n[key]]
|
363
394
|
result = nil
|
364
395
|
loop do
|
396
|
+
break if stop?
|
397
|
+
|
365
398
|
response = api.send(key, param)
|
366
399
|
raise StreamError, JSON[response.error] if !!response.error
|
367
400
|
result = response.result
|
368
401
|
break if !!result
|
369
402
|
warnning "#{key}: #{param} result missing, retrying with timeout: #{@timeout || INITIAL_TIMEOUT} seconds"
|
370
|
-
|
403
|
+
reset_api
|
371
404
|
sleep timeout
|
372
405
|
end
|
373
406
|
@timeout = INITIAL_TIMEOUT
|
@@ -388,6 +421,11 @@ module Radiator
|
|
388
421
|
end
|
389
422
|
end
|
390
423
|
|
424
|
+
def reset_api
|
425
|
+
shutdown
|
426
|
+
!!api && !!block_api
|
427
|
+
end
|
428
|
+
|
391
429
|
def timeout
|
392
430
|
@timeout ||= INITIAL_TIMEOUT
|
393
431
|
@timeout *= 2
|
@@ -401,6 +439,10 @@ module Radiator
|
|
401
439
|
(Radiator::OperationTypes::TYPES.keys && type).any?
|
402
440
|
end
|
403
441
|
|
442
|
+
def stop?
|
443
|
+
@api.nil? || @block_api.nil?
|
444
|
+
end
|
445
|
+
|
404
446
|
def standby(message, options = {})
|
405
447
|
error = options[:error]
|
406
448
|
secondary = options[:and] || {}
|
data/lib/radiator/transaction.rb
CHANGED
@@ -164,7 +164,7 @@ module Radiator
|
|
164
164
|
if block.nil?
|
165
165
|
warning "Block missing while trying to prepare transaction, retrying ..."
|
166
166
|
else
|
167
|
-
debug block if ENV['LOG']
|
167
|
+
debug block if %w(DEBUG TRACE).include? ENV['LOG']
|
168
168
|
|
169
169
|
warning "Block structure while trying to prepare transaction, retrying ..."
|
170
170
|
end
|
@@ -197,14 +197,17 @@ module Radiator
|
|
197
197
|
Digest::SHA256.digest(to_bytes)
|
198
198
|
end
|
199
199
|
|
200
|
+
# May not find all non-canonicals, see: https://github.com/lian/bitcoin-ruby/issues/196
|
200
201
|
def signature
|
201
202
|
public_key_hex = @private_key.pub
|
202
203
|
ec = Bitcoin::OpenSSL_EC
|
203
204
|
digest_hex = digest.freeze
|
205
|
+
count = 0
|
204
206
|
|
205
207
|
loop do
|
206
|
-
|
207
|
-
|
208
|
+
count += 1
|
209
|
+
debug "#{count} attempts to find canonical signature" if count % 40 == 0
|
210
|
+
sig = ec.sign_compact(digest_hex, @private_key.priv, public_key_hex, false)
|
208
211
|
|
209
212
|
next if public_key_hex != ec.recover_compact(digest_hex, sig)
|
210
213
|
|
data/lib/radiator/utils.rb
CHANGED
data/lib/radiator/version.rb
CHANGED
data/lib/radiator.rb
CHANGED
data/lib/steem.rb
ADDED
data/radiator.gemspec
CHANGED
@@ -27,11 +27,13 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_development_dependency 'vcr', '~> 3.0', '>= 3.0.3'
|
28
28
|
spec.add_development_dependency 'yard', '~> 0.9.9'
|
29
29
|
|
30
|
-
|
30
|
+
# net-http-persistent has an open-ended dependency because radiator directly
|
31
|
+
# supports net-http-persistent-3.0.0 as well as net-http-persistent-2.5.2.
|
32
|
+
spec.add_dependency('net-http-persistent', '>= 2.5.2')
|
31
33
|
spec.add_dependency('json', '~> 2.0', '>= 2.0.2')
|
32
34
|
spec.add_dependency('logging', '~> 2.2', '>= 2.2.0')
|
33
35
|
spec.add_dependency('hashie', '~> 3.5', '>= 3.5.5')
|
34
36
|
spec.add_dependency('bitcoin-ruby', '~> 0.0', '>= 0.0.11')
|
35
37
|
spec.add_dependency('ffi', '~> 1.9', '>= 1.9.18')
|
36
|
-
spec.add_dependency
|
38
|
+
spec.add_dependency('awesome_print', '~> 1.7', '>= 1.7.0')
|
37
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: radiator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anthony Martin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-11-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -176,9 +176,6 @@ dependencies:
|
|
176
176
|
name: net-http-persistent
|
177
177
|
requirement: !ruby/object:Gem::Requirement
|
178
178
|
requirements:
|
179
|
-
- - "~>"
|
180
|
-
- !ruby/object:Gem::Version
|
181
|
-
version: '3.0'
|
182
179
|
- - ">="
|
183
180
|
- !ruby/object:Gem::Version
|
184
181
|
version: 2.5.2
|
@@ -186,9 +183,6 @@ dependencies:
|
|
186
183
|
prerelease: false
|
187
184
|
version_requirements: !ruby/object:Gem::Requirement
|
188
185
|
requirements:
|
189
|
-
- - "~>"
|
190
|
-
- !ruby/object:Gem::Version
|
191
|
-
version: '3.0'
|
192
186
|
- - ">="
|
193
187
|
- !ruby/object:Gem::Version
|
194
188
|
version: 2.5.2
|
@@ -331,12 +325,14 @@ files:
|
|
331
325
|
- images/Anthony Martin.png
|
332
326
|
- images/Marvin Hofmann.jpg
|
333
327
|
- images/Marvin Hofmann.png
|
328
|
+
- lib/golos.rb
|
334
329
|
- lib/radiator.rb
|
335
330
|
- lib/radiator/account_by_key_api.rb
|
336
331
|
- lib/radiator/api.rb
|
337
332
|
- lib/radiator/base_error.rb
|
338
333
|
- lib/radiator/block_api.rb
|
339
334
|
- lib/radiator/broadcast_operations.json
|
335
|
+
- lib/radiator/chain.rb
|
340
336
|
- lib/radiator/chain_config.rb
|
341
337
|
- lib/radiator/chain_stats_api.rb
|
342
338
|
- lib/radiator/condenser_api.rb
|
@@ -365,6 +361,7 @@ files:
|
|
365
361
|
- lib/radiator/type/u_int32.rb
|
366
362
|
- lib/radiator/utils.rb
|
367
363
|
- lib/radiator/version.rb
|
364
|
+
- lib/steem.rb
|
368
365
|
- radiator.gemspec
|
369
366
|
homepage: https://github.com/inertia186/radiator
|
370
367
|
licenses:
|