hive-ruby 1.0.0.pre.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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +54 -0
  3. data/CONTRIBUTING.md +79 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +91 -0
  6. data/LICENSE +21 -0
  7. data/README.md +248 -0
  8. data/Rakefile +358 -0
  9. data/gource.sh +6 -0
  10. data/hive-ruby.gemspec +40 -0
  11. data/images/Anthony Martin.png +0 -0
  12. data/lib/hive.rb +89 -0
  13. data/lib/hive/api.rb +223 -0
  14. data/lib/hive/base_error.rb +218 -0
  15. data/lib/hive/block_api.rb +78 -0
  16. data/lib/hive/bridge.rb +12 -0
  17. data/lib/hive/broadcast.rb +1334 -0
  18. data/lib/hive/chain_config.rb +34 -0
  19. data/lib/hive/fallback.rb +287 -0
  20. data/lib/hive/formatter.rb +14 -0
  21. data/lib/hive/jsonrpc.rb +112 -0
  22. data/lib/hive/marshal.rb +231 -0
  23. data/lib/hive/mixins/jsonable.rb +37 -0
  24. data/lib/hive/mixins/retriable.rb +58 -0
  25. data/lib/hive/mixins/serializable.rb +45 -0
  26. data/lib/hive/operation.rb +141 -0
  27. data/lib/hive/operation/account_create.rb +10 -0
  28. data/lib/hive/operation/account_create_with_delegation.rb +12 -0
  29. data/lib/hive/operation/account_update.rb +8 -0
  30. data/lib/hive/operation/account_witness_proxy.rb +4 -0
  31. data/lib/hive/operation/account_witness_vote.rb +5 -0
  32. data/lib/hive/operation/cancel_transfer_from_savings.rb +4 -0
  33. data/lib/hive/operation/challenge_authority.rb +5 -0
  34. data/lib/hive/operation/change_recovery_account.rb +5 -0
  35. data/lib/hive/operation/claim_account.rb +5 -0
  36. data/lib/hive/operation/claim_reward_balance.rb +6 -0
  37. data/lib/hive/operation/comment.rb +9 -0
  38. data/lib/hive/operation/comment_options.rb +10 -0
  39. data/lib/hive/operation/convert.rb +5 -0
  40. data/lib/hive/operation/create_claimed_account.rb +10 -0
  41. data/lib/hive/operation/custom.rb +5 -0
  42. data/lib/hive/operation/custom_binary.rb +8 -0
  43. data/lib/hive/operation/custom_json.rb +6 -0
  44. data/lib/hive/operation/decline_voting_rights.rb +4 -0
  45. data/lib/hive/operation/delegate_vesting_shares.rb +5 -0
  46. data/lib/hive/operation/delete_comment.rb +4 -0
  47. data/lib/hive/operation/escrow_approve.rb +8 -0
  48. data/lib/hive/operation/escrow_dispute.rb +7 -0
  49. data/lib/hive/operation/escrow_release.rb +10 -0
  50. data/lib/hive/operation/escrow_transfer.rb +12 -0
  51. data/lib/hive/operation/feed_publish.rb +4 -0
  52. data/lib/hive/operation/limit_order_cancel.rb +4 -0
  53. data/lib/hive/operation/limit_order_create.rb +8 -0
  54. data/lib/hive/operation/limit_order_create2.rb +8 -0
  55. data/lib/hive/operation/prove_authority.rb +4 -0
  56. data/lib/hive/operation/recover_account.rb +6 -0
  57. data/lib/hive/operation/report_over_production.rb +5 -0
  58. data/lib/hive/operation/request_account_recovery.rb +6 -0
  59. data/lib/hive/operation/reset_account.rb +5 -0
  60. data/lib/hive/operation/set_reset_account.rb +5 -0
  61. data/lib/hive/operation/set_withdraw_vesting_route.rb +6 -0
  62. data/lib/hive/operation/transfer.rb +6 -0
  63. data/lib/hive/operation/transfer_from_savings.rb +7 -0
  64. data/lib/hive/operation/transfer_to_savings.rb +6 -0
  65. data/lib/hive/operation/transfer_to_vesting.rb +5 -0
  66. data/lib/hive/operation/vote.rb +6 -0
  67. data/lib/hive/operation/withdraw_vesting.rb +4 -0
  68. data/lib/hive/operation/witness_set_properties.rb +5 -0
  69. data/lib/hive/operation/witness_update.rb +7 -0
  70. data/lib/hive/rpc/base_client.rb +179 -0
  71. data/lib/hive/rpc/http_client.rb +143 -0
  72. data/lib/hive/rpc/thread_safe_http_client.rb +35 -0
  73. data/lib/hive/stream.rb +385 -0
  74. data/lib/hive/transaction.rb +115 -0
  75. data/lib/hive/transaction_builder.rb +406 -0
  76. data/lib/hive/type/amount.rb +126 -0
  77. data/lib/hive/type/base_type.rb +10 -0
  78. data/lib/hive/utils.rb +17 -0
  79. data/lib/hive/version.rb +4 -0
  80. metadata +502 -0
