steem-ruby 0.9.0 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -1
  3. data/README.md +88 -15
  4. data/Rakefile +128 -31
  5. data/lib/steem.rb +49 -0
  6. data/lib/steem/api.rb +39 -37
  7. data/lib/steem/base_error.rb +80 -41
  8. data/lib/steem/block_api.rb +23 -3
  9. data/lib/steem/broadcast.rb +465 -29
  10. data/lib/steem/chain_config.rb +3 -3
  11. data/lib/steem/marshal.rb +231 -0
  12. data/lib/steem/mixins/jsonable.rb +37 -0
  13. data/lib/steem/mixins/retriable.rb +30 -24
  14. data/lib/steem/mixins/serializable.rb +45 -0
  15. data/lib/steem/operation.rb +141 -0
  16. data/lib/steem/operation/account_create.rb +10 -0
  17. data/lib/steem/operation/account_create_with_delegation.rb +12 -0
  18. data/lib/steem/operation/account_update.rb +8 -0
  19. data/lib/steem/operation/account_witness_proxy.rb +4 -0
  20. data/lib/steem/operation/account_witness_vote.rb +5 -0
  21. data/lib/steem/operation/cancel_transfer_from_savings.rb +4 -0
  22. data/lib/steem/operation/challenge_authority.rb +5 -0
  23. data/lib/steem/operation/change_recovery_account.rb +5 -0
  24. data/lib/steem/operation/claim_account.rb +5 -0
  25. data/lib/steem/operation/claim_reward_balance.rb +6 -0
  26. data/lib/steem/operation/comment.rb +9 -0
  27. data/lib/steem/operation/comment_options.rb +10 -0
  28. data/lib/steem/operation/convert.rb +5 -0
  29. data/lib/steem/operation/create_claimed_account.rb +10 -0
  30. data/lib/steem/operation/custom.rb +5 -0
  31. data/lib/steem/operation/custom_binary.rb +8 -0
  32. data/lib/steem/operation/custom_json.rb +6 -0
  33. data/lib/steem/operation/decline_voting_rights.rb +4 -0
  34. data/lib/steem/operation/delegate_vesting_shares.rb +5 -0
  35. data/lib/steem/operation/delete_comment.rb +4 -0
  36. data/lib/steem/operation/escrow_approve.rb +8 -0
  37. data/lib/steem/operation/escrow_dispute.rb +7 -0
  38. data/lib/steem/operation/escrow_release.rb +10 -0
  39. data/lib/steem/operation/escrow_transfer.rb +12 -0
  40. data/lib/steem/operation/feed_publish.rb +4 -0
  41. data/lib/steem/operation/limit_order_cancel.rb +4 -0
  42. data/lib/steem/operation/limit_order_create.rb +8 -0
  43. data/lib/steem/operation/limit_order_create2.rb +8 -0
  44. data/lib/steem/operation/prove_authority.rb +4 -0
  45. data/lib/steem/operation/recover_account.rb +6 -0
  46. data/lib/steem/operation/report_over_production.rb +5 -0
  47. data/lib/steem/operation/request_account_recovery.rb +6 -0
  48. data/lib/steem/operation/reset_account.rb +5 -0
  49. data/lib/steem/operation/set_reset_account.rb +5 -0
  50. data/lib/steem/operation/set_withdraw_vesting_route.rb +6 -0
  51. data/lib/steem/operation/transfer.rb +6 -0
  52. data/lib/steem/operation/transfer_from_savings.rb +7 -0
  53. data/lib/steem/operation/transfer_to_savings.rb +6 -0
  54. data/lib/steem/operation/transfer_to_vesting.rb +5 -0
  55. data/lib/steem/operation/vote.rb +6 -0
  56. data/lib/steem/operation/withdraw_vesting.rb +4 -0
  57. data/lib/steem/operation/witness_set_properties.rb +5 -0
  58. data/lib/steem/operation/witness_update.rb +7 -0
  59. data/lib/steem/rpc/base_client.rb +16 -4
  60. data/lib/steem/rpc/http_client.rb +18 -2
  61. data/lib/steem/stream.rb +385 -0
  62. data/lib/steem/transaction.rb +96 -0
  63. data/lib/steem/transaction_builder.rb +176 -103
  64. data/lib/steem/type/amount.rb +61 -9
  65. data/lib/steem/version.rb +1 -1
  66. data/steem-ruby.gemspec +9 -4
  67. metadata +203 -56
  68. data/Gemfile.lock +0 -73
