beowulf-ruby-testnet 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +58 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +204 -0
- data/Rakefile +52 -0
- data/beowulf.gemspec +43 -0
- data/lib/beowulf.rb +35 -0
- data/lib/beowulf/account_history_api.rb +15 -0
- data/lib/beowulf/api.rb +845 -0
- data/lib/beowulf/base_error.rb +21 -0
- data/lib/beowulf/block_api.rb +14 -0
- data/lib/beowulf/broadcast_operations.json +74 -0
- data/lib/beowulf/chain_config.rb +15 -0
- data/lib/beowulf/condenser_api.rb +48 -0
- data/lib/beowulf/database_api.rb +5 -0
- data/lib/beowulf/error_parser.rb +228 -0
- data/lib/beowulf/logger.rb +20 -0
- data/lib/beowulf/methods.json +129 -0
- data/lib/beowulf/network_broadcast_api.rb +7 -0
- data/lib/beowulf/operation.rb +99 -0
- data/lib/beowulf/operation_ids.rb +33 -0
- data/lib/beowulf/operation_types.rb +31 -0
- data/lib/beowulf/transaction.rb +320 -0
- data/lib/beowulf/type/amount.rb +23 -0
- data/lib/beowulf/type/array.rb +17 -0
- data/lib/beowulf/type/authority.rb +48 -0
- data/lib/beowulf/type/authority_update.rb +50 -0
- data/lib/beowulf/type/future.rb +16 -0
- data/lib/beowulf/type/hash.rb +17 -0
- data/lib/beowulf/type/permission.rb +17 -0
- data/lib/beowulf/type/point_in_time.rb +17 -0
- data/lib/beowulf/type/public_key.rb +18 -0
- data/lib/beowulf/type/serializer.rb +12 -0
- data/lib/beowulf/type/u_int16.rb +17 -0
- data/lib/beowulf/type/u_int32.rb +17 -0
- data/lib/beowulf/utils.rb +221 -0
- data/lib/beowulf/version.rb +4 -0
- data/lib/beowulf/wallet.rb +212 -0
- metadata +463 -0
@@ -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
|