@@ -0,0 +1,115 @@
1
+ module Hive
2
+ class Transaction
3
+ include JSONable
4
+ include Utils
5
+
6
+ ATTRIBUTES = %i(id ref_block_num ref_block_prefix expiration operations
7
+ extensions signatures)
8
+
9
+ attr_accessor *ATTRIBUTES
10
+
11
+ def initialize(options = {})
12
+ if !!(hex = options.delete(:hex))
13
+ marshal = Marshal.new(hex: hex)
14
+ marshal.transaction(trx: self)
15
+ end
16
+
17
+ options.each do |k, v|
18
+ raise Hive::ArgumentError, "Invalid option specified: #{k}" unless ATTRIBUTES.include?(k.to_sym)
19
+
20
+ send("#{k}=", v)
21
+ end
22
+
23
+ self.operations ||= []
24
+ self.extensions ||= []
25
+ self.signatures ||= []
26
+
27
+ self.expiration = case @expiration
28
+ when String then Time.parse(@expiration + 'Z')
29
+ else; @expiration
30
+ end
31
+ end
32
+
33
+ def inspect
34
+ properties = ATTRIBUTES.map do |prop|
35
+ unless (v = instance_variable_get("@#{prop}")).nil?
36
+ v = if v.respond_to? :strftime
37
+ v.strftime('%Y-%m-%dT%H:%M:%S')
38
+ else
39
+ v
40
+ end
41
+
42
+ "@#{prop}=#{v}"
43
+ end
44
+ end.compact.join(', ')
45
+
46
+ "#<#{self.class.name} [#{properties}]>"
47
+ end
48
+
49
+ def expiration
50
+ if @expiration.respond_to? :strftime
51
+ @expiration.strftime('%Y-%m-%dT%H:%M:%S')
52
+ else
53
+ @expiration
54
+ end
55
+ end
56
+
57
+ def expired?
58
+ @expiration.nil? || @expiration < Time.now
59
+ end
60
+
61
+ def [](key)
62
+ key = key.to_sym
63
+ send(key) if self.class.attributes.include?(key)
64
+ end
65
+
66
+ def []=(key, value)
67
+ key = key.to_sym
68
+ send("#{key}=", value) if self.class.attributes.include?(key)
69
+ end
70
+
71
+ def ==(other_trx)
72
+ return true if self.equal? other_trx
73
+ return false unless self.class == other_trx.class
74
+
75
+ begin
76
+ return false if self[:ref_block_num].to_i != transform_value(other_trx[:ref_block_num]).to_i
77
+ return false if self[:ref_block_prefix].to_i != transform_value(other_trx[:ref_block_prefix]).to_i
78
+ return false if self[:expiration].to_i != other_trx[:expiration].to_i
79
+ return false if self[:operations].size != other_trx[:operations].size
80
+
81
+ op_values = self[:operations].map do |type, value|
82
+ [type.to_s, value.values.map{|v| transform_value(v).gsub(/[^a-zA-Z0-9-]/, '')}]
83
+ end.flatten.sort
84
+
85
+ other_op_values = other_trx[:operations].map do |type, value|
86
+ [type.to_s, value.values.map{|v| transform_value(v).gsub(/[^a-zA-Z0-9-]/, '')}]
87
+ end.flatten.sort
88
+
89
+ # unless op_values == other_op_values
90
+ # require 'pry'; binding.pry
91
+ # end
92
+
93
+ op_values == other_op_values
94
+ rescue => e
95
+ # require 'pry'; binding.pry
96
+
97
+ false
98
+ end
99
+ end
100
+ private
101
+ def transform_value(v)
102
+ case v
103
+ when Array then v.map{|e| transform_value(e)}.to_s
104
+ when Hash
105
+ if !v['tl_in_read'] && !!v['value']
106
+ v['value'].to_s
107
+ else
108
+ v.values.map{|e| transform_value(e)}.to_s
109
+ end
110
+ else
111
+ v.to_s
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,406 @@
1
+ module Hive
2
+ # {TransactionBuilder} can be used to create a transaction that the
3
+ # {NetworkBroadcastApi} can broadcast to the rest of the platform. The main
4
+ # feature of this class is the ability to cryptographically sign the
5
+ # transaction so that it conforms to the consensus rules that are required by
6
+ # the blockchain.
7
+ #
8
+ # wif = '5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC'
9
+ # builder = Hive::TransactionBuilder.new(wif: wif)
10
+ # builder.put(vote: {
11
+ # voter: 'alice',
12
+ # author: 'bob',
13
+ # permlink: 'my-burgers',
14
+ # weight: 10000
15
+ # })
16
+ #
17
+ # trx = builder.transaction
18
+ # network_broadcast_api = Hive::CondenserApi.new
19
+ # network_broadcast_api.broadcast_transaction_synchronous(trx: trx)
20
+ #
21
+ #
22
+ # The `wif` value may also be an array, when signing with multiple signatures
23
+ # (multisig).
24
+ class TransactionBuilder
25
+ include Retriable
26
+ include ChainConfig
27
+ include Utils
28
+
29
+ attr_accessor :app_base, :database_api, :block_api, :expiration, :operations
30
+ attr_writer :wif
31
+ attr_reader :signed, :testnet, :force_serialize
32
+
33
+ alias app_base? app_base
34
+ alias testnet? testnet
35
+ alias force_serialize? force_serialize
36
+
37
+ def initialize(options = {})
38
+ @app_base = !!options[:app_base] # default false
39
+ @database_api = options[:database_api]
40
+ @block_api = options[:block_api]
41
+
42
+ if app_base?
43
+ @database_api ||= Hive::DatabaseApi.new(options)
44
+ @block_api ||= Hive::BlockApi.new(options)
45
+ else
46
+ @database_api ||= Hive::CondenserApi.new(options)
47
+ @block_api ||= Hive::CondenserApi.new(options)
48
+ end
49
+
50
+ @wif = [options[:wif]].flatten
51
+ @signed = false
52
+ @testnet = !!options[:testnet]
53
+ @force_serialize = !!options[:force_serialize]
54
+
55
+ if !!(trx = options[:trx])
56
+ trx = case trx
57
+ when String then JSON[trx]
58
+ else; trx
59
+ end
60
+
61
+ @trx = Transaction.new(trx)
62
+ end
63
+
64
+ @trx ||= Transaction.new
65
+ @chain = options[:chain] || :hive
66
+ @error_pipe = options[:error_pipe] || STDERR
67
+ @chain_id = options[:chain_id] || ENV['HIVE_CHAIN_ID']
68
+
69
+ @network_chain_id ||= case @chain
70
+ when :hive then @database_api.get_config{|config| config['HIVE_CHAIN_ID']} rescue NETWORKS_HIVE_CHAIN_ID
71
+ when :test then @database_api.get_config{|config| config['HIVE_CHAIN_ID']} rescue NETWORKS_TEST_CHAIN_ID
72
+ else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
73
+ end
74
+
75
+ @chain_id ||= @network_chain_id
76
+
77
+ if testnet? && (@chain_id == NETWORKS_HIVE_CHAIN_ID || @chain_id == NETWORKS_HIVE_LEGACY_CHAIN_ID)
78
+ raise UnsupportedChainError, "Unsupported testnet chain id: #{@chain_id}"
79
+ end
80
+
81
+ if @chain_id != @network_chain_id
82
+ raise UnsupportedChainError, "Unsupported chain id (expected: #{@chain_id}, network was: #{@network_chain_id})"
83
+ end
84
+ end
85
+
86
+ def inspect
87
+ properties = %w(trx).map do |prop|
88
+ if !!(v = instance_variable_get("@#{prop}"))
89
+ "@#{prop}=#{v.inspect}"
90
+ end
91
+ end.compact.join(', ')
92
+
93
+ "#<#{self.class.name} [#{properties}]>"
94
+ end
95
+
96
+ def reset
97
+ @trx = Transaction.new
98
+ @signed = false
99
+
100
+ self
101
+ end
102
+
103
+ # If the transaction can be prepared, this method will do so and set the
104
+ # expiration. Once the expiration is set, it will not re-prepare. If you
105
+ # call {#put}, the expiration is set {::Nil} so that it can be re-prepared.
106
+ #
107
+ # Usually, this method is called automatically by {#put} and/or {#transaction}.
108
+ #
109
+ # @return {TransactionBuilder}
110
+ def prepare
111
+ if @trx.expired?
112
+ catch :prepare_header do; begin
113
+ @database_api.get_dynamic_global_properties do |properties|
114
+ block_number = properties.last_irreversible_block_num
115
+ block_header_args = if app_base?
116
+ {block_num: block_number}
117
+ else
118
+ block_number
119
+ end
120
+
121
+ @block_api.get_block_header(block_header_args) do |result|
122
+ header = if app_base?
123
+ result.header
124
+ else
125
+ result
126
+ end
127
+
128
+ @trx.ref_block_num = (block_number - 1) & 0xFFFF
129
+ @trx.ref_block_prefix = unhexlify(header.previous[8..-1]).unpack('V*')[0]
130
+ @trx.expiration ||= (Time.parse(properties.time + 'Z') + EXPIRE_IN_SECS).utc
131
+ end
132
+ end
133
+ rescue => e
134
+ if can_retry? e
135
+ @error_pipe.puts "#{e} ... retrying."
136
+ throw :prepare_header
137
+ else
138
+ raise e
139
+ end
140
+ end; end
141
+ end
142
+
143
+ self
144
+ end
145
+
146
+ # Sets operations all at once, then prepares.
147
+ def operations=(operations)
148
+ @trx.operations = operations.map{ |op| normalize_operation(op) }
149
+ prepare
150
+ @trx.operations
151
+ end
152
+
153
+ # A quick and flexible way to append a new operation to the transaction.
154
+ # This method uses ducktyping to figure out how to form the operation.
155
+ #
156
+ # There are three main ways you can call this method. These assume that
157
+ # `op_type` is a {::Symbol} (or {::String}) representing the type of operation and `op` is the
158
+ # operation {::Hash}.
159
+ #
160
+ # put(op_type, op)
161
+ #
162
+ # ... or ...
163
+ #
164
+ # put(op_type => op)
165
+ #
166
+ # ... or ...
167
+ #
168
+ # put([op_type, op])
169
+ #
170
+ # You can also chain multiple operations:
171
+ #
172
+ # builder = Hive::TransactionBuilder.new
173
+ # builder.put(vote: vote1).put(vote: vote2)
174
+ # @return {TransactionBuilder}
175
+ def put(type, op = nil)
176
+ @trx.expiration = nil
177
+ @trx.operations << normalize_operation(type, op)
178
+ prepare
179
+ self
180
+ end
181
+
182
+ # If all of the required values are set, this returns a fully formed
183
+ # transaction that is ready to broadcast.
184
+ #
185
+ # @return
186
+ # {
187
+ # :ref_block_num => 18912,
188
+ # :ref_block_prefix => 575781536,
189
+ # :expiration => "2018-04-26T15:26:12",
190
+ # :extensions => [],
191
+ # :operations => [[:vote, {
192
+ # :voter => "alice",
193
+ # :author => "bob",
194
+ # :permlink => "my-burgers",
195
+ # :weight => 10000
196
+ # }
197
+ # ]],
198
+ # :signatures => ["1c45b65740b4b2c17c4bcf6bcc3f8d90ddab827d50532729fc3b8f163f2c465a532b0112ae4bf388ccc97b7c2e0bc570caadda78af48cf3c261037e65eefcd941e"]
199
+ # }
200
+ def transaction(options = {prepare: true, sign: true})
201
+ options[:prepare] = true unless options.has_key? :prepare
202
+ options[:sign] = true unless options.has_key? :sign
203
+
204
+ prepare if !!options[:prepare]
205
+
206
+ if !!options[:sign]
207
+ sign
208
+ else
209
+ @trx
210
+ end
211
+ end
212
+
213
+ # Appends to the `signatures` array of the transaction, built from a
214
+ # serialized digest.
215
+ #
216
+ # @return {Hash | TransactionBuilder} The fully signed transaction if a `wif` is provided or the instance of the {TransactionBuilder} if a `wif` has not yet been provided.
217
+ def sign
218
+ return self if @wif.empty?
219
+ return self if @trx.expired?
220
+
221
+ unless @signed
222
+ catch :serialize do; begin
223
+ transaction_hex.tap do |result|
224
+ hex = if app_base?
225
+ result.hex
226
+ else
227
+ result
228
+ end
229
+
230
+ unless force_serialize?
231
+ derrived_trx = Transaction.new(hex: hex)
232
+ derrived_ops = derrived_trx.operations
233
+ derrived_trx.operations = derrived_ops.map do |op|
234
+ op_name = if app_base?
235
+ op[:type].to_sym
236
+ else
237
+ op[:type].to_s.sub(/_operation$/, '').to_sym
238
+ end
239
+
240
+ normalize_operation op_name, JSON[op[:value].to_json]
241
+ end
242
+
243
+ unless @trx == derrived_trx
244
+ if defined? JsonCompare
245
+ raise SerializationMismatchError, JSON.pretty_generate({trx: @trx, derrived_trx: derrived_trx})
246
+ else
247
+ raise SerializationMismatchError
248
+ end
249
+ end
250
+ end
251
+
252
+ hex = hex[0..-4] # drop empty signature array
253
+ @trx.id = Digest::SHA256.hexdigest(unhexlify(hex))[0..39]
254
+
255
+ hex = @chain_id + hex
256
+ digest = unhexlify(hex)
257
+ digest_hex = Digest::SHA256.digest(digest)
258
+ private_keys = @wif.map{ |wif| Bitcoin::Key.from_base58 wif }
259
+ ec = Bitcoin::OpenSSL_EC
260
+ count = 0
261
+
262
+ private_keys.each do |private_key|
263
+ sig = nil
264
+
265
+ loop do
266
+ count += 1
267
+ @error_pipe.puts "#{count} attempts to find canonical signature" if count % 40 == 0
268
+ public_key_hex = private_key.pub
269
+ sig = ec.sign_compact(digest_hex, private_key.priv, public_key_hex, false)
270
+
271
+ next if public_key_hex != ec.recover_compact(digest_hex, sig)
272
+ break if canonical? sig
273
+ end
274
+
275
+ @trx.signatures << hexlify(sig)
276
+ end
277
+
278
+ @signed = true
279
+ end
280
+ rescue => e
281
+ if can_retry? e
282
+ @error_pipe.puts "#{e} ... retrying."
283
+ throw :serialize
284
+ else
285
+ raise e
286
+ end
287
+ end; end
288
+ end
289
+
290
+ @trx
291
+ end
292
+
293
+ def transaction_hex
294
+ trx = transaction(prepare: true, sign: false)
295
+
296
+ transaction_hex_args = if app_base?
297
+ {trx: trx}
298
+ else
299
+ trx
300
+ end
301
+
302
+ @database_api.get_transaction_hex(transaction_hex_args) do |result|
303
+ if app_base?
304
+ result[:hex]
305
+ else
306
+ result
307
+ end
308
+ end
309
+ end
310
+
311
+ # @return [Array] All public keys that could possibly sign for a given transaction.
312
+ def potential_signatures
313
+ potential_signatures_args = if app_base?
314
+ {trx: transaction}
315
+ else
316
+ transaction
317
+ end
318
+
319
+ @database_api.get_potential_signatures(potential_signatures_args) do |result|
320
+ if app_base?
321
+ result[:keys]
322
+ else
323
+ result
324
+ end
325
+ end
326
+ end
327
+
328
+ # This API will take a partially signed transaction and a set of public keys
329
+ # that the owner has the ability to sign for and return the minimal subset
330
+ # of public keys that should add signatures to the transaction.
331
+ #
332
+ # @return [Array] The minimal subset of public keys that should add signatures to the transaction.
333
+ def required_signatures
334
+ required_signatures_args = if app_base?
335
+ {trx: transaction}
336
+ else
337
+ [transaction, []]
338
+ end
339
+
340
+ @database_api.get_required_signatures(*required_signatures_args) do |result|
341
+ if app_base?
342
+ result[:keys]
343
+ else
344
+ result
345
+ end
346
+ end
347
+ end
348
+
349
+ # @return [Boolean] True if the transaction has all of the required signatures.
350
+ def valid?
351
+ verify_authority_args = if app_base?
352
+ {trx: transaction}
353
+ else
354
+ transaction
355
+ end
356
+
357
+ @database_api.verify_authority(verify_authority_args) do |result|
358
+ if app_base?
359
+ result.valid
360
+ else
361
+ result
362
+ end
363
+ end
364
+ end
365
+ private
366
+ # See: https://github.com/openhive-network/hive/pull/2500
367
+ # @private
368
+ def canonical?(sig)
369
+ sig = sig.unpack('C*')
370
+
371
+ !(
372
+ ((sig[0] & 0x80 ) != 0) || ( sig[0] == 0 ) ||
373
+ ((sig[1] & 0x80 ) != 0) ||
374
+ ((sig[32] & 0x80 ) != 0) || ( sig[32] == 0 ) ||
375
+ ((sig[33] & 0x80 ) != 0)
376
+ )
377
+ end
378
+
379
+ def normalize_operation(type, op = nil)
380
+ if app_base?
381
+ case type
382
+ when Symbol, String
383
+ type_value = "#{type}_operation"
384
+ {type: type_value, value: op}
385
+ when Hash
386
+ type_value = "#{type.keys.first}_operation"
387
+ {type: type_value, value: type.values.first}
388
+ when Array
389
+ type_value = "#{type[0]}_operation"
390
+ {type: type_value, value: type[1]}
391
+ else
392
+ raise Hive::ArgumentError, "Don't know what to do with operation type #{type.class}: #{type} (#{op})"
393
+ end
394
+ else
395
+ case type
396
+ when Symbol then [type, op]
397
+ when String then [type.to_sym, op]
398
+ when Hash then [type.keys.first.to_sym, type.values.first]
399
+ when Array then type
400
+ else
401
+ raise Hive::ArgumentError, "Don't know what to do with operation type #{type.class}: #{type} (#{op})"
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end