@@ -3,8 +3,8 @@ module Steem
3
3
  EXPIRE_IN_SECS = 600
4
4
  EXPIRE_IN_SECS_PROPOSAL = 24 * 60 * 60
5
5
 
6
- NETWORKS_STEEM_CHAIN_ID = '0000000000000000000000000000000000000000000000000000000000000000'
7
- NETWORKS_STEEM_ADDRESS_PREFIX = 'STM'
6
+ NETWORKS_STEEM_CHAIN_ID = 'abc93c9021bbd9a8dd21c438ee3c480a661ca1966b5e4e838326dcf42a3dac2d'
7
+ NETWORKS_STEEM_ADDRESS_PREFIX = 'TST'
8
8
  NETWORKS_STEEM_CORE_ASSET = ["0", 3, "@@000000021"] # STEEM
9
9
  NETWORKS_STEEM_DEBT_ASSET = ["0", 3, "@@000000013"] # SBD
10
10
  NETWORKS_STEEM_VEST_ASSET = ["0", 6, "@@000000037"] # VESTS
@@ -24,7 +24,7 @@ module Steem
24
24
  # NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.steemviz.com'
25
25
  # NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.steemgigs.org'
26
26
 
27
- NETWORKS_TEST_CHAIN_ID = '46d82ab7d8db682eb1959aed0ada039a6d49afa1602491f93dde9cac3e8e6c32'
27
+ NETWORKS_TEST_CHAIN_ID = 'abc93c9021bbd9a8dd21c438ee3c480a661ca1966b5e4e838326dcf42a3dac2d'
28
28
  NETWORKS_TEST_ADDRESS_PREFIX = 'TST'
29
29
  NETWORKS_TEST_CORE_ASSET = ["0", 3, "@@000000021"] # TESTS
30
30
  NETWORKS_TEST_DEBT_ASSET = ["0", 3, "@@000000013"] # TBD
