beowulf-ruby-testnet 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.
@@ -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