dpay-ruby 0.1.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,35 @@
1
+ module DPay
2
+ module RPC
3
+ # {ThreadSafeHttpClient} is the default RPC Client used by `dpay-ruby.`
4
+ # It's perfect for simple requests. But for higher performance, it's better
5
+ # to override {HttpClient} and implement something other than {Net::HTTP}.
6
+ #
7
+ # It performs http requests in a {Mutex} critical section because {Net::HTTP}
8
+ # is not thread safe. This is the very minimum level thread safety
9
+ # available.
10
+ class ThreadSafeHttpClient < HttpClient
11
+ SEMAPHORE = Mutex.new.freeze
12
+
13
+ # Same as #{HttpClient#http_post}, but scoped to each thread so it is
14
+ # thread safe.
15
+ def http_post
16
+ thread = Thread.current
17
+ http_post = thread.thread_variable_get(:http_post)
18
+ http_post ||= Net::HTTP::Post.new(uri.request_uri, POST_HEADERS)
19
+ thread.thread_variable_set(:http_post, http_post)
20
+ end
21
+
22
+ def http_request(request); SEMAPHORE.synchronize{super}; end
23
+
24
+ # Same as #{BaseClient#rpc_id}, auto-increment, but scoped to each thread
25
+ # so it is thread safe.
26
+ def rpc_id
27
+ thread = Thread.current
28
+ rpc_id = thread.thread_variable_get(:rpc_id)
29
+ rpc_id ||= 0
30
+ rpc_id += 1
31
+ thread.thread_variable_set(:rpc_id, rpc_id)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,377 @@
1
+ module DPay
2
+ # DPay::Stream allows a live view of the dPay blockchain.
3
+ #
4
+ # Example streaming blocks:
5
+ #
6
+ # stream = DPay::Stream.new
7
+ #
8
+ # stream.blocks do |block, block_num|
9
+ # puts "#{block_num} :: #{block.witness}"
10
+ # end
11
+ #
12
+ # Example streaming transactions:
13
+ #
14
+ # stream = DPay::Stream.new
15
+ #
16
+ # stream.transactions do |trx, trx_id, block_num|
17
+ # puts "#{block_num} :: #{trx_id} :: operations: #{trx.operations.size}"
18
+ # end
19
+ #
20
+ # Example streaming operations:
21
+ #
22
+ # stream = DPay::Stream.new
23
+ #
24
+ # stream.operations do |op, trx_id, block_num|
25
+ # puts "#{block_num} :: #{trx_id} :: #{op.type}: #{op.value.to_json}"
26
+ # end
27
+ #
28
+ # Allows streaming of block headers, full blocks, transactions, operations and
29
+ # virtual operations.
30
+ class Stream
31
+ attr_reader :database_api, :block_api, :account_history_api, :mode
32
+
33
+ BLOCK_INTERVAL = 3
34
+ MAX_BACKOFF_BLOCK_INTERVAL = 30
35
+ MAX_RETRY_COUNT = 10
36
+
37
+ VOP_TRX_ID = ('0' * 40).freeze
38
+
39
+ # @param options [Hash] additional options
40
+ # @option options [DPay::DatabaseApi] :database_api
41
+ # @option options [DPay::BlockApi] :block_api
42
+ # @option options [DPay::AccountHistoryApi || DPay::CondenserApi] :account_history_api
43
+ # @option options [Symbol] :mode we have the choice between
44
+ # * :head the last block
45
+ # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
46
+ # @option options [Boolean] :no_warn do not generate warnings
47
+ def initialize(options = {mode: :irreversible})
48
+ @instance_options = options
49
+ @database_api = options[:database_api] || DPay::DatabaseApi.new(options)
50
+ @block_api = options[:block_api] || DPay::BlockApi.new(options)
51
+ @account_history_api = options[:account_history_api]
52
+ @mode = options[:mode] || :irreversible
53
+ @no_warn = !!options[:no_warn]
54
+ end
55
+
56
+ # Use this method to stream block numbers. This is significantly faster
57
+ # than requesting full blocks and even block headers. Basically, the only
58
+ # thing this method does is call {DPay::Database#get_dynamic_global_properties} at 3 second
59
+ # intervals.
60
+ #
61
+ # @param options [Hash] additional options
62
+ # @option options [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
63
+ # @option options [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
64
+ def block_numbers(options = {}, &block)
65
+ block_objects(options.merge(object: :block_numbers), block)
66
+ end
67
+
68
+ # Use this method to stream block headers. This is quite a bit faster than
69
+ # requesting full blocks.
70
+ #
71
+ # @param options [Hash] additional options
72
+ # @option options [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
73
+ # @option options [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
74
+ def block_headers(options = {}, &block)
75
+ block_objects(options.merge(object: :block_headers), block)
76
+ end
77
+
78
+ # Use this method to stream full blocks.
79
+ #
80
+ # @param options [Hash] additional options
81
+ # @option options [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
82
+ # @option options [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
83
+ def blocks(options = {}, &block)
84
+ block_objects(options.merge(object: :blocks), block)
85
+ end
86
+
87
+ # Use this method to stream each transaction.
88
+ #
89
+ # @param options [Hash] additional options
90
+ # @option options [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
91
+ # @option options [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
92
+ def transactions(options = {}, &block)
93
+ blocks(options) do |block, block_num|
94
+ block.transactions.each_with_index do |transaction, index|
95
+ trx_id = block.transaction_ids[index]
96
+
97
+ yield transaction, trx_id, block_num
98
+ end
99
+ end
100
+ end
101
+
102
+ # Returns the latest operations from the blockchain.
103
+ #
104
+ # stream = DPay::Stream.new
105
+ # stream.operations do |op|
106
+ # puts op.to_json
107
+ # end
108
+ #
109
+ # If symbol are passed to `types` option, then only that operation is
110
+ # returned. Expected symbols are:
111
+ #
112
+ # account_create_operation
113
+ # account_create_with_delegation_operation
114
+ # account_update_operation
115
+ # account_witness_proxy_operation
116
+ # account_witness_vote_operation
117
+ # cancel_transfer_from_savings_operation
118
+ # change_recovery_account_operation
119
+ # claim_reward_balance_operation
120
+ # comment_operation
121
+ # comment_options_operation
122
+ # convert_operation
123
+ # custom_operation
124
+ # custom_json_operation
125
+ # decline_voting_rights_operation
126
+ # delegate_vesting_shares_operation
127
+ # delete_comment_operation
128
+ # escrow_approve_operation
129
+ # escrow_dispute_operation
130
+ # escrow_release_operation
131
+ # escrow_transfer_operation
132
+ # feed_publish_operation
133
+ # limit_order_cancel_operation
134
+ # limit_order_create_operation
135
+ # limit_order_create2_operation
136
+ # pow_operation
137
+ # pow2_operation
138
+ # recover_account_operation
139
+ # request_account_recovery_operation
140
+ # set_withdraw_vesting_route_operation
141
+ # transfer_operation
142
+ # transfer_from_savings_operation
143
+ # transfer_to_savings_operation
144
+ # transfer_to_vesting_operation
145
+ # vote_operation
146
+ # withdraw_vesting_operation
147
+ # witness_update_operation
148
+ #
149
+ # For example, to stream only votes:
150
+ #
151
+ # stream = DPay::Stream.new
152
+ # stream.operations(types: :vote_operation) do |vote|
153
+ # puts vote.to_json
154
+ # end
155
+ #
156
+ # ... Or ...
157
+ #
158
+ # stream = DPay::Stream.new
159
+ # stream.operations(:vote_operation) do |vote|
160
+ # puts vote.to_json
161
+ # end
162
+ #
163
+ # You can also stream virtual operations:
164
+ #
165
+ # stream = DPay::Stream.new
166
+ # stream.operations(types: :author_reward_operation, only_virtual: true) do |vop|
167
+ # v = vop.value
168
+ # puts "#{v.author} got paid for #{v.permlink}: #{[v.bbd_payout, v.dpay_payout, v.vesting_payout]}"
169
+ # end
170
+ #
171
+ # ... or multiple virtual operation types;
172
+ #
173
+ # stream = DPay::Stream.new
174
+ # stream.operations(types: [:producer_reward_operation, :author_reward_operation], only_virtual: true) do |vop|
175
+ # puts vop.to_json
176
+ # end
177
+ #
178
+ # ... or all types, including virtual operation types from the head block number:
179
+ #
180
+ # stream = DPay::Stream.new(mode: :head)
181
+ # stream.operations(include_virtual: true) do |op|
182
+ # puts op.to_json
183
+ # end
184
+ #
185
+ # Expected virtual operation types:
186
+ #
187
+ # producer_reward_operation
188
+ # author_reward_operation
189
+ # curation_reward_operation
190
+ # fill_convert_request_operation
191
+ # fill_order_operation
192
+ # fill_vesting_withdraw_operation
193
+ # interest_operation
194
+ # shutdown_witness_operation
195
+ #
196
+ # @param args [Symbol || Array<Symbol> || Hash] the type(s) of operation or hash of expanded options, optional.
197
+ # @option args [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
198
+ # @option args [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
199
+ # @option args [Symbol || Array<Symbol>] :types the type(s) of operation, optional.
200
+ # @option args [Boolean] :only_virtual Only stream virtual options. Setting this true will improve performance because the stream only needs block numbers to then retrieve virtual operations. Default: false.
201
+ # @option args [Boolean] :include_virtual Also stream virtual options. Setting this true will impact performance. Default: false.
202
+ # @param block the block to execute for each result. Yields: |op, trx_id, block_num|
203
+ def operations(*args, &block)
204
+ options = {}
205
+ types = []
206
+ only_virtual = false
207
+ include_virtual = false
208
+ last_block_num = nil
209
+
210
+ case args.first
211
+ when Hash
212
+ options = args.first
213
+ types = transform_types(options[:types])
214
+ only_virtual = !!options[:only_virtual] || false
215
+ include_virtual = !!options[:include_virtual] || only_virtual || false
216
+ when Symbol, Array then types = transform_types(args)
217
+ end
218
+
219
+ if only_virtual
220
+ block_numbers(options) do |block_num|
221
+ get_virtual_ops(types, block_num, block)
222
+ end
223
+ else
224
+ transactions(options) do |transaction, trx_id, block_num|
225
+ transaction.operations.each do |op|
226
+ yield op, trx_id, block_num if types.none? || types.include?(op.type)
227
+
228
+ next unless last_block_num != block_num
229
+
230
+ last_block_num = block_num
231
+
232
+ get_virtual_ops(types, block_num, block) if include_virtual
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ def account_history_api
239
+ @account_history_api ||= begin
240
+ DPay::AccountHistoryApi.new(@instance_options)
241
+ rescue DPay::UnknownApiError => e
242
+ warn "#{e.inspect}, falling back to DPay::CondenserApi." unless @no_warn
243
+ DPay::CondenserApi.new(@instance_options)
244
+ end
245
+ end
246
+ private
247
+ # @private
248
+ def block_objects(options = {}, block)
249
+ object = options[:object]
250
+ object_method = "get_#{object}".to_sym
251
+ block_interval = BLOCK_INTERVAL
252
+
253
+ at_block_num, until_block_num = if !!block_range = options[:block_range]
254
+ [block_range.first, block_range.last]
255
+ else
256
+ [options[:at_block_num], options[:until_block_num]]
257
+ end
258
+
259
+ loop do
260
+ break if !!until_block_num && !!at_block_num && until_block_num < at_block_num
261
+
262
+ database_api.get_dynamic_global_properties do |properties|
263
+ current_block_num = find_block_number(properties)
264
+ current_block_num = [current_block_num, until_block_num].compact.min
265
+ at_block_num ||= current_block_num
266
+
267
+ if current_block_num >= at_block_num
268
+ range = at_block_num..current_block_num
269
+
270
+ if object == :block_numbers
271
+ range.each do |n|
272
+ block.call n
273
+ block_interval = BLOCK_INTERVAL
274
+ end
275
+ else
276
+ block_api.send(object_method, block_range: range) do |b, n|
277
+ block.call b, n
278
+ block_interval = BLOCK_INTERVAL
279
+ end
280
+ end
281
+
282
+ at_block_num = range.max + 1
283
+ else
284
+ # The stream has stalled, so let's back off and let the node sync
285
+ # up. We'll catch up with a bigger batch in the next cycle.
286
+ block_interval = [block_interval * 2, MAX_BACKOFF_BLOCK_INTERVAL].min
287
+ end
288
+ end
289
+
290
+ sleep block_interval
291
+ end
292
+ end
293
+
294
+ # @private
295
+ def find_block_number(properties)
296
+ block_num = case mode
297
+ when :head then properties.head_block_number
298
+ when :irreversible then properties.last_irreversible_block_num
299
+ else; raise DPay::ArgumentError, "Unknown mode: #{mode}"
300
+ end
301
+
302
+ block_num
303
+ end
304
+
305
+ # @private
306
+ def transform_types(types)
307
+ [types].compact.flatten.map do |type|
308
+ type = type.to_s
309
+
310
+ unless type.end_with? '_operation'
311
+ warn "Op type #{type} is deprecated. Use #{type}_operation instead." unless @no_warn
312
+ type += '_operation'
313
+ end
314
+
315
+ type
316
+ end
317
+ end
318
+
319
+ # @private
320
+ def get_virtual_ops(types, block_num, block)
321
+ retries = 0
322
+
323
+ loop do
324
+ get_ops_in_block_options = case account_history_api
325
+ when DPay::CondenserApi
326
+ [block_num, true]
327
+ when DPay::AccountHistoryApi
328
+ {
329
+ block_num: block_num,
330
+ only_virtual: true
331
+ }
332
+ end
333
+
334
+ response = account_history_api.get_ops_in_block(*get_ops_in_block_options)
335
+ result = response.result
336
+
337
+ if result.nil?
338
+ if retries < MAX_RETRY_COUNT
339
+ warn "Retrying get_ops_in_block on block #{block_num}" unless @no_warn
340
+ retries = retries + 1
341
+ sleep 9
342
+ redo
343
+ else
344
+ raise TooManyRetriesError, "unable to get valid result while finding virtual operations for block: #{block_num}"
345
+ end
346
+ end
347
+
348
+ ops = case account_history_api
349
+ when DPay::CondenserApi
350
+ result.map do |trx|
351
+ op = {type: trx.op[0] + '_operation', value: trx.op[1]}
352
+ op = Hashie::Mash.new(op)
353
+ end
354
+ when DPay::AccountHistoryApi then result.ops.map { |trx| trx.op }
355
+ end
356
+
357
+ if ops.empty?
358
+ if retries < MAX_RETRY_COUNT
359
+ sleep 3
360
+ retries = retries + 1
361
+ redo
362
+ else
363
+ raise TooManyRetriesError, "unable to find virtual operations for block: #{block_num}"
364
+ end
365
+ end
366
+
367
+ ops.each do |op|
368
+ next if types.any? && !types.include?(op.type)
369
+
370
+ block.call op, VOP_TRX_ID, block_num
371
+ end
372
+
373
+ break
374
+ end
375
+ end
376
+ end
377
+ end
@@ -0,0 +1,386 @@
1
+ module DPay
2
+ # {TransactionBuilder} can be used to create a transaction that the
3
+ # {NetworkBroadcastApi} can broadcast to the rest of the platform. The main
4
+ # feature of this class is the ability to cryptographically sign the
5
+ # transaction so that it conforms to the consensus rules that are required by
6
+ # the blockchain.
7
+ #
8
+ # wif = '5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC'
9
+ # builder = DPay::TransactionBuilder.new(wif: wif)
10
+ # builder.put(vote: {
11
+ # voter: 'alice',
12
+ # author: 'bob',
13
+ # permlink: 'my-burgers',
14
+ # weight: 10000
15
+ # })
16
+ #
17
+ # trx = builder.transaction
18
+ # network_broadcast_api = DPay::CondenserApi.new
19
+ # network_broadcast_api.broadcast_transaction_synchronous(trx: trx)
20
+ #
21
+ #
22
+ # The `wif` value may also be an array, when signing with multiple signatures
23
+ # (multisig).
24
+ class TransactionBuilder
25
+ include Retriable
26
+ include ChainConfig
27
+ include Utils
28
+
29
+ attr_accessor :app_base, :database_api, :block_api, :expiration, :operations
30
+ attr_writer :wif
31
+ attr_reader :signed
32
+
33
+ alias app_base? app_base
34
+
35
+ def initialize(options = {})
36
+ @app_base = !!options[:app_base] # default false
37
+ @database_api = options[:database_api]
38
+ @block_api = options[:block_api]
39
+
40
+ if app_base?
41
+ @database_api ||= DPay::DatabaseApi.new(options)
42
+ @block_api ||= DPay::BlockApi.new(options)
43
+ else
44
+ @database_api ||= DPay::CondenserApi.new(options)
45
+ @block_api ||= DPay::CondenserApi.new(options)
46
+ end
47
+
48
+ @wif = [options[:wif]].flatten
49
+ @signed = false
50
+
51
+ if !!(trx = options[:trx])
52
+ trx = case trx
53
+ when String then JSON[trx]
54
+ else; trx
55
+ end
56
+
57
+ trx_options = {
58
+ ref_block_num: trx['ref_block_num'],
59
+ ref_block_prefix: trx['ref_block_prefix'],
60
+ extensions: (trx['extensions']),
61
+ operations: trx['operations'],
62
+ signatures: (trx['signatures']),
63
+ }
64
+
65
+ trx_options[:expiration] = case trx['expiration']
66
+ when String then Time.parse(trx['expiration'] + 'Z')
67
+ else; trx['expiration']
68
+ end
69
+
70
+ options = options.merge(trx_options)
71
+ end
72
+
73
+ @ref_block_num = options[:ref_block_num]
74
+ @ref_block_prefix = options[:ref_block_prefix]
75
+ @operations = options[:operations] || []
76
+ @expiration = options[:expiration]
77
+ @extensions = options[:extensions] || []
78
+ @signatures = options[:signatures] || []
79
+ @chain = options[:chain] || :dpay
80
+ @error_pipe = options[:error_pipe] || STDERR
81
+ @chain_id = case @chain
82
+ when :dpay then NETWORKS_DPAY_CHAIN_ID
83
+ when :test then NETWORKS_TEST_CHAIN_ID
84
+ else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
85
+ end
86
+ end
87
+
88
+ def inspect
89
+ properties = %w(
90
+ ref_block_num ref_block_prefix expiration operations extensions
91
+ signatures
92
+ ).map do |prop|
93
+ if !!(v = instance_variable_get("@#{prop}"))
94
+ "@#{prop}=#{v}"
95
+ end
96
+ end.compact.join(', ')
97
+
98
+ "#<#{self.class.name} [#{properties}]>"
99
+ end
100
+
101
+ def reset
102
+ @ref_block_num = nil
103
+ @ref_block_prefix = nil
104
+ @expiration = nil
105
+ @operations = []
106
+ @extensions = []
107
+ @signatures = []
108
+ @signed = false
109
+
110
+ self
111
+ end
112
+
113
+ def expired?
114
+ @expiration.nil? || @expiration < Time.now
115
+ end
116
+
117
+ # If the transaction can be prepared, this method will do so and set the
118
+ # expiration. Once the expiration is set, it will not re-prepare. If you
119
+ # call {#put}, the expiration is set {::Nil} so that it can be re-prepared.
120
+ #
121
+ # Usually, this method is called automatically by {#put} and/or {#transaction}.
122
+ #
123
+ # @return {TransactionBuilder}
124
+ def prepare
125
+ if expired?
126
+ catch :prepare_header do; begin
127
+ @database_api.get_dynamic_global_properties do |properties|
128
+ block_number = properties.last_irreversible_block_num
129
+ block_header_args = if app_base?
130
+ {block_num: block_number}
131
+ else
132
+ block_number
133
+ end
134
+
135
+ @block_api.get_block_header(block_header_args) do |result|
136
+ header = if app_base?
137
+ result.header
138
+ else
139
+ result
140
+ end
141
+
142
+ @ref_block_num = (block_number - 1) & 0xFFFF
143
+ @ref_block_prefix = unhexlify(header.previous[8..-1]).unpack('V*')[0]
144
+ @expiration ||= (Time.parse(properties.time + 'Z') + EXPIRE_IN_SECS).utc
145
+ end
146
+ end
147
+ rescue => e
148
+ if can_retry? e
149
+ @error_pipe.puts "#{e} ... retrying."
150
+ throw :prepare_header
151
+ else
152
+ raise e
153
+ end
154
+ end; end
155
+ end
156
+
157
+ self
158
+ end
159
+
160
+ # Sets operations all at once, then prepares.
161
+ def operations=(operations)
162
+ @operations = operations.map{ |op| normalize_operation(op) }
163
+ prepare
164
+ @operations
165
+ end
166
+
167
+ # A quick and flexible way to append a new operation to the transaction.
168
+ # This method uses ducktyping to figure out how to form the operation.
169
+ #
170
+ # There are three main ways you can call this method. These assume that
171
+ # `op_type` is a {::Symbol} (or {::String}) representing the type of operation and `op` is the
172
+ # operation {::Hash}.
173
+ #
174
+ # put(op_type, op)
175
+ #
176
+ # ... or ...
177
+ #
178
+ # put(op_type => op)
179
+ #
180
+ # ... or ...
181
+ #
182
+ # put([op_type, op])
183
+ #
184
+ # You can also chain multiple operations:
185
+ #
186
+ # builder = DPay::TransactionBuilder.new
187
+ # builder.put(vote: vote1).put(vote: vote2)
188
+ # @return {TransactionBuilder}
189
+ def put(type, op = nil)
190
+ @expiration = nil
191
+ @operations << normalize_operation(type, op)
192
+ prepare
193
+ self
194
+ end
195
+
196
+ # If all of the required values are set, this returns a fully formed
197
+ # transaction that is ready to broadcast.
198
+ #
199
+ # @return
200
+ # {
201
+ # :ref_block_num => 18912,
202
+ # :ref_block_prefix => 575781536,
203
+ # :expiration => "2018-04-26T15:26:12",
204
+ # :extensions => [],
205
+ # :operations => [[:vote, {
206
+ # :voter => "alice",
207
+ # :author => "bob",
208
+ # :permlink => "my-burgers",
209
+ # :weight => 10000
210
+ # }
211
+ # ]],
212
+ # :signatures => ["1c45b65740b4b2c17c4bcf6bcc3f8d90ddab827d50532729fc3b8f163f2c465a532b0112ae4bf388ccc97b7c2e0bc570caadda78af48cf3c261037e65eefcd941e"]
213
+ # }
214
+ def transaction
215
+ prepare
216
+ sign
217
+ end
218
+
219
+ # Appends to the `signatures` array of the transaction, built from a
220
+ # serialized digest.
221
+ #
222
+ # @return {Hash | TransactionBuilder} The fully signed transaction if a `wif` is provided or the instance of the {TransactionBuilder} if a `wif` has not yet been provided.
223
+ def sign
224
+ return self if @wif.empty?
225
+ return self if expired?
226
+
227
+ trx = {
228
+ ref_block_num: @ref_block_num,
229
+ ref_block_prefix: @ref_block_prefix,
230
+ expiration: @expiration.strftime('%Y-%m-%dT%H:%M:%S'),
231
+ operations: @operations,
232
+ extensions: @extensions,
233
+ signatures: @signatures
234
+ }
235
+
236
+ unless @signed
237
+ catch :serialize do; begin
238
+ transaction_hex_args = if app_base?
239
+ {trx: trx}
240
+ else
241
+ trx
242
+ end
243
+
244
+ @database_api.get_transaction_hex(transaction_hex_args) do |result|
245
+ hex = if app_base?
246
+ result.hex
247
+ else
248
+ result
249
+ end
250
+
251
+ hex = @chain_id + hex[0..-4] # Why do we have to chop the last two bytes?
252
+ digest = unhexlify(hex)
253
+ digest_hex = Digest::SHA256.digest(digest)
254
+ private_keys = @wif.map{ |wif| Bitcoin::Key.from_base58 wif }
255
+ ec = Bitcoin::OpenSSL_EC
256
+ count = 0
257
+ sigs = []
258
+
259
+ private_keys.each do |private_key|
260
+ sig = nil
261
+
262
+ loop do
263
+ count += 1
264
+ @error_pipe.puts "#{count} attempts to find canonical signature" if count % 40 == 0
265
+ public_key_hex = private_key.pub
266
+ sig = ec.sign_compact(digest_hex, private_key.priv, public_key_hex, false)
267
+
268
+ next if public_key_hex != ec.recover_compact(digest_hex, sig)
269
+ break if canonical? sig
270
+ end
271
+
272
+ @signatures << hexlify(sig)
273
+ end
274
+
275
+ @signed = true
276
+ trx[:signatures] = @signatures
277
+ end
278
+ rescue => e
279
+ if can_retry? e
280
+ @error_pipe.puts "#{e} ... retrying."
281
+ throw :serialize
282
+ else
283
+ raise e
284
+ end
285
+ end; end
286
+ end
287
+
288
+ Hashie::Mash.new trx
289
+ end
290
+
291
+ # @return [Array] All public keys that could possibly sign for a given transaction.
292
+ def potential_signatures
293
+ potential_signatures_args = if app_base?
294
+ {trx: transaction}
295
+ else
296
+ transaction
297
+ end
298
+
299
+ @database_api.get_potential_signatures(potential_signatures_args) do |result|
300
+ if app_base?
301
+ result[:keys]
302
+ else
303
+ result
304
+ end
305
+ end
306
+ end
307
+
308
+ # This API will take a partially signed transaction and a set of public keys
309
+ # that the owner has the ability to sign for and return the minimal subset
310
+ # of public keys that should add signatures to the transaction.
311
+ #
312
+ # @return [Array] The minimal subset of public keys that should add signatures to the transaction.
313
+ def required_signatures
314
+ required_signatures_args = if app_base?
315
+ {trx: transaction}
316
+ else
317
+ [transaction, []]
318
+ end
319
+
320
+ @database_api.get_required_signatures(*required_signatures_args) do |result|
321
+ if app_base?
322
+ result[:keys]
323
+ else
324
+ result
325
+ end
326
+ end
327
+ end
328
+
329
+ # @return [Boolean] True if the transaction has all of the required signatures.
330
+ def valid?
331
+ verify_authority_args = if app_base?
332
+ {trx: transaction}
333
+ else
334
+ transaction
335
+ end
336
+
337
+ @database_api.verify_authority(verify_authority_args) do |result|
338
+ if app_base?
339
+ result.valid
340
+ else
341
+ result
342
+ end
343
+ end
344
+ end
345
+ private
346
+ # See: https://github.com/dpays/dpay/pull/2500
347
+ # @private
348
+ def canonical?(sig)
349
+ sig = sig.unpack('C*')
350
+
351
+ !(
352
+ ((sig[0] & 0x80 ) != 0) || ( sig[0] == 0 ) ||
353
+ ((sig[1] & 0x80 ) != 0) ||
354
+ ((sig[32] & 0x80 ) != 0) || ( sig[32] == 0 ) ||
355
+ ((sig[33] & 0x80 ) != 0)
356
+ )
357
+ end
358
+
359
+ def normalize_operation(type, op = nil)
360
+ if app_base?
361
+ case type
362
+ when Symbol, String
363
+ type_value = "#{type}_operation"
364
+ {type: type_value, value: op}
365
+ when Hash
366
+ type_value = "#{type.keys.first}_operation"
367
+ {type: type_value, value: type.values.first}
368
+ when Array
369
+ type_value = "#{type[0]}_operation"
370
+ {type: type_value, value: type[1]}
371
+ else
372
+ raise DPay::ArgumentError, "Don't know what to do with operation type #{type.class}: #{type} (#{op})"
373
+ end
374
+ else
375
+ case type
376
+ when Symbol then [type, op]
377
+ when String then [type.to_sym, op]
378
+ when Hash then [type.keys.first.to_sym, type.values.first]
379
+ when Array then type
380
+ else
381
+ raise DPay::ArgumentError, "Don't know what to do with operation type #{type.class}: #{type} (#{op})"
382
+ end
383
+ end
384
+ end
385
+ end
386
+ end