@@ -0,0 +1,231 @@
1
+ require 'bindata'
2
+ require 'base58'
3
+
4
+ module Steem
5
+ class Marshal
6
+ include Utils
7
+ include ChainConfig
8
+
9
+ PUBLIC_KEY_DISABLED = '1111111111111111111111111111111114T1Anm'
10
+
11
+ attr_reader :bytes, :cursor
12
+
13
+ def initialize(options = {})
14
+ @bytes = if !!(hex = options[:hex])
15
+ unhexlify hex
16
+ else
17
+ options[:bytes]
18
+ end
19
+
20
+ @chain = options[:chain] || :steem
21
+ @prefix ||= case @chain
22
+ when :steem then NETWORKS_STEEM_ADDRESS_PREFIX
23
+ when :test then NETWORKS_TEST_ADDRESS_PREFIX
24
+ else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
25
+ end
26
+ @cursor = 0
27
+ end
28
+
29
+ def hex
30
+ hexlify bytes
31
+ end
32
+
33
+ def rewind!
34
+ @cursor = 0
35
+ end
36
+
37
+ def step(n = 0)
38
+ @cursor += n
39
+ end
40
+
41
+ def scan(len)
42
+ bytes.slice(@cursor..(@cursor - 1) + len).tap { |_| @cursor += len }
43
+ end
44
+
45
+ def operation_type
46
+ Operation::IDS[unsigned_char]
47
+ end
48
+
49
+ def unsigned_char; BinData::Uint8le.read(scan(1)); end # 8-bit unsigned
50
+ def uint16; BinData::Uint16le.read(scan(2)); end # 16-bit unsigned, VAX (little-endian) byte order
51
+ def uint32; BinData::Uint32le.read(scan(4)); end # 32-bit unsigned, VAX (little-endian) byte order
52
+ def uint64; BinData::Uint64le.read(scan(8)); end # 64-bit unsigned, little-endian
53
+
54
+ def signed_char; BinData::Int8le.read(scan(1)); end # 8-bit signed
55
+ def int16; BinData::Int16le.read(scan(2)); end # 16-bit signed, little-endian
56
+ def int32; BinData::Int32le.read(scan(4)); end # 32-bit signed, little-endian
57
+ def int64; BinData::Int64le.read(scan(8)); end # 64-bit signed, little-endian
58
+
59
+ def boolean; scan(1) == "\x01"; end
60
+
61
+ def varint
62
+ shift = 0
63
+ result = 0
64
+ bytes = []
65
+
66
+ while (n = unsigned_char) >> 7 == 1
67
+ bytes << n
68
+ end
69
+
70
+ bytes << n
71
+
72
+ bytes.each do |b|
73
+ result += ((b & 0x7f) << shift)
74
+ break unless (b & 0x80)
75
+ shift += 7
76
+ end
77
+
78
+ result
79
+ end
80
+
81
+ def string(len = nil); scan(len || varint); end
82
+
83
+ def raw_bytes(len = nil); scan(len || varint).force_encoding('BINARY'); end
84
+
85
+ def point_in_time
86
+ if (time = uint32) == 2**32-1
87
+ Time.at -1
88
+ else
89
+ Time.at time
90
+ end.utc
91
+ end
92
+
93
+ def public_key(prefix = @prefix)
94
+ raw_public_key = raw_bytes(33)
95
+ checksum = OpenSSL::Digest::RIPEMD160.digest(raw_public_key)
96
+ key = Base58.binary_to_base58(raw_public_key + checksum.slice(0, 4), :bitcoin)
97
+
98
+ prefix + key unless key == PUBLIC_KEY_DISABLED
99
+ end
100
+
101
+ def amount
102
+ amount = uint64.to_f
103
+ precision = signed_char
104
+ asset = scan(7).strip
105
+
106
+ amount = "%.#{precision}f #{asset}" % (amount / 10 ** precision)
107
+
108
+ Steem::Type::Amount.new(amount)
109
+ end
110
+
111
+ def price
112
+ {base: amount, quote: amount}
113
+ end
114
+
115
+ def authority(options = {optional: false})
116
+ return if !!options[:optional] && unsigned_char == 0
117
+
118
+ {
119
+ weight_threshold: uint32,
120
+ account_auths: varint.times.map { [string, uint16] },
121
+ key_auths: varint.times.map { [public_key, uint16] }
122
+ }
123
+ end
124
+
125
+ def optional_authority
126
+ authority(optional: true)
127
+ end
128
+
129
+ def comment_options_extensions
130
+ if scan(1) == "\x01"
131
+ beneficiaries
132
+ else
133
+ []
134
+ end
135
+ end
136
+
137
+ def beneficiaries
138
+ if scan(1) == "\x00"
139
+ varint.times.map {{account: string, weight: uint16}}
140
+ end
141
+ end
142
+
143
+ def chain_properties
144
+ {
145
+ account_creation_fee: amount,
146
+ maximum_block_size: uint32,
147
+ sbd_interest_rate: uint16
148
+ }
149
+ end
150
+
151
+ def required_auths
152
+ varint.times.map { string }
153
+ end
154
+
155
+ def witness_properties
156
+ properties = {}
157
+
158
+ varint.times do
159
+ key = string.to_sym
160
+ properties[key] = case key
161
+ when :account_creation_fee then Steem::Type::Amount.new(string)
162
+ when :account_subsidy_budget then scan(3)
163
+ when :account_subsidy_decay, :maximum_block_size then uint32
164
+ when :url then string
165
+ when :sbd_exchange_rate
166
+ JSON[string].tap do |rate|
167
+ rate["base"] = Steem::Type::Amount.new(rate["base"])
168
+ rate["quote"] = Steem::Type::Amount.new(rate["quote"])
169
+ end
170
+ when :sbd_interest_rate then uint16
171
+ when :key, :new_signing_key then @prefix + scan(50)
172
+ else; raise "Unknown witness property: #{key}"
173
+ end
174
+ end
175
+
176
+ properties
177
+ end
178
+
179
+ def empty_array
180
+ unsigned_char == 0 and [] or raise "Found non-empty array."
181
+ end
182
+
183
+ def transaction(options = {})
184
+ trx = options[:trx] || Transaction.new
185
+
186
+ trx.ref_block_num = uint16
187
+ trx.ref_block_prefix = uint32
188
+ trx.expiration = point_in_time
189
+
190
+ trx.operations = operations
191
+
192
+ trx
193
+ rescue => e
194
+ raise DeserializationError.new("Transaction failed\nOriginal serialized bytes:\n[#{hex[0..(@cursor * 2) - 1]}]#{hex[((@cursor) * 2)..-1]}", e)
195
+ end
196
+
197
+ def operations
198
+ operations_len = signed_char
199
+ operations = []
200
+
201
+ while operations.size < operations_len do
202
+ begin
203
+ type = operation_type
204
+ break if type.nil?
205
+
206
+ op_class_name = type.to_s.sub!(/_operation$/, '')
207
+ op_class_name = "Steem::Operation::" + op_class_name.split('_').map(&:capitalize).join
208
+ op_class = Object::const_get(op_class_name)
209
+ op = op_class.new
210
+
211
+ op_class::serializable_types.each do |k, v|
212
+ begin
213
+ # binding.pry if v == :comment_options_extensions
214
+ op.send("#{k}=", send(v))
215
+ rescue => e
216
+ raise DeserializationError.new("#{type}.#{k} (#{v}) failed", e)
217
+ end
218
+ end
219
+
220
+ operations << {type: type, value: op}
221
+ rescue => e
222
+ raise DeserializationError.new("#{type} failed", e)
223
+ end
224
+ end
225
+
226
+ operations
227
+ rescue => e
228
+ raise DeserializationError.new("Operations failed", e)
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,37 @@
1
+ module Steem
2
+ module JSONable
3
+ module ClassMethods
4
+ attr_accessor :attributes
5
+
6
+ def attr_accessor *attrs
7
+ self.attributes = Array attrs
8
+
9
+ super
10
+ end
11
+ end
12
+
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ def as_json options = {}
18
+ serialized = Hash.new
19
+
20
+ self.class.attributes.each do |attribute|
21
+ unless (value = self.public_send attribute).nil?
22
+ serialized[attribute] = if value.respond_to? :strftime
23
+ value.strftime('%Y-%m-%dT%H:%M:%S')
24
+ else
25
+ value
26
+ end
27
+ end
28
+ end
29
+
30
+ serialized
31
+ end
32
+
33
+ def to_json *a
34
+ as_json.to_json *a
35
+ end
36
+ end
37
+ end
@@ -1,43 +1,28 @@
1
1
  module Steem
