crea-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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