beowulf-ruby-testnet 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ module Beowulf
2
+ class NetworkBroadcastApi < Api
3
+ def api_name
4
+ :network_broadcast_api
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,99 @@
1
+ module Beowulf
2
+ class Operation
3
+ include OperationIds
4
+ include OperationTypes
5
+ include Utils
6
+
7
+ def initialize(options = {})
8
+ opt = options.dup
9
+ @type = opt.delete(:type)
10
+
11
+ opt.each do |k, v|
12
+ instance_variable_set("@#{k}", type(@type, k, v))
13
+ end
14
+
15
+ @use_condenser_namespace = if options.keys.include? :use_condenser_namespace
16
+ options.delete(:use_condenser_namespace)
17
+ else
18
+ true
19
+ end
20
+
21
+ unless Operation::known_operation_names.include? @type
22
+ raise OperationError, "Unsupported operation type: #{@type}"
23
+ end
24
+ end
25
+
26
+ def to_bytes
27
+ bytes = [id(@type.to_sym)].pack('C')
28
+
29
+ Operation::param_names(@type.to_sym).each do |p|
30
+ next unless defined? p
31
+ # puts p
32
+ v = instance_variable_get("@#{p}")
33
+ # puts v
34
+ bytes += v.to_bytes and next if v.respond_to? :to_bytes
35
+
36
+ bytes += case v
37
+ when Symbol then pakStr(v.to_s)
38
+ when String then pakStr(v)
39
+ when Integer then paks(v)
40
+ when TrueClass then pakC(1)
41
+ when FalseClass then pakC(0)
42
+ when ::Array then pakArr(v)
43
+ when ::Hash then pakHash(v)
44
+ when Authority then v.to_bytes
45
+ when AuthorityUpdate then v.to_bytes
46
+ when NilClass then next
47
+ else
48
+ raise OperationError, "Unsupported type: #{v.class}"
49
+ end
50
+ end
51
+
52
+ bytes
53
+ end
54
+
55
+ def payload
56
+ params = {}
57
+
58
+ Operation::param_names(@type.to_sym).each do |p|
59
+ next unless defined? p
60
+
61
+ v = instance_variable_get("@#{p}")
62
+ next if v.nil?
63
+ next if v.class == Beowulf::Type::Future
64
+
65
+ params[p] = case v
66
+ when Beowulf::Type::Amount
67
+ v.to_s
68
+ else; v
69
+ end
70
+ end
71
+
72
+ [@type, params]
73
+ end
74
+ private
75
+ def self.broadcast_operations_json_path
76
+ @broadcast_operations_json_path ||= "#{File.dirname(__FILE__)}/broadcast_operations.json"
77
+ end
78
+
79
+ def self.broadcast_operations
80
+ @broadcast_operations ||= JSON[File.read broadcast_operations_json_path]
81
+ end
82
+
83
+ def self.known_operation_names
84
+ broadcast_operations.map { |op| op["operation"].to_sym }
85
+ end
86
+
87
+ def self.param_names(type)
88
+ broadcast_operations.each do |op|
89
+ if op['operation'].to_sym == type.to_sym
90
+ return op['params'].map(&:to_sym)
91
+ end
92
+ end
93
+ end
94
+
95
+ def use_condenser_namespace?
96
+ @use_condenser_namespace
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,33 @@
1
+ module Beowulf
2
+ module OperationIds
3
+ IDS = [
4
+ :transfer_operation,
5
+ :transfer_to_vesting_operation,
6
+ :withdraw_vesting_operation,
7
+
8
+ :account_create_operation,
9
+ :account_update_operation,
10
+
11
+ # :supernode_update_operation,
12
+ # :account_supernode_vote_operation,
13
+
14
+ # SMT operations
15
+ # :smt_create_operation,
16
+
17
+ # virtual operations below this point
18
+ # :fill_vesting_withdraw_operation,
19
+ # :shutdown_supernode_operation,
20
+ # :hardfork_operation,
21
+ # :producer_reward_operation,
22
+ # :clear_null_account_balance_operation
23
+ ]
24
+
25
+ def id(op)
26
+ if op.to_s =~ /_operation$/
27
+ IDS.find_index op
28
+ else
29
+ IDS.find_index "#{op}_operation".to_sym
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ module Beowulf
2
+ module OperationTypes
3
+ TYPES = {
4
+ transfer: {
5
+ amount: Type::Amount,
6
+ fee: Type::Amount
7
+ },
8
+ transfer_to_vesting: {
9
+ amount: Type::Amount
10
+ },
11
+ withdraw_vesting: {
12
+ vesting_shares: Type::Amount
13
+ },
14
+ account_create: {
15
+ fee: Type::Amount,
16
+ owner: Type::Authority
17
+ },
18
+ account_update: {
19
+ owner: Type::AuthorityUpdate,
20
+ fee: Type::Amount
21
+ }
22
+ }
23
+
24
+ def type(key, param, value)
25
+ return if value.nil?
26
+ t = TYPES[key] or return value
27
+ p = t[param] or return value
28
+ p.new(value)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,320 @@
1
+ require 'bitcoin'
2
+ require 'digest'
3
+ require 'time'
4
+
5
+ module Beowulf
6
+ class Transaction
7
+ include ChainConfig
8
+ include Utils
9
+
10
+ VALID_OPTIONS = %w(
11
+ wif private_key ref_block_num ref_block_prefix expiration
12
+ chain use_condenser_namespace
13
+ ).map(&:to_sym)
14
+ VALID_OPTIONS.each { |option| attr_accessor option }
15
+
16
+ def initialize(options = {})
17
+ #puts 'Transaction.initialize.options1', options.to_json
18
+ options = options.dup
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
+ #puts 'Transaction.initialize.options2', options.to_json
27
+
28
+ @chain ||= :beowulf
29
+ @chain = @chain.to_sym
30
+ @chain_id = chain_id options[:chain_id]
31
+ @url = options[:url] || url
32
+ @operations = options[:operations] || []
33
+
34
+ @self_logger = false
35
+ @logger = if options[:logger].nil?
36
+ @self_logger = true
37
+ Beowulf.logger
38
+ else
39
+ options[:logger]
40
+ end
41
+
42
+ unless NETWORK_CHAIN_IDS.include? @chain_id
43
+ warning "Unknown chain id: #{@chain_id}"
44
+ end
45
+
46
+ if !!wif && !!private_key
47
+ raise TransactionError, "Do not pass both wif and private_key. That's confusing."
48
+ end
49
+
50
+ if !!wif
51
+ @private_key = Bitcoin::Key.from_base58 wif
52
+ end
53
+
54
+ @ref_block_num ||= nil
55
+ @ref_block_prefix ||= nil
56
+ @expiration ||= nil
57
+ @created_time ||= Time.now.utc.to_i
58
+ @immutable_expiration = !!@expiration
59
+
60
+ options = options.merge(
61
+ url: @url,
62
+ chain: @chain,
63
+ pool_size: 1,
64
+ persist: false,
65
+ reuse_ssl_sessions: false
66
+ )
67
+
68
+ @api = Api.new(options)
69
+ @network_broadcast_api = NetworkBroadcastApi.new(options)
70
+
71
+ @use_condenser_namespace = if options.keys.include? :use_condenser_namespace
72
+ options[:use_condenser_namespace]
73
+ else
74
+ true
75
+ end
76
+
77
+ ObjectSpace.define_finalizer(self, self.class.finalize(@api, @network_broadcast_api, @self_logger, @logger))
78
+ end
79
+
80
+ def chain_id(chain_id = nil)
81
+ return chain_id if !!chain_id
82
+
83
+ case chain.to_s.downcase.to_sym
84
+ when :beowulf then NETWORKS_BEOWULF_CHAIN_ID
85
+ # when :test then NETWORKS_TEST_CHAIN_ID
86
+ end
87
+ end
88
+
89
+ def url
90
+ case chain.to_s.downcase.to_sym
91
+ when :beowulf then NETWORKS_BEOWULF_DEFAULT_NODE
92
+ # when :test then NETWORKS_TEST_DEFAULT_NODE
93
+ end
94
+ end
95
+
96
+ def process(broadcast = false)
97
+ prepare
98
+
99
+ if broadcast
100
+ loop do
101
+ response = broadcast_payload(payload)
102
+
103
+ if !!response.error
104
+ parser = ErrorParser.new(response)
105
+
106
+ if parser.can_reprepare?
107
+ debug "Error code: #{parser}, repreparing transaction ..."
108
+ prepare
109
+ redo
110
+ end
111
+ end
112
+
113
+ return response
114
+ end
115
+ else
116
+ self
117
+ end
118
+ ensure
119
+ shutdown
120
+ end
121
+
122
+ def operations
123
+ @operations = @operations.map do |op|
124
+ case op
125
+ when Operation then op
126
+ else; Operation.new(op)
127
+ end
128
+ end
129
+ end
130
+
131
+ def operations=(operations)
132
+ @operations = operations
133
+ end
134
+
135
+ def shutdown
136
+ @api.shutdown if !!@api
137
+ @network_broadcast_api.shutdown if !!@network_broadcast_api
138
+
139
+ if @self_logger
140
+ if !!@logger && defined?(@logger.close)
141
+ if defined?(@logger.closed?)
142
+ @logger.close unless @logger.closed?
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def use_condenser_namespace?
149
+ !!@use_condenser_namespace
150
+ end
151
+
152
+ def inspect
153
+ properties = %w(
154
+ url ref_block_num ref_block_prefix expiration chain
155
+ use_condenser_namespace immutable_expiration payload
156
+ ).map do |prop|
157
+ if !!(v = instance_variable_get("@#{prop}"))
158
+ "@#{prop}=#{v}"
159
+ end
160
+ end.compact.join(', ')
161
+
162
+ "#<#{self.class.name} [#{properties}]>"
163
+ end
164
+ private
165
+ def broadcast_payload(payload)
166
+ puts "Transaction.broadcast_payload:", payload.to_json
167
+ if use_condenser_namespace?
168
+ @api.broadcast_transaction_synchronous(payload)
169
+ else
170
+ @network_broadcast_api.broadcast_transaction_synchronous(trx: payload)
171
+ end
172
+ end
173
+
174
+ def payload
175
+ @payload ||= {
176
+ expiration: @expiration.strftime('%Y-%m-%dT%H:%M:%S'),
177
+ ref_block_num: @ref_block_num,
178
+ ref_block_prefix: @ref_block_prefix,
179
+ operations: operations.map { |op| op.payload },
180
+ extensions: [],
181
+ created_time: @created_time,
182
+ signatures: [hexlify(signature)]
183
+ }
184
+ end
185
+
186
+ def prepare
187
+ raise TransactionError, "No wif or private key." unless !!@wif || !!@private_key
188
+
189
+ @payload = nil
190
+
191
+ while @expiration.nil? && @ref_block_num.nil? && @ref_block_prefix.nil?
192
+ @api.get_dynamic_global_properties do |properties, error|
193
+ if !!error
194
+ raise TransactionError, "Unable to prepare transaction.", error
195
+ end
196
+
197
+ @properties = properties
198
+ end
199
+
200
+ # You can actually go back as far as the TaPoS buffer will allow, which
201
+ # is something like 50,000 blocks.
202
+
203
+ block_number = @properties.last_irreversible_block_num
204
+
205
+ @api.get_block(block_number) do |block, error|
206
+ if !!error
207
+ ap error if defined?(ap) && ENV['DEBUG'] == 'true'
208
+ raise TransactionError, "Unable to prepare transaction: #{error.message || 'Unknown cause.'}"
209
+ end
210
+
211
+ if !!block && !!block.previous
212
+ @ref_block_num = (block_number - 1) & 0xFFFF
213
+ @ref_block_prefix = unhexlify(block.previous[8..-1]).unpack('V*')[0]
214
+
215
+ # The expiration allows for transactions to expire if they are not
216
+ # included into a block by that time. Always update it to the current
217
+ # time + EXPIRE_IN_SECS.
218
+ #
219
+ block_time = Time.parse(@properties.time + 'Z')
220
+ @expiration ||= block_time + EXPIRE_IN_SECS
221
+ else
222
+ # Suspect this happens when there are microforks, but it should be
223
+ # rare, especially since we're asking for the last irreversible
224
+ # block.
225
+
226
+ if block.nil?
227
+ warning "Block missing while trying to prepare transaction, retrying ..."
228
+ else
229
+ debug block if %w(DEBUG TRACE).include? ENV['LOG']
230
+
231
+ warning "Block structure while trying to prepare transaction, retrying ..."
232
+ end
233
+
234
+ @expiration = nil unless @immutable_expiration
235
+ end
236
+ end
237
+ end
238
+
239
+ self
240
+ end
241
+
242
+ def to_bytes
243
+ bytes = unhexlify(@chain_id)
244
+ bytes << pakS(@ref_block_num) # 16-bit, 4 Hex
245
+ bytes << pakI(@ref_block_prefix) # 32-bit, 8 Hex
246
+ bytes << pakI(@expiration.to_i) # 32-bit, 8 Hex
247
+ bytes << pakC(operations.size) # 8-bit, 2 Hex
248
+
249
+ operations.each do |op|
250
+ bytes << op.to_bytes # n-bit ...
251
+ end
252
+
253
+ bytes << 0x0000 # extensions # 16-bit, 4 Hex
254
+ bytes << pakQ(@created_time.to_i) # 64-bit, 16 Hex
255
+
256
+ puts "Transaction.to_bytes:", hexlify(bytes)
257
+ bytes
258
+ end
259
+
260
+ def digest
261
+ Digest::SHA256.digest(to_bytes)
262
+ end
263
+
264
+ # May not find all non-canonicals, see: https://github.com/lian/bitcoin-ruby/issues/196
265
+ def signature
266
+ public_key_hex = @private_key.pub
267
+ ec = Bitcoin::OpenSSL_EC
268
+ digest_hex = digest.freeze
269
+ # puts "digest_hex:", hexlify(digest_hex)
270
+ count = 0
271
+
272
+ loop do
273
+ count += 1
274
+ debug "#{count} attempts to find canonical signature" if count % 40 == 0
275
+ sig = ec.sign_compact(digest_hex, @private_key.priv, public_key_hex, false)
276
+
277
+ next if public_key_hex != ec.recover_compact(digest_hex, sig)
278
+
279
+ return sig if canonical? sig
280
+ end
281
+ end
282
+
283
+ def canonical?(sig)
284
+ sig = sig.unpack('C*')
285
+
286
+ !(
287
+ ((sig[0] & 0x80 ) != 0) || ( sig[0] == 0 ) ||
288
+ ((sig[1] & 0x80 ) != 0) ||
289
+ ((sig[32] & 0x80 ) != 0) || ( sig[32] == 0 ) ||
290
+ ((sig[33] & 0x80 ) != 0)
291
+ )
292
+ end
293
+
294
+ def self.finalize(api, network_broadcast_api, self_logger, logger)
295
+ proc {
296
+ if !!api && !api.stopped?
297
+ puts "DESTROY: #{api.inspect}" if ENV['LOG'] == 'TRACE'
298
+ api.shutdown
299
+ api = nil
300
+ end
301
+
302
+ if !!network_broadcast_api && !network_broadcast_api.stopped?
303
+ puts "DESTROY: #{network_broadcast_api.inspect}" if ENV['LOG'] == 'TRACE'
304
+ network_broadcast_api.shutdown
305
+ network_broadcast_api = nil
306
+ end
307
+
308
+ begin
309
+ if self_logger
310
+ if !!logger && defined?(logger.close)
311
+ if defined?(logger.closed?)
312
+ logger.close unless logger.closed?
313
+ end
314
+ end
315
+ end
316
+ rescue IOError, NoMethodError => _; end
317
+ }
318
+ end
319
+ end
320
+ end