2
2
  module Retriable
3
3
  # @private
4
- MAX_RETRY_COUNT = 100
4
+ MAX_RETRY_COUNT = 30
5
5
 
6
- # @private
7
- MAX_BACKOFF = 30
6
+ MAX_RETRY_ELAPSE = 60
8
7
 
9
- MAX_RETRY_ELAPSE = 300
8
+ # @private
9
+ MAX_BACKOFF = MAX_RETRY_ELAPSE / 4
10
10
 
11
11
  RETRYABLE_EXCEPTIONS = [
12
12
  NonCanonicalSignatureError, IncorrectRequestIdError,
13
13
  IncorrectResponseIdError, RemoteDatabaseLockError
14
14
  ]
15
15
 
16
- # Expontential backoff.
17
- #
18
- # @private
19
- def backoff
20
- @backoff ||= 0.1
21
- @backoff *= 2
22
- if @backoff > MAX_BACKOFF
23
- @backoff = 0.1
24
-
25
- if Time.now.utc - @first_retry_at > MAX_RETRY_ELAPSE
26
- @retry_count = nil
27
- @first_retry_at = nil
28
- end
29
- end
30
-
31
- sleep @backoff
32
- end
33
-
34
16
  def can_retry?(e = nil)
35
17
  @retry_count ||= 0
