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.
- 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
|