radiator 0.2.0a

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,113 @@
1
+ module Radiator
2
+
3
+ # See: https://github.com/steemit/steem-js/blob/766746adb5ded86380be982c844f4c269f7800ae/src/auth/serializer/src/operations.js
4
+ module OperationTypes
5
+ TYPES = {
6
+ transfer: {
7
+ amount: Type::Amount
8
+ },
9
+ transfer_to_vesting: {
10
+ amount: Type::Amount
11
+ },
12
+ withdraw_vesting: {
13
+ vesting_shares: Type::Amount
14
+ },
15
+ limit_order_create: {
16
+ orderid: Type::Uint32,
17
+ amount_to_sell: Type::Amount,
18
+ min_to_receive: Type::Amount,
19
+ expiration: Type::PointInTime
20
+ },
21
+ limit_order_cancel: {
22
+ orderid: Type::Uint32
23
+ },
24
+ convert: {
25
+ requestid: Type::Uint32,
26
+ amount: Type::Amount
27
+ },
28
+ account_create: {
29
+ fee: Type::Amount,
30
+ owner: Type::Permission,
31
+ active: Type::Permission,
32
+ posting: Type::Permission,
33
+ memo: Type::Permission
34
+ },
35
+ account_update: {
36
+ owner: Type::Permission,
37
+ active: Type::Permission,
38
+ posting: Type::Permission,
39
+ memo: Type::PublicKey
40
+ },
41
+ custom: {
42
+ id: Type::Uint16
43
+ },
44
+ comment_options: {
45
+ max_accepted_payout: Type::Amount
46
+ },
47
+ set_withdraw_vesting_route: {
48
+ percent: Type::Uint16
49
+ },
50
+ request_account_recovery: {
51
+ new_owner_Permission: Type::Permission
52
+ },
53
+ recover_account: {
54
+ new_owner_Permission: Type::Permission,
55
+ recent_owner_Permission: Type::Permission
56
+ },
57
+ escrow_transfer: {
58
+ sbd_amount: Type::Amount,
59
+ steem_amount: Type::Amount,
60
+ escrow_id: Type::Uint32,
61
+ fee: Type::Amount,
62
+ ratification_deadline: Type::PointInTime,
63
+ escrow_expiration: Type::PointInTime
64
+ },
65
+ escrow_dispute: {
66
+ escrow_id: Type::Uint32
67
+ },
68
+ escrow_release: {
69
+ escrow_id: Type::Uint32,
70
+ sbd_amount: Type::Amount,
71
+ steem_amount: Type::Amount
72
+ },
73
+ escrow_approve: {
74
+ escrow_id: Type::Uint32
75
+ },
76
+ transfer_to_savings: {
77
+ amount: Type::Amount
78
+ },
79
+ transfer_from_savings: {
80
+ request_id: Type::Uint32,
81
+ amount: Type::Amount
82
+ },
83
+ cancel_transfer_from_savings: {
84
+ request_id: Type::Uint32
85
+ },
86
+ reset_account: {
87
+ new_owner_Permission: Type::Amount
88
+ },
89
+ set_reset_account: {
90
+ reward_steem: Type::Amount,
91
+ reward_sbd: Type::Amount,
92
+ reward_vests: Type::Amount
93
+ },
94
+ claim_reward_balance: {
95
+ reward_steem: Type::Amount,
96
+ reward_sbd: Type::Amount,
97
+ reward_vests: Type::Amount
98
+ },
99
+ delegate_vesting_shares: {
100
+ vesting_shares: Type::Amount
101
+ }
102
+ }
103
+
104
+ def type(key, param, value)
105
+ return if value.nil?
106
+
107
+ t = TYPES[key] or return value
108
+ p = t[param] or return value
109
+
110
+ p.new(value)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,260 @@
1
+ module Radiator
2
+ # Radiator::Stream allows a live view of the STEEM blockchain.
3
+ #
4
+ # All values returned by `get_dynamic_global_properties` can be streamed.
5
+ #
6
+ # For example, if you want to know which witness is currently signing blocks,
7
+ # use the following:
8
+ #
9
+ # stream = Radiator::Stream.new
10
+ # stream.current_witness do |witness|
11
+ # puts witness
12
+ # end
13
+ class Stream < Api
14
+ INITIAL_TIMEOUT = 0.0200
15
+ MAX_TIMEOUT = 80
16
+ MAX_BLOCKS_PER_NODE = 100
17
+
18
+ def initialize(options = {})
19
+ @api_options = options
20
+ @logger = @api_options[:logger] || Radiator.logger
21
+ end
22
+
23
+ def api
24
+ @api ||= Api.new(@api_options)
25
+ end
26
+
27
+ def method_names
28
+ @method_names ||= [
29
+ :head_block_number,
30
+ :head_block_id,
31
+ :time,
32
+ :current_witness,
33
+ :total_pow,
34
+ :num_pow_witnesses,
35
+ :virtual_supply,
36
+ :current_supply,
37
+ :confidential_supply,
38
+ :current_sbd_supply,
39
+ :confidential_sbd_supply,
40
+ :total_vesting_fund_steem,
41
+ :total_vesting_shares,
42
+ :total_reward_fund_steem,
43
+ :total_reward_shares2,
44
+ :total_activity_fund_steem,
45
+ :total_activity_fund_shares,
46
+ :sbd_interest_rate,
47
+ :average_block_size,
48
+ :maximum_block_size,
49
+ :current_aslot,
50
+ :recent_slots_filled,
51
+ :participation_count,
52
+ :last_irreversible_block_num,
53
+ :max_virtual_bandwidth,
54
+ :current_reserve_ratio,
55
+ :block_numbers,
56
+ :blocks
57
+ ].freeze
58
+ end
59
+
60
+ def method_params(method)
61
+ case method
62
+ when :block_numbers then {head_block_number: nil}
63
+ when :blocks then {get_block: :head_block_number}
64
+ else; nil
65
+ end
66
+ end
67
+
68
+ # Returns the latest operations from the blockchain.
69
+ #
70
+ # If symbol are passed, then only that operation is returned. Expected
71
+ # symbols are:
72
+ #
73
+ # transfer_to_vesting
74
+ # withdraw_vesting
75
+ # interest
76
+ # transfer
77
+ # liquidity_reward
78
+ # author_reward
79
+ # curation_reward
80
+ # transfer_to_savings
81
+ # transfer_from_savings
82
+ # cancel_transfer_from_savings
83
+ # escrow_transfer
84
+ # escrow_approve
85
+ # escrow_dispute
86
+ # escrow_release
87
+ # comment
88
+ # limit_order_create
89
+ # limit_order_cancel
90
+ # fill_convert_request
91
+ # fill_order
92
+ # vote
93
+ # account_witness_vote
94
+ # account_witness_proxy
95
+ # account_create
96
+ # account_update
97
+ # witness_update
98
+ # pow
99
+ # custom
100
+ #
101
+ # @param type [symbol || Array<symbol>] the type(s) of operation, optional.
102
+ # @param start starting block
103
+ # @param mode we have the choice between
104
+ # * :head the last block
105
+ # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
106
+ # @param block the block to execute for each result, optional.
107
+ # @return [Hash]
108
+ def operations(type = nil, start = nil, mode = :irreversible, &block)
109
+ transactions(start, mode) do |transaction|
110
+ ops = transaction.operations.map do |t, op|
111
+ t = t.to_sym
112
+ if type == t
113
+ op
114
+ elsif type.nil? || [type].flatten.include?(t)
115
+ {t => op}
116
+ end
117
+ end.compact
118
+
119
+ next if ops.none?
120
+
121
+ return ops unless !!block
122
+
123
+ ops.each do |op|
124
+ yield op
125
+ end
126
+ end
127
+ end
128
+
129
+ # Returns the latest transactions from the blockchain.
130
+ #
131
+ # @param start starting block
132
+ # @param mode we have the choice between
133
+ # * :head the last block
134
+ # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
135
+ # @param block the block to execute for each result, optional.
136
+ # @return [Hash]
137
+ def transactions(start = nil, mode = :irreversible, &block)
138
+ blocks(start, mode) do |b|
139
+ next if (_transactions = b.transactions).nil?
140
+ return _transactions unless !!block
141
+
142
+ _transactions.each.each do |transaction|
143
+ yield transaction
144
+ end
145
+ end
146
+ end
147
+
148
+ # Returns the latest blocks from the blockchain.
149
+ #
150
+ # @param start starting block
151
+ # @param mode we have the choice between
152
+ # * :head the last block
153
+ # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
154
+ # * :max_blocks_per_node the number of blocks to read before trying a new node
155
+ # @param block the block to execute for each result, optional.
156
+ # @return [Hash]
157
+ def blocks(start = nil, mode = :irreversible, max_blocks_per_node = MAX_BLOCKS_PER_NODE, &block)
158
+ counter = 0
159
+
160
+ if start.nil?
161
+ properties = api.get_dynamic_global_properties.result
162
+ start = case mode.to_sym
163
+ when :head then properties.head_block_number
164
+ when :irreversible then properties.last_irreversible_block_num
165
+ else; raise StreamError, '"mode" has to be "head" or "irreversible"'
166
+ end
167
+ end
168
+
169
+ loop do
170
+ properties = api.get_dynamic_global_properties.result
171
+
172
+ head_block = case mode.to_sym
173
+ when :head then properties.head_block_number
174
+ when :irreversible then properties.last_irreversible_block_num
175
+ else; raise StreamError, '"mode" has to be "head" or "irreversible"'
176
+ end
177
+
178
+ [*(start..(head_block))].each do |n|
179
+ if (counter += 1) > max_blocks_per_node
180
+ shutdown
181
+ counter = 0
182
+ end
183
+
184
+ response = api.send(:get_block, n)
185
+ raise StreamError, JSON[response.error] if !!response.error
186
+ result = response.result
187
+
188
+ if !!block
189
+ yield result
190
+ else
191
+ return result
192
+ end
193
+ end
194
+
195
+ start = head_block + 1
196
+ sleep 3
197
+ end
198
+ end
199
+
200
+ def method_missing(m, *args, &block)
201
+ super unless respond_to_missing?(m)
202
+
203
+ @latest_values ||= []
204
+ @latest_values.shift(5) if @latest_values.size > 20
205
+ loop do
206
+ value = if (n = method_params(m)).nil?
207
+ key_value = api.get_dynamic_global_properties.result[m]
208
+ else
209
+ key = n.keys.first
210
+ if !!n[key]
211
+ r = api.get_dynamic_global_properties.result
212
+ key_value = param = r[n[key]]
213
+ result = nil
214
+ loop do
215
+ response = api.send(key, param)
216
+ raise StreamError, JSON[response.error] if !!response.error
217
+ result = response.result
218
+ break if !!result
219
+ @logger.warn "#{key}: #{param} result missing, retrying with timeout: #{@timeout || INITIAL_TIMEOUT} seconds"
220
+ shutdown
221
+ sleep timeout
222
+ end
223
+ @timeout = INITIAL_TIMEOUT
224
+ result
225
+ else
226
+ key_value = api.get_dynamic_global_properties.result[key]
227
+ end
228
+ end
229
+ unless @latest_values.include? key_value
230
+ @latest_values << key_value
231
+ if !!block
232
+ yield value
233
+ else
234
+ return value
235
+ end
236
+ end
237
+ sleep 0.0200
238
+ end
239
+ end
240
+
241
+ def timeout
242
+ @timeout ||= INITIAL_TIMEOUT
243
+ @timeout *= 2
244
+ @timeout = INITIAL_TIMEOUT if @timeout > MAX_TIMEOUT
245
+ @timeout
246
+ end
247
+
248
+ # Stops the persistant http connections.
249
+ #
250
+ def shutdown
251
+ begin
252
+ @api.shutdown
253
+ rescue => e
254
+ @logger.warn("Unable to shut down: #{e}")
255
+ end
256
+
257
+ @api = nil
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,11 @@
1
+ module Radiator
2
+ class TagApi < Api
3
+ def method_names
4
+ @method_names ||= [:get_tags].freeze
5
+ end
6
+
7
+ def api_name
8
+ :tag_api
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,150 @@
1
+ require 'bitcoin'
2
+ require 'digest'
3
+ require 'time'
4
+
5
+ module Radiator
6
+ # * graphenej:
7
+ # * https://github.com/kenCode-de/graphenej/blob/master/graphenej/src/main/java/de/bitsharesmunich/graphenej/Transaction.java#L142
8
+ class Transaction
9
+ include ChainConfig
10
+ include Utils
11
+
12
+ VALID_OPTIONS = %w(
13
+ wif private_key ref_block_num ref_block_prefix expiration operations
14
+ chain
15
+ ).map(&:to_sym)
16
+ VALID_OPTIONS.each { |option| attr_accessor option }
17
+
18
+ def initialize(options = {})
19
+ options.each do |k, v|
20
+ k = k.to_sym
21
+ if VALID_OPTIONS.include?(k.to_sym)
22
+ options.delete(k)
23
+ send("#{k}=", v)
24
+ end
25
+ end
26
+
27
+ @logger = options[:logger] || Radiator.logger
28
+ @chain ||= :steem
29
+ @chain_id = chain_id options[:chain_id]
30
+ @url = options[:url] || url
31
+ @operations ||= []
32
+
33
+ unless NETWORK_CHAIN_IDS.include? @chain_id
34
+ @logger.warn "Unknown chain id: #{@chain_id}"
35
+ end
36
+
37
+ if !!wif && !!private_key
38
+ raise TransactionError, "Do not pass both wif and private_key. That's confusing."
39
+ end
40
+
41
+ if !!wif
42
+ @private_key = Bitcoin::Key.from_base58 wif
43
+ end
44
+
45
+ options = options.merge(url: @url)
46
+ @api = Api.new(options)
47
+ @network_broadcast_api = NetworkBroadcastApi.new(options)
48
+ end
49
+
50
+ def chain_id(chain_id = nil)
51
+ return chain_id if !!chain_id
52
+
53
+ case chain.to_s.downcase.to_sym
54
+ when :steem then NETWORKS_STEEM_CHAIN_ID
55
+ when :golos then NETWORKS_GOLOS_CHAIN_ID
56
+ when :test then NETWORKS_TEST_CHAIN_ID
57
+ end
58
+ end
59
+
60
+ def url
61
+ case chain.to_s.downcase.to_sym
62
+ when :steem then NETWORKS_STEEM_DEFAULT_NODE
63
+ when :golos then NETWORKS_GOLOS_DEFAULT_NODE
64
+ when :test then NETWORKS_TEST_DEFAULT_NODE
65
+ end
66
+ end
67
+
68
+ def process(broadcast = false)
69
+ prepare
70
+
71
+ if broadcast
72
+ @network_broadcast_api.broadcast_transaction_synchronous(payload)
73
+ else
74
+ self
75
+ end
76
+ end
77
+ private
78
+ def payload
79
+ {
80
+ expiration: @expiration.strftime('%Y-%m-%dT%H:%M:%S'),
81
+ ref_block_num: @ref_block_num,
82
+ ref_block_prefix: @ref_block_prefix,
83
+ operations: @operations.map { |op| op.payload },
84
+ extensions: [],
85
+ signatures: [hexlify(signature)]
86
+ }
87
+ end
88
+
89
+ def prepare
90
+ raise TransactionError, "No wif or private key." unless !!@wif || !!@private_key
91
+
92
+ @properties = @api.get_dynamic_global_properties.result
93
+ @ref_block_num = @properties.head_block_number & 0xFFFF
94
+ @ref_block_prefix = unhexlify(@properties.head_block_id[8..-1]).unpack('V*')[0]
95
+
96
+ # The expiration allows for transactions to expire if they are not
97
+ # included into a block by that time. Always update it to the current
98
+ # time + EXPIRE_IN_SECS.
99
+ @expiration = Time.parse(@properties.time + 'Z') + EXPIRE_IN_SECS
100
+
101
+ self
102
+ end
103
+
104
+ def to_bytes
105
+ bytes = unhexlify(@chain_id)
106
+ bytes << pakS(@ref_block_num)
107
+ bytes << pakI(@ref_block_prefix)
108
+ bytes << pakI(@expiration.to_i)
109
+ bytes << pakC(@operations.size)
110
+
111
+ @operations.each do |op|
112
+ bytes << op.to_bytes
113
+ end
114
+
115
+ bytes << 0x00 # extensions
116
+
117
+ bytes
118
+ end
119
+
120
+ def digest
121
+ Digest::SHA256.digest(to_bytes)
122
+ end
123
+
124
+ def signature
125
+ public_key_hex = @private_key.pub
126
+ ec = Bitcoin::OpenSSL_EC
127
+ digest_hex = digest.freeze
128
+
129
+ loop do
130
+ @expiration += 1
131
+ sig = ec.sign_compact(digest_hex, @private_key.priv, public_key_hex)
132
+
133
+ next if public_key_hex != ec.recover_compact(digest_hex, sig)
134
+
135
+ return sig if canonical? sig
136
+ end
137
+ end
138
+
139
+ def canonical?(sig)
140
+ sig = sig.unpack('C*')
141
+
142
+ !(
143
+ ((sig[0] & 0x80 ) != 0) || ( sig[0] == 0 ) ||
144
+ ((sig[1] & 0x80 ) != 0) ||
145
+ ((sig[32] & 0x80 ) != 0) || ( sig[32] == 0 ) ||
146
+ ((sig[33] & 0x80 ) != 0)
147
+ )
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,36 @@
1
+ module Radiator
2
+ module Type
3
+
4
+ # See: https://github.com/xeroc/piston-lib/blob/34a7525cee119ec9b24a99577ede2d54466fca0e/steembase/operations.py
5
+ class Amount
6
+ def initialize(value)
7
+ @amount, @asset = value.strip.split(' ')
8
+ @precision = case @asset
9
+ when 'STEEM' then 3
10
+ when 'VESTS' then 6
11
+ when 'SBD' then 3
12
+ when 'GOLOS' then 3
13
+ when 'GESTS' then 6
14
+ when 'GBG' then 3
15
+ when 'CORE' then 3
16
+ when 'CESTS' then 6
17
+ when 'TEST' then 3
18
+ else; raise TypeError, "Asset #{@asset} unknown."
19
+ end
20
+ end
21
+
22
+ def to_bytes
23
+ asset = @asset.ljust(7, "\x00")
24
+ amount = (@amount.to_f * 10 ** @precision).round
25
+
26
+ [amount].pack('q') +
27
+ [@precision].pack('c') +
28
+ asset
29
+ end
30
+
31
+ def to_s
32
+ "#{@amount} #{@asset}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ module Radiator
2
+ module Type
3
+
4
+ # See: https://github.com/xeroc/piston-lib/blob/34a7525cee119ec9b24a99577ede2d54466fca0e/steembase/operations.py
5
+ class Permission
6
+ def initialize()
7
+ raise NotImplementedError, 'stub'
8
+ end
9
+
10
+ def to_bytes
11
+ end
12
+
13
+ def to_s
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Radiator
2
+ module Type
3
+
4
+ # See: https://github.com/xeroc/python-graphenelib/blob/98de98e219264d45fe04b3c28f3aabd1a9f58b71/graphenebase/types.py
5
+ class PointInTime
6
+ def initialize(value)
7
+ @value = value
8
+ end
9
+
10
+ def to_bytes
11
+ [Time.parse(@value + 'Z').to_i].pack('I')
12
+ end
13
+
14
+ def to_s
15
+ @value
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Radiator
2
+ module Type
3
+
4
+ # See: https://github.com/xeroc/piston-lib/blob/34a7525cee119ec9b24a99577ede2d54466fca0e/steembase/operations.py
5
+ class PublicKey
6
+ def initialize()
7
+ raise NotImplementedError, 'stub'
8
+ end
9
+
10
+ def to_bytes
11
+ end
12
+
13
+ def to_s
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Radiator
2
+ module Type
3
+
4
+ # See: https://github.com/xeroc/piston-lib/blob/34a7525cee119ec9b24a99577ede2d54466fca0e/steembase/operations.py
5
+ class Uint16
6
+ def initialize(value)
7
+ @value = value.to_i
8
+ end
9
+
10
+ def to_bytes
11
+ [@value].pack('S')
12
+ end
13
+
14
+ def to_s
15
+ @value.to_s
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Radiator
2
+ module Type
3
+
4
+ # See: https://github.com/xeroc/piston-lib/blob/34a7525cee119ec9b24a99577ede2d54466fca0e/steembase/operations.py
5
+ class Uint32
6
+ def initialize(value)
7
+ @value = value.to_i
8
+ end
9
+
10
+ def to_bytes
11
+ [@value].pack('L')
12
+ end
13
+
14
+ def to_s
15
+ @value.to_s
16
+ end
17
+ end
18
+ end
19
+ end