36
- @first_retry_at ||= Time.now.utc
37
18
 
38
19
  return false if @retry_count >= MAX_RETRY_COUNT
39
20
 
40
- @retry_count += 1
21
+ @retry_count = if retry_reset?
22
+ @first_retry_at = nil
23
+ else
24
+ @retry_count + 1
25
+ end
41
26
 
42
27
  can_retry = case e
43
28
  when *RETRYABLE_EXCEPTIONS then true
@@ -48,5 +33,26 @@ module Steem
48
33
 
49
34
  can_retry
50
35
  end
36
+ private
37
+ # @private
38
+ def first_retry_at
39
+ @first_retry_at ||= Time.now.utc
40
+ end
41
+
42
+ # @private
43
+ def retry_reset?
44
+ Time.now.utc - first_retry_at > MAX_RETRY_ELAPSE
45
+ end
46
+
47
+ # Expontential backoff.
48
+ #
49
+ # @private
50
+ def backoff
51
+ @backoff ||= 0.1
52
+ @backoff *= 2
53
+ @backoff = 0.1 if @backoff > MAX_BACKOFF
54
+
55
+ sleep @backoff
56
+ end
51
57
  end
52
58
  end
@@ -0,0 +1,45 @@
1
+ module Steem
2
+ module Serializable
3
+ NUMERIC_TYPES = %i(unsigned_char uint16 uint32 uint64 signed_char int16 int32
4
+ int64 varint)
5
+
6
+ KNOWN_TYPES = NUMERIC_TYPES + %i(boolean string raw_bytes point_in_time
7
+ public_key amount price authority optional_authority
8
+ comment_options_extensions beneficiaries chain_properties required_auths
9
+ witness_properties empty_array lambda)
10
+
11
+ module ClassMethods
12
+ def def_attr key_pair
13
+ name = key_pair.keys.first.to_sym
14
+ type = key_pair.values.first.to_sym
15
+
16
+ self.attributes ||= []
17
+ self.attributes << name
18
+
19
+ attr_accessor *attributes
20
+ add_type name, type
21
+ end
22
+
23
+ def add_type name, type
24
+ name = name.to_sym
25
+ type = type.to_sym
26
+ raise "Unknown type: #{type}" unless KNOWN_TYPES.include? type
27
+
28
+ @serializable_types ||= {}
29
+ @serializable_types[name] = type
30
+ end
31
+
32
+ def numeric?(name)
33
+ NUMERIC_TYPES.include? @serializable_types[name.to_sym]
34
+ end
35
+
36
+ def serializable_types
37
+ @serializable_types
38
+ end
39
+ end
40
+
41
+ def self.included(base)
42
+ base.extend(ClassMethods)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,141 @@
1
+ module Steem
2
+ class Operation
3
+ include JSONable
4
+ include Serializable
5
+ include Utils
6
+
7
+ # IDs derrived from:
8
+ # https://github.com/steemit/steem/blob/127a441fbac2f06804359968bda83b66e602c891/libraries/protocol/include/steem/protocol/operations.hpp
9
+
10
+ IDS = [
11
+ :vote_operation,
12
+ :comment_operation,
13
+
14
+ :transfer_operation,
15
+ :transfer_to_vesting_operation,
16
+ :withdraw_vesting_operation,
17
+
18
+ :limit_order_create_operation,
19
+ :limit_order_cancel_operation,
20
+
21
+ :feed_publish_operation,
22
+ :convert_operation,
23
+
24
+ :account_create_operation,
25
+ :account_update_operation,
26
+
27
+ :witness_update_operation,
28
+ :account_witness_vote_operation,
29
+ :account_witness_proxy_operation,
30
+
31
+ :pow_operation,
32
+
33
+ :custom_operation,
34
+
35
+ :report_over_production_operation,
36
+
37
+ :delete_comment_operation,
38
+ :custom_json_operation,
39
+ :comment_options_operation,
40
+ :set_withdraw_vesting_route_operation,
41
+ :limit_order_create2_operation,
42
+ :claim_account_operation,
43
+ :create_claimed_account_operation,
44
+ :request_account_recovery_operation,
45
+ :recover_account_operation,
46
+ :change_recovery_account_operation,
47
+ :escrow_transfer_operation,
48
+ :escrow_dispute_operation,
49
+ :escrow_release_operation,
50
+ :pow2_operation,
51
+ :escrow_approve_operation,
52
+ :transfer_to_savings_operation,
53
+ :transfer_from_savings_operation,
54
+ :cancel_transfer_from_savings_operation,
55
+ :custom_binary_operation,
56
+ :decline_voting_rights_operation,
57
+ :reset_account_operation,
58
+ :set_reset_account_operation,
59
+ :claim_reward_balance_operation,
60
+ :delegate_vesting_shares_operation,
61
+ :account_create_with_delegation_operation,
62
+ :witness_set_properties_operation,
63
+
64
+ # SMT operations
65
+ :claim_reward_balance2_operation,
66
+
67
+ :smt_setup_operation,
68
+ :smt_cap_reveal_operation,
69
+ :smt_refund_operation,
70
+ :smt_setup_emissions_operation,
71
+ :smt_set_setup_parameters_operation,
72
+ :smt_set_runtime_parameters_operation,
73
+ :smt_create_operation,
74
+
75
+ # virtual operations below this point
76
+ :fill_convert_request_operation,
77
+ :author_reward_operation,
78
+ :curation_reward_operation,
79
+ :comment_reward_operation,
80
+ :liquidity_reward_operation,
81
+ :interest_operation,
82
+ :fill_vesting_withdraw_operation,
83
+ :fill_order_operation,
84
+ :shutdown_witness_operation,
85
+ :fill_transfer_from_savings_operation,
86
+ :hardfork_operation,
87
+ :comment_payout_update_operation,
88
+ :return_vesting_delegation_operation,
89
+ :comment_benefactor_reward_operation,
90
+ :producer_reward_operation,
91
+ :clear_null_account_balance_operation
92
+ ]
93
+
94
+ def self.op_id(op)
95
+ IDS.find_index op
96
+ end
97
+
98
+ def inspect
99
+ properties = self.class.attributes.map do |prop|
100
+ unless (v = instance_variable_get("@#{prop}")).nil?
101
+ v = if v.respond_to? :strftime
102
+ v.strftime('%Y-%m-%dT%H:%M:%S')
103
+ else
104
+ v
105
+ end
106
+
107
+ "@#{prop}=#{v}"
108
+ end
109
+ end.compact.join(', ')
110
+
111
+ "#<#{self.class.name} [#{properties}]>"
112
+ end
113
+
114
+ def [](key)
115
+ key = key.to_sym
116
+ send(key) if self.class.attributes.include?(key)
117
+ end
118
+
119
+ def []=(key, value)
120
+ key = key.to_sym
121
+
122
+ if self.class.attributes.include?(key)
123
+ if self.class.numeric? key
124
+ send("#{key}=", value.to_i)
125
+ else
126
+ send("#{key}=", value)
127
+ end
128
+ end
129
+ end
130
+
131
+ def ==(other_op)
132
+ return false if self.class != other_op.class
133
+
134
+ self.class.attributes.each do |prop|
135
+ return false if self[prop] != other_op[prop]
136
+ end
137
+
138
+ true
139
+ end
140
+ end
141
+ end