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