dpay-ruby 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +54 -0
- data/CONTRIBUTING.md +79 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +73 -0
- data/LICENSE +22 -0
- data/README.md +250 -0
- data/Rakefile +332 -0
- data/dpay-ruby.gemspec +37 -0
- data/gource.sh +6 -0
- data/images/Anthony Martin.png +0 -0
- data/lib/dpay.rb +37 -0
- data/lib/dpay/api.rb +229 -0
- data/lib/dpay/base_error.rb +216 -0
- data/lib/dpay/block_api.rb +78 -0
- data/lib/dpay/broadcast.rb +1171 -0
- data/lib/dpay/chain_config.rb +21 -0
- data/lib/dpay/formatter.rb +14 -0
- data/lib/dpay/jsonrpc.rb +108 -0
- data/lib/dpay/mixins/retriable.rb +58 -0
- data/lib/dpay/rpc/base_client.rb +166 -0
- data/lib/dpay/rpc/http_client.rb +127 -0
- data/lib/dpay/rpc/thread_safe_http_client.rb +35 -0
- data/lib/dpay/stream.rb +377 -0
- data/lib/dpay/transaction_builder.rb +386 -0
- data/lib/dpay/type/amount.rb +101 -0
- data/lib/dpay/type/base_type.rb +10 -0
- data/lib/dpay/utils.rb +17 -0
- data/lib/dpay/version.rb +4 -0
- metadata +392 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rake/testtask'
|
|
3
|
+
require 'yard'
|
|
4
|
+
require 'dpay'
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(test: ['clean:vcr', 'test:threads']) do |t|
|
|
7
|
+
t.libs << 'test'
|
|
8
|
+
t.libs << 'lib'
|
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
|
10
|
+
t.ruby_opts << if ENV['HELL_ENABLED']
|
|
11
|
+
'-W2'
|
|
12
|
+
else
|
|
13
|
+
'-W1'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
namespace :test do
|
|
18
|
+
Rake::TestTask.new(static: 'clean:vcr') do |t|
|
|
19
|
+
t.description = <<-EOD
|
|
20
|
+
Run static tests, which are those that have static request/responses.
|
|
21
|
+
These are tests that are typically read-only and do not require heavy
|
|
22
|
+
matches on the json-rpc request body. Often, the only difference between
|
|
23
|
+
one execution and another is the json-rpc-id.
|
|
24
|
+
EOD
|
|
25
|
+
t.libs << 'test'
|
|
26
|
+
t.libs << 'lib'
|
|
27
|
+
t.test_files = [
|
|
28
|
+
'test/dpay/account_by_key_api_test.rb',
|
|
29
|
+
'test/dpay/account_history_api_test.rb',
|
|
30
|
+
'test/dpay/block_api_test.rb',
|
|
31
|
+
'test/dpay/database_api_test.rb',
|
|
32
|
+
'test/dpay/follow_api_test.rb',
|
|
33
|
+
'test/dpay/jsonrpc_test.rb',
|
|
34
|
+
'test/dpay/market_history_api_test.rb',
|
|
35
|
+
'test/dpay/tags_api_test.rb',
|
|
36
|
+
'test/dpay/witness_api_test.rb'
|
|
37
|
+
]
|
|
38
|
+
t.ruby_opts << if ENV['HELL_ENABLED']
|
|
39
|
+
'-W2'
|
|
40
|
+
else
|
|
41
|
+
'-W1'
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Rake::TestTask.new(broadcast: 'clean:vcr') do |t|
|
|
46
|
+
t.description = <<-EOD
|
|
47
|
+
Run broadcast tests, which are those that only use network_broadcast_api
|
|
48
|
+
and/or database_api.verify_authority (pretend: true).
|
|
49
|
+
EOD
|
|
50
|
+
t.libs << 'test'
|
|
51
|
+
t.libs << 'lib'
|
|
52
|
+
t.test_files = [
|
|
53
|
+
'test/dpay/broadcast_test.rb',
|
|
54
|
+
'test/dpay/transaction_builder_test.rb'
|
|
55
|
+
]
|
|
56
|
+
t.ruby_opts << if ENV['HELL_ENABLED']
|
|
57
|
+
'-W2'
|
|
58
|
+
else
|
|
59
|
+
'-W1'
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Rake::TestTask.new(testnet: 'clean:vcr') do |t|
|
|
64
|
+
t.description = <<-EOD
|
|
65
|
+
Run testnet tests, which are those that use network_broadcast_api to do
|
|
66
|
+
actual broadcast operations, on a specified (or default) testnet.
|
|
67
|
+
EOD
|
|
68
|
+
t.libs << 'test'
|
|
69
|
+
t.libs << 'lib'
|
|
70
|
+
t.test_files = [
|
|
71
|
+
'test/dpay/testnet_test.rb'
|
|
72
|
+
]
|
|
73
|
+
t.ruby_opts << if ENV['HELL_ENABLED']
|
|
74
|
+
'-W2'
|
|
75
|
+
else
|
|
76
|
+
'-W1'
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc 'Tests the API using multiple threads.'
|
|
81
|
+
task :threads do
|
|
82
|
+
next if !!ENV['TEST']
|
|
83
|
+
|
|
84
|
+
threads = []
|
|
85
|
+
api = DPay::Api.new(url: ENV['TEST_NODE'])
|
|
86
|
+
database_api = DPay::DatabaseApi.new(url: ENV['TEST_NODE'])
|
|
87
|
+
witnesses = {}
|
|
88
|
+
keys = %i(created url total_missed props running_version
|
|
89
|
+
hardfork_version_vote hardfork_time_vote)
|
|
90
|
+
|
|
91
|
+
if defined? Thread.report_on_exception
|
|
92
|
+
Thread.report_on_exception = true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
database_api.get_active_witnesses do |result|
|
|
96
|
+
print "Found #{result.witnesses.size} witnesses ..."
|
|
97
|
+
|
|
98
|
+
result.witnesses.each do |witness_name|
|
|
99
|
+
threads << Thread.new do
|
|
100
|
+
api.get_witness_by_account(witness_name) do |witness|
|
|
101
|
+
witnesses[witness.owner] = witness.map do |k, v|
|
|
102
|
+
[k, v] if keys.include? k.to_sym
|
|
103
|
+
end.compact.to_h
|
|
104
|
+
|
|
105
|
+
bbd_exchange_rate = witness[:bbd_exchange_rate]
|
|
106
|
+
base = bbd_exchange_rate[:base].to_f
|
|
107
|
+
|
|
108
|
+
if (quote = bbd_exchange_rate[:quote].to_f) > 0
|
|
109
|
+
rate = (base / quote).round(3)
|
|
110
|
+
witnesses[witness.owner][:bbd_exchange_rate] = rate
|
|
111
|
+
else
|
|
112
|
+
witnesses[witness.owner][:bbd_exchange_rate] = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
last_bbd_exchange_update = witness[:last_bbd_exchange_update]
|
|
116
|
+
last_bbd_exchange_update = Time.parse(last_bbd_exchange_update + 'Z')
|
|
117
|
+
last_bbd_exchange_elapsed = '%.2f hours ago' % ((Time.now.utc - last_bbd_exchange_update) / 60)
|
|
118
|
+
witnesses[witness.owner][:last_bbd_exchange_elapsed] = last_bbd_exchange_elapsed
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
threads.each do |thread|
|
|
125
|
+
print '.'
|
|
126
|
+
thread.join
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
puts ' done!'
|
|
130
|
+
|
|
131
|
+
if threads.size != witnesses.size
|
|
132
|
+
puts "Bug: expected #{threads.size} witnesses, only found #{witnesses.size}."
|
|
133
|
+
else
|
|
134
|
+
puts JSON.pretty_generate witnesses rescue puts witnesses
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
namespace :stream do
|
|
140
|
+
desc 'Test the ability to stream a block range.'
|
|
141
|
+
task :block_range, [:mode, :at_block_num] do |t, args|
|
|
142
|
+
mode = (args[:mode] || 'irreversible').to_sym
|
|
143
|
+
first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
|
|
144
|
+
stream = DPay::Stream.new(url: ENV['TEST_NODE'], mode: mode)
|
|
145
|
+
api = DPay::Api.new(url: ENV['TEST_NODE'])
|
|
146
|
+
last_block_num = nil
|
|
147
|
+
last_timestamp = nil
|
|
148
|
+
range_complete = false
|
|
149
|
+
|
|
150
|
+
api.get_dynamic_global_properties do |properties|
|
|
151
|
+
current_block_num = if mode == :head
|
|
152
|
+
properties.head_block_number
|
|
153
|
+
else
|
|
154
|
+
properties.last_irreversible_block_num
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# First pass replays latest a random number of blocks to test chunking.
|
|
158
|
+
first_block_num ||= current_block_num - (rand * 200).to_i
|
|
159
|
+
|
|
160
|
+
range = first_block_num..current_block_num
|
|
161
|
+
puts "Initial block range: #{range.size}"
|
|
162
|
+
|
|
163
|
+
stream.blocks(at_block_num: range.first) do |block, block_num|
|
|
164
|
+
current_timestamp = Time.parse(block.timestamp + 'Z')
|
|
165
|
+
|
|
166
|
+
if !range_complete && block_num > range.last
|
|
167
|
+
puts 'Done with initial range.'
|
|
168
|
+
range_complete = true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if !!last_timestamp && block_num != last_block_num + 1
|
|
172
|
+
puts "Bug: Last block number was #{last_block_num} then jumped to: #{block_num}"
|
|
173
|
+
exit
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if !!last_timestamp && current_timestamp < last_timestamp
|
|
177
|
+
puts "Bug: Went back in time. Last timestamp was #{last_timestamp}, then jumped back to #{current_timestamp}"
|
|
178
|
+
exit
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
puts "\t#{block_num} Timestamp: #{current_timestamp}, witness: #{block.witness}"
|
|
182
|
+
last_block_num = block_num
|
|
183
|
+
last_timestamp = current_timestamp
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
desc 'Test the ability to stream a block range of transactions.'
|
|
189
|
+
task :trx_range, [:mode, :at_block_num] do |t, args|
|
|
190
|
+
mode = (args[:mode] || 'irreversible').to_sym
|
|
191
|
+
first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
|
|
192
|
+
stream = DPay::Stream.new(url: ENV['TEST_NODE'], mode: mode)
|
|
193
|
+
api = DPay::Api.new(url: ENV['TEST_NODE'])
|
|
194
|
+
|
|
195
|
+
api.get_dynamic_global_properties do |properties|
|
|
196
|
+
current_block_num = if mode == :head
|
|
197
|
+
properties.head_block_number
|
|
198
|
+
else
|
|
199
|
+
properties.last_irreversible_block_num
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# First pass replays latest a random number of blocks to test chunking.
|
|
203
|
+
first_block_num ||= current_block_num - (rand * 200).to_i
|
|
204
|
+
|
|
205
|
+
stream.transactions(at_block_num: first_block_num) do |trx, trx_id, block_num|
|
|
206
|
+
puts "#{block_num} :: #{trx_id}; ops: #{trx.operations.map(&:type).join(', ')}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
desc 'Test the ability to stream a block range of operations.'
|
|
212
|
+
task :op_range, [:mode, :at_block_num] do |t, args|
|
|
213
|
+
mode = (args[:mode] || 'irreversible').to_sym
|
|
214
|
+
first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
|
|
215
|
+
stream = DPay::Stream.new(url: ENV['TEST_NODE'], mode: mode)
|
|
216
|
+
api = DPay::Api.new(url: ENV['TEST_NODE'])
|
|
217
|
+
|
|
218
|
+
api.get_dynamic_global_properties do |properties|
|
|
219
|
+
current_block_num = if mode == :head
|
|
220
|
+
properties.head_block_number
|
|
221
|
+
else
|
|
222
|
+
properties.last_irreversible_block_num
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# First pass replays latest a random number of blocks to test chunking.
|
|
226
|
+
first_block_num ||= current_block_num - (rand * 200).to_i
|
|
227
|
+
|
|
228
|
+
stream.operations(at_block_num: first_block_num) do |op, trx_id, block_num|
|
|
229
|
+
puts "#{block_num} :: #{trx_id}; op: #{op.type}"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
desc 'Test the ability to stream a block range of virtual operations.'
|
|
235
|
+
task :vop_range, [:mode, :at_block_num] do |t, args|
|
|
236
|
+
mode = (args[:mode] || 'irreversible').to_sym
|
|
237
|
+
first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
|
|
238
|
+
stream = DPay::Stream.new(url: ENV['TEST_NODE'], mode: mode)
|
|
239
|
+
api = DPay::Api.new(url: ENV['TEST_NODE'])
|
|
240
|
+
|
|
241
|
+
api.get_dynamic_global_properties do |properties|
|
|
242
|
+
current_block_num = if mode == :head
|
|
243
|
+
properties.head_block_number
|
|
244
|
+
else
|
|
245
|
+
properties.last_irreversible_block_num
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# First pass replays latest a random number of blocks to test chunking.
|
|
249
|
+
first_block_num ||= current_block_num - (rand * 200).to_i
|
|
250
|
+
|
|
251
|
+
stream.operations(at_block_num: first_block_num, only_virtual: true) do |op, trx_id, block_num|
|
|
252
|
+
puts "#{block_num} :: #{trx_id}; op: #{op.type}"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
desc 'Test the ability to stream a block range of all operations (including virtual).'
|
|
258
|
+
task :all_op_range, [:mode, :at_block_num] do |t, args|
|
|
259
|
+
mode = (args[:mode] || 'irreversible').to_sym
|
|
260
|
+
first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
|
|
261
|
+
stream = DPay::Stream.new(url: ENV['TEST_NODE'], mode: mode)
|
|
262
|
+
api = DPay::Api.new(url: ENV['TEST_NODE'])
|
|
263
|
+
|
|
264
|
+
api.get_dynamic_global_properties do |properties|
|
|
265
|
+
current_block_num = if mode == :head
|
|
266
|
+
properties.head_block_number
|
|
267
|
+
else
|
|
268
|
+
properties.last_irreversible_block_num
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# First pass replays latest a random number of blocks to test chunking.
|
|
272
|
+
first_block_num ||= current_block_num - (rand * 200).to_i
|
|
273
|
+
|
|
274
|
+
stream.operations(at_block_num: first_block_num, include_virtual: true) do |op, trx_id, block_num|
|
|
275
|
+
puts "#{block_num} :: #{trx_id}; op: #{op.type}"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
YARD::Rake::YardocTask.new do |t|
|
|
282
|
+
t.files = ['lib/**/*.rb']
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
task default: :test
|
|
286
|
+
|
|
287
|
+
desc 'Ruby console with dpay already required.'
|
|
288
|
+
task :console do
|
|
289
|
+
exec 'irb -r dpay -I ./lib'
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
namespace :clean do
|
|
293
|
+
desc 'Remove test/fixtures/vcr_cassettes/*.yml so they can be rebuilt fresh.'
|
|
294
|
+
task :vcr do |t|
|
|
295
|
+
cmd = 'echo Cleaned cassettes: $(rm -v test/fixtures/vcr_cassettes/*.yml | wc -l)'
|
|
296
|
+
system cmd
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
namespace :show do
|
|
301
|
+
desc 'Shows known API names.'
|
|
302
|
+
task :apis do
|
|
303
|
+
url = ENV['URL']
|
|
304
|
+
jsonrpc = DPay::Jsonrpc.new(url: url)
|
|
305
|
+
api_methods = jsonrpc.get_api_methods
|
|
306
|
+
puts api_methods.keys
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
desc 'Shows known method names for specified API.'
|
|
310
|
+
task :methods, [:api] do |t, args|
|
|
311
|
+
url = ENV['URL']
|
|
312
|
+
jsonrpc = DPay::Jsonrpc.new(url: url)
|
|
313
|
+
api_methods = jsonrpc.get_api_methods
|
|
314
|
+
api_methods[args[:api]].each do |method|
|
|
315
|
+
jsonrpc.get_signature(method: "#{args[:api]}.#{method}") do |signature|
|
|
316
|
+
print "#{method} "
|
|
317
|
+
params = signature.args.map do |k, v|
|
|
318
|
+
if v =~ /\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[0-1])T(2[0-3]|[01]\d):[0-5]\d:[0-5]\d/
|
|
319
|
+
"#{k}: Time"
|
|
320
|
+
elsif v.class == Hashie::Array
|
|
321
|
+
"#{k}: []"
|
|
322
|
+
elsif v.class == Hashie::Mash
|
|
323
|
+
"#{k}: {}"
|
|
324
|
+
else
|
|
325
|
+
"#{k}: #{v.class}"
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
puts params.join(', ')
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
data/dpay-ruby.gemspec
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'dpay/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'dpay-ruby'
|
|
8
|
+
spec.version = DPay::VERSION
|
|
9
|
+
spec.authors = ['dPay, DAO']
|
|
10
|
+
spec.email = ['labs@dpays.io']
|
|
11
|
+
|
|
12
|
+
spec.summary = %q{dPay Ruby Client}
|
|
13
|
+
spec.description = %q{Client for accessing the dPay blockchain.}
|
|
14
|
+
spec.homepage = 'https://github.com/dpays/dpay-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', '~> 1.16', '>= 1.16.1'
|
|
21
|
+
spec.add_development_dependency 'rake', '~> 12.3', '>= 12.3.0'
|
|
22
|
+
spec.add_development_dependency 'minitest', '~> 5.10', '>= 5.10.3'
|
|
23
|
+
spec.add_development_dependency 'minitest-line', '~> 0.6', '>= 0.6.4'
|
|
24
|
+
spec.add_development_dependency 'minitest-proveit', '~> 1.0', '>= 1.0.0'
|
|
25
|
+
spec.add_development_dependency 'webmock', '~> 3.3', '>= 3.3.0'
|
|
26
|
+
spec.add_development_dependency 'simplecov', '~> 0.15', '>= 0.15.1'
|
|
27
|
+
spec.add_development_dependency 'vcr', '~> 4.0', '>= 4.0.0'
|
|
28
|
+
spec.add_development_dependency 'yard', '~> 0.9', '>= 0.9.12'
|
|
29
|
+
spec.add_development_dependency 'pry', '~> 0.11', '>= 0.11.3'
|
|
30
|
+
spec.add_development_dependency 'awesome_print', '~> 1.8', '>= 1.8.0'
|
|
31
|
+
|
|
32
|
+
spec.add_dependency 'json', '~> 2.1', '>= 2.1.0'
|
|
33
|
+
spec.add_dependency 'logging', '~> 2.2', '>= 2.2.0'
|
|
34
|
+
spec.add_dependency 'hashie', '~> 3.5', '>= 3.5.7'
|
|
35
|
+
spec.add_dependency 'bitcoin-ruby', '~> 0.0', '>= 0.0.18'
|
|
36
|
+
spec.add_dependency 'ffi', '~> 1.9', '>= 1.9.23'
|
|
37
|
+
end
|
data/gource.sh
ADDED
|
Binary file
|
data/lib/dpay.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
require 'json' unless defined?(JSON)
|
|
3
|
+
require 'net/https'
|
|
4
|
+
|
|
5
|
+
require 'hashie'
|
|
6
|
+
require 'dpay/version'
|
|
7
|
+
require 'dpay/utils'
|
|
8
|
+
require 'dpay/base_error'
|
|
9
|
+
require 'dpay/mixins/retriable'
|
|
10
|
+
require 'dpay/chain_config'
|
|
11
|
+
require 'dpay/type/base_type'
|
|
12
|
+
require 'dpay/type/amount'
|
|
13
|
+
require 'dpay/transaction_builder'
|
|
14
|
+
require 'dpay/rpc/base_client'
|
|
15
|
+
require 'dpay/rpc/http_client'
|
|
16
|
+
require 'dpay/rpc/thread_safe_http_client'
|
|
17
|
+
require 'dpay/api'
|
|
18
|
+
require 'dpay/jsonrpc'
|
|
19
|
+
require 'dpay/block_api'
|
|
20
|
+
require 'dpay/formatter'
|
|
21
|
+
require 'dpay/broadcast'
|
|
22
|
+
require 'dpay/stream'
|
|
23
|
+
|
|
24
|
+
module DPay
|
|
25
|
+
def self.api_classes
|
|
26
|
+
@api_classes ||= {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.const_missing(api_name)
|
|
30
|
+
api = api_classes[api_name]
|
|
31
|
+
api ||= Api.clone(freeze: true) rescue Api.clone
|
|
32
|
+
api.api_name = api_name
|
|
33
|
+
api_classes[api_name] = api
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Hashie.logger = Logger.new(ENV['HASHIE_LOGGER'])
|
data/lib/dpay/api.rb
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
module DPay
|
|
2
|
+
# This ruby API works with
|
|
3
|
+
# {https://github.com/dpays/dpay/releases dpayd-0.19.10} and other AppBase
|
|
4
|
+
# compatible upstreams. To access different API namespaces, use the
|
|
5
|
+
# following:
|
|
6
|
+
#
|
|
7
|
+
# api = DPay::Api.new
|
|
8
|
+
# api.get_dynamic_global_properties
|
|
9
|
+
#
|
|
10
|
+
# The above example will make an instance that can access the
|
|
11
|
+
# {https://developers.dpays.io/apidefinitions/condenser-api condenser_api}
|
|
12
|
+
# namespace. Alternatively, you may also create a direct instances with its
|
|
13
|
+
# full name, if you prefer:
|
|
14
|
+
#
|
|
15
|
+
# api = DPay::CondenserApi.new
|
|
16
|
+
# api.get_dynamic_global_properties
|
|
17
|
+
#
|
|
18
|
+
# If you know the name of another API that is supported by the remote node,
|
|
19
|
+
# you can create an instance to that instead, for example:
|
|
20
|
+
#
|
|
21
|
+
# api = DPay::MarketHistoryApi.new
|
|
22
|
+
# api.get_volume
|
|
23
|
+
#
|
|
24
|
+
# All known API by namespace:
|
|
25
|
+
#
|
|
26
|
+
# * {AccountByKeyApi}
|
|
27
|
+
# * {AccountHistoryApi}
|
|
28
|
+
# * {BlockApi}
|
|
29
|
+
# * {DatabaseApi}
|
|
30
|
+
# * {FollowApi}
|
|
31
|
+
# * {Jsonrpc}
|
|
32
|
+
# * {MarketHistoryApi}
|
|
33
|
+
# * {NetworkBroadcastApi}
|
|
34
|
+
# * {TagsApi}
|
|
35
|
+
# * {WitnessApi}
|
|
36
|
+
#
|
|
37
|
+
# Also see: {https://developers.dpays.io/apidefinitions/ Complete API Definitions}
|
|
38
|
+
class Api
|
|
39
|
+
attr_accessor :chain, :methods, :rpc_client
|
|
40
|
+
|
|
41
|
+
# Use this for debugging naive thread handler.
|
|
42
|
+
# DEFAULT_RPC_CLIENT_CLASS = RPC::HttpClient
|
|
43
|
+
DEFAULT_RPC_CLIENT_CLASS = RPC::ThreadSafeHttpClient
|
|
44
|
+
|
|
45
|
+
def self.api_name=(api_name)
|
|
46
|
+
@api_name = api_name.to_s.
|
|
47
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
|
48
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
|
49
|
+
tr('-', '_').downcase.to_sym
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.api_name
|
|
53
|
+
@api_name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.api_class_name
|
|
57
|
+
@api_name.to_s.split('_').map(&:capitalize).join
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.jsonrpc=(jsonrpc, url = nil)
|
|
61
|
+
@jsonrpc ||= {}
|
|
62
|
+
@jsonrpc[url || jsonrpc.rpc_client.uri.to_s] = jsonrpc
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.jsonrpc(url = nil)
|
|
66
|
+
if @jsonrpc.size < 2 && url.nil?
|
|
67
|
+
@jsonrpc.values.first
|
|
68
|
+
else
|
|
69
|
+
@jsonrpc[url]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Override this if you want to just use your own client. Otherwise, inject
|
|
74
|
+
# the default using:
|
|
75
|
+
#
|
|
76
|
+
# DPay::Api.register default_rpc_client_class: MyClient
|
|
77
|
+
def self.default_rpc_client_class
|
|
78
|
+
if !!@injected_dependencies && !!@injected_dependencies[:default_rpc_client_class]
|
|
79
|
+
@injected_dependencies[:default_rpc_client_class]
|
|
80
|
+
else
|
|
81
|
+
DEFAULT_RPC_CLIENT_CLASS
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Used for dependency injection. Currently, the only key supported is:
|
|
86
|
+
#
|
|
87
|
+
# `default_rpc_client_class`
|
|
88
|
+
def self.register(register)
|
|
89
|
+
@injected_dependencies ||= {}
|
|
90
|
+
@injected_dependencies = @injected_dependencies.merge register
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def initialize(options = {})
|
|
94
|
+
@chain = options[:chain] || :dpay
|
|
95
|
+
@error_pipe = options[:error_pipe] || STDERR
|
|
96
|
+
@api_name = self.class.api_name ||= :condenser_api
|
|
97
|
+
|
|
98
|
+
@rpc_client = if !!options[:rpc_client]
|
|
99
|
+
options[:rpc_client]
|
|
100
|
+
else
|
|
101
|
+
rpc_client_class = self.class.default_rpc_client_class
|
|
102
|
+
rpc_client_class.new(options.merge(api_name: @api_name))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if @api_name == :jsonrpc
|
|
106
|
+
Api::jsonrpc = self
|
|
107
|
+
else
|
|
108
|
+
# Note, we have to wait until initialize to check this because we don't
|
|
109
|
+
# have access to instance options until now.
|
|
110
|
+
|
|
111
|
+
Api::jsonrpc = Jsonrpc.new(options)
|
|
112
|
+
@methods = Api::jsonrpc(rpc_client.uri.to_s).get_api_methods
|
|
113
|
+
|
|
114
|
+
unless !!@methods[@api_name]
|
|
115
|
+
raise UnknownApiError, "#{@api_name} (known APIs: #{@methods.keys.join(' ')})"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@methods = @methods[@api_name]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@try_count = 0
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def inspect
|
|
125
|
+
properties = %w(chain methods).map do |prop|
|
|
126
|
+
if !!(v = instance_variable_get("@#{prop}"))
|
|
127
|
+
case v
|
|
128
|
+
when Array then "@#{prop}=<#{v.size} #{v.size == 1 ? 'element' : 'elements'}>"
|
|
129
|
+
else; "@#{prop}=#{v}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end.compact.join(', ')
|
|
133
|
+
|
|
134
|
+
"#<#{self.class.api_class_name} [#{properties}]>"
|
|
135
|
+
end
|
|
136
|
+
private
|
|
137
|
+
# @private
|
|
138
|
+
def args_keys_to_s(rpc_method_name)
|
|
139
|
+
args = signature(rpc_method_name).args
|
|
140
|
+
args_keys = JSON[args.to_json]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @private
|
|
144
|
+
def signature(rpc_method_name)
|
|
145
|
+
url = rpc_client.uri.to_s
|
|
146
|
+
|
|
147
|
+
@@signatures ||= {}
|
|
148
|
+
@@signatures[url] ||= {}
|
|
149
|
+
@@signatures[url][rpc_method_name] ||= Api::jsonrpc(url).get_signature(method: rpc_method_name).result
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @private
|
|
153
|
+
def raise_error_response(rpc_method_name, rpc_args, response)
|
|
154
|
+
raise UnknownError, "#{rpc_method_name}: #{response}" if response.error.nil?
|
|
155
|
+
|
|
156
|
+
error = response.error
|
|
157
|
+
|
|
158
|
+
if error.message == 'Invalid Request'
|
|
159
|
+
raise DPay::ArgumentError, "Unexpected arguments: #{rpc_args.inspect}. Expected: #{rpc_method_name} (#{args_keys_to_s(rpc_method_name)})"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
BaseError.build_error(error, rpc_method_name)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# @private
|
|
166
|
+
def respond_to_missing?(m, include_private = false)
|
|
167
|
+
methods.nil? ? false : methods.include?(m.to_sym)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @private
|
|
171
|
+
def method_missing(m, *args, &block)
|
|
172
|
+
super unless respond_to_missing?(m)
|
|
173
|
+
|
|
174
|
+
rpc_method_name = "#{@api_name}.#{m}"
|
|
175
|
+
rpc_args = case @api_name
|
|
176
|
+
when :condenser_api then args
|
|
177
|
+
when :jsonrpc then args.first
|
|
178
|
+
else
|
|
179
|
+
expected_args = signature(rpc_method_name).args || []
|
|
180
|
+
expected_args_key_string = if expected_args.size > 0
|
|
181
|
+
" (#{args_keys_to_s(rpc_method_name)})"
|
|
182
|
+
end
|
|
183
|
+
expected_args_size = expected_args.size
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
args = args.first.to_h
|
|
187
|
+
args_size = args.size
|
|
188
|
+
|
|
189
|
+
# Some argument are optional, but if the arguments passed are greater
|
|
190
|
+
# than the expected arguments size, we can warn.
|
|
191
|
+
if args_size > expected_args_size
|
|
192
|
+
@error_pipe.puts "Warning #{rpc_method_name} expects arguments: #{expected_args_size}, got: #{args_size}"
|
|
193
|
+
end
|
|
194
|
+
rescue NoMethodError => e
|
|
195
|
+
error = DPay::ArgumentError.new("#{rpc_method_name} expects arguments: #{expected_args_size}", e)
|
|
196
|
+
raise error
|
|
197
|
+
rescue => e
|
|
198
|
+
raise UnknownError.new("#{rpc_method_name} unknown error.", e)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
args
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
response = rpc_client.rpc_execute(@api_name, m, rpc_args)
|
|
205
|
+
|
|
206
|
+
if defined?(response.error) && !!response.error
|
|
207
|
+
if !!response.error.message
|
|
208
|
+
raise_error_response rpc_method_name, rpc_args, response
|
|
209
|
+
else
|
|
210
|
+
raise DPay::ArgumentError, response.error.inspect
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if !!block
|
|
215
|
+
case response
|
|
216
|
+
when Hashie::Mash then yield response.result, response.error, response.id
|
|
217
|
+
when Hashie::Array
|
|
218
|
+
response.each do |r|
|
|
219
|
+
r = Hashie::Mash.new(r)
|
|
220
|
+
yield r.result, r.error, r.id
|
|
221
|
+
end
|
|
222
|
+
else; yield response
|
|
223
|
+
end
|
|
224
|
+
else
|
|
225
|
+
return response
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|