glueby 1.1.2 → 1.2.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,462 @@
1
+ module Glueby
2
+ module Internal
3
+ class ContractBuilder < Tapyrus::TxBuilder
4
+ attr_reader :fee_estimator, :sender_wallet, :prev_txs, :p2c_utxos, :use_unfinalized_utxo, :use_auto_fulfill_inputs
5
+
6
+ # @param [Glueby::Internal::Wallet] sender_wallet The wallet that is an user's wallet who send the transaction
7
+ # to a blockchain.
8
+ # @param [Symbol|Glueby::Contract::FeeEstimator] fee_estimator :auto or :fixed
9
+ # @param [Boolean] use_auto_fulfill_inputs
10
+ # If it's true, inputs are automatically added up to fulfill the TPC and tokens requirement that is added by
11
+ # #pay and #burn. The option also support to fill adding TPC inputs for paying fee.
12
+ # If Glueby.configuration.use_utxo_provider? is true, All the TPC inputs are added from the UtxoProvider's
13
+ # wallet. It it's false, all the TPC inputs are from sender_wallet.
14
+ # If Glueby.configuration.fee_provider_bears? is true, it won't add TPC amount for fee.
15
+ # @param [Boolean] use_unfinalized_utxo If it's true, The ContractBuilder use unfinalized UTXO that is not
16
+ # included in the block in its inputs.
17
+ # @raise [Glueby::ArgumentError] If the fee_estimator is not :auto or :fixed
18
+ def initialize(
19
+ sender_wallet:,
20
+ fee_estimator: :auto,
21
+ use_auto_fulfill_inputs: false,
22
+ use_unfinalized_utxo: false
23
+ )
24
+ @sender_wallet = sender_wallet
25
+ set_fee_estimator(fee_estimator)
26
+ @use_auto_fulfill_inputs = use_auto_fulfill_inputs
27
+ @use_unfinalized_utxo = use_unfinalized_utxo
28
+ @p2c_utxos = []
29
+ @prev_txs = []
30
+ @change_script_pubkeys = {}
31
+ @burn_contract = false
32
+ @issues = Hash.new(0)
33
+ super()
34
+ end
35
+
36
+ # Issue reissuable token
37
+ # @param script_pubkey [Tapyrus::Script] the script pubkey in the issue input.
38
+ # @param address [String] p2pkh or p2sh address.
39
+ # @param value [Integer] issued amount.
40
+ def reissuable(script_pubkey, address, value)
41
+ color_id = Tapyrus::Color::ColorIdentifier.reissuable(script_pubkey)
42
+ @issues[color_id] += value
43
+ super
44
+ end
45
+
46
+ # Issue reissuable token to the split outputs
47
+ # @param [Tapyrus::Script] script_pubkey The color id is generate from this script pubkey
48
+ # @param [String] address The address that is the token is sent to
49
+ # @param [Integer] value The issue amount of the token
50
+ # @param [Integer] split The number of the split outputs
51
+ def reissuable_split(script_pubkey, address, value, split)
52
+ split_value(value, split) do |value|
53
+ reissuable(script_pubkey, address, value)
54
+ end
55
+ end
56
+
57
+ # Issue non reissuable token
58
+ # @param out_point [Tapyrus::OutPoint] the out point at issue input.
59
+ # @param address [String] p2pkh or p2sh address.
60
+ # @param value [Integer] issued amount.
61
+ def non_reissuable(out_point, address, value)
62
+ color_id = Tapyrus::Color::ColorIdentifier.non_reissuable(out_point)
63
+ @issues[color_id] += value
64
+ super
65
+ end
66
+
67
+ # Issue non-reissuable token to the split outputs
68
+ # @param [Tapyrus::OutPoint] out_point The outpoint of the reissuable token
69
+ # @param [String] address The address that is the token is sent to
70
+ # @param [Integer] value The issue amount of the token
71
+ # @param [Integer] split The number of the split outputs
72
+ def non_reissuable_split(out_point, address, value, split)
73
+ split_value(value, split) do |value|
74
+ non_reissuable(out_point, address, value)
75
+ end
76
+ end
77
+
78
+ # Issue NFT
79
+ # @param out_point [Tapyrus::OutPoint] the out point at issue input.
80
+ # @param address [String] p2pkh or p2sh address.
81
+ # @raise [Glueby::ArgumentError] If the NFT is already issued in this contract builder.
82
+ def nft(out_point, address)
83
+ color_id = Tapyrus::Color::ColorIdentifier.nft(out_point)
84
+ raise Glueby::ArgumentError, 'NFT is already issued.' if @issues[color_id] == 1
85
+ @issues[color_id] = 1
86
+ super
87
+ end
88
+
89
+ # Burn token
90
+ # @param [Integer] value The burn amount of the token
91
+ # @param [Tapyrus::Color::ColorIdentifier] color_id The color id of the token
92
+ # @raise [Glueby::ArgumentError] If the color_id is default color id
93
+ def burn(value, color_id)
94
+ raise Glueby::ArgumentError, 'Burn TPC is not supported.' if color_id.default?
95
+
96
+ @burn_contract = true
97
+ @outgoings[color_id] ||= 0
98
+ @outgoings[color_id] += value
99
+ self
100
+ end
101
+
102
+ # Add utxo to the transaction
103
+ # @param [Hash] utxo The utxo to add
104
+ # @option utxo [String] :txid The txid
105
+ # @option utxo [Integer] :vout The index of the output in the tx
106
+ # @option utxo [Integer] :amount The value of the output
107
+ # @option utxo [String] :script_pubkey The hex string of the script pubkey
108
+ # @option utxo [String] :color_id The color id hex string of the output
109
+ def add_utxo(utxo)
110
+ super(to_tapyrusrb_utxo_hash(utxo))
111
+ self
112
+ end
113
+
114
+ # Add an UTXO which is sent to the address
115
+ # If the configuration is set to use UTXO provider, the UTXO is provided by the UTXO provider.
116
+ # Otherwise, the UTXO is provided by the wallet. In this case, the address parameter is ignored.
117
+ # This method creates and broadcasts a transaction that sends the amount to the address and add the UTXO
118
+ # to the transaction.
119
+ # @param [String] address The address that is the UTXO is sent to
120
+ # @param [Integer] amount The amount of the UTXO
121
+ # @param [Glueby::Internal::UtxoProvider] utxo_provider The UTXO provider
122
+ # @param [Boolean] only_finalized If true, the UTXO is provided from the finalized UTXO set. It is ignored if the configuration is set to use UTXO provider.
123
+ # @param [Glueby::Contract::FeeEstimator] fee_estimator It estimate fee for prev tx. It is ignored if the configuration is set to use UTXO provider.
124
+ def add_utxo_to!(
125
+ address:,
126
+ amount:,
127
+ utxo_provider: nil,
128
+ only_finalized: use_only_finalized_utxo,
129
+ fee_estimator: nil
130
+ )
131
+ tx, index = nil
132
+
133
+ if Glueby.configuration.use_utxo_provider? || utxo_provider
134
+ utxo_provider ||= UtxoProvider.instance
135
+ script_pubkey = Tapyrus::Script.parse_from_addr(address)
136
+ tx, index = utxo_provider.get_utxo(script_pubkey, amount)
137
+ else
138
+ fee_estimator ||= @fee_estimator
139
+ txb = Tapyrus::TxBuilder.new
140
+ fee = fee_estimator.fee(Contract::FeeEstimator.dummy_tx(txb.build))
141
+ _sum, utxos = sender_wallet
142
+ .collect_uncolored_outputs(fee + amount, nil, only_finalized)
143
+ utxos.each { |utxo| txb.add_utxo(to_tapyrusrb_utxo_hash(utxo)) }
144
+ tx = txb.pay(address, amount)
145
+ .change_address(sender_wallet.change_address)
146
+ .fee(fee)
147
+ .build
148
+ sender_wallet.sign_tx(tx)
149
+ index = 0
150
+ end
151
+
152
+ ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
153
+ # Here needs to use the return tx from Internal::Wallet#broadcast because the txid
154
+ # is changed if you enable FeeProvider.
155
+ tx = sender_wallet.broadcast(tx)
156
+ end
157
+
158
+ @prev_txs << tx
159
+
160
+ add_utxo({
161
+ script_pubkey: tx.outputs[index].script_pubkey.to_hex,
162
+ txid: tx.txid,
163
+ vout: index,
164
+ amount: tx.outputs[index].value
165
+ })
166
+ self
167
+ end
168
+
169
+ # Add an UTXO which is sent to the pay-to-contract address which is generated from the metadata
170
+ # @param [String] metadata The metadata of the pay-to-contract address
171
+ # @param [Integer] amount The amount of the UTXO
172
+ # @param [String] p2c_address The pay-to-contract address. You can use this parameter if you want to create an
173
+ # UTXO to before created address. It must be use with payment_base parameter.
174
+ # @param [String] payment_base The payment base of the pay-to-contract address. It should be compressed public
175
+ # key format. It must be use with p2c_address parameter.
176
+ # @param [Boolean] only_finalized If true, the UTXO is provided from the finalized UTXO set. It is ignored if the configuration is set to use UTXO provider.
177
+ # @param [Glueby::Contract::FeeEstimator] fee_estimator It estimate fee for prev tx. It is ignored if the configuration is set to use UTXO provider.
178
+ def add_p2c_utxo_to!(
179
+ metadata:,
180
+ amount:,
181
+ p2c_address: nil,
182
+ payment_base: nil,
183
+ only_finalized: use_only_finalized_utxo,
184
+ fee_estimator: nil
185
+ )
186
+ if p2c_address.nil? || payment_base.nil?
187
+ p2c_address, payment_base = sender_wallet
188
+ .create_pay_to_contract_address(metadata)
189
+ end
190
+
191
+ add_utxo_to!(
192
+ address: p2c_address,
193
+ amount: amount,
194
+ only_finalized: only_finalized,
195
+ fee_estimator: fee_estimator
196
+ )
197
+ @p2c_utxos << to_p2c_sign_tx_utxo_hash(@utxos.last)
198
+ .merge({
199
+ p2c_address: p2c_address,
200
+ payment_base: payment_base,
201
+ metadata: metadata
202
+ })
203
+ self
204
+ end
205
+
206
+ alias :original_build :build
207
+ def build
208
+ auto_fulfill_inputs_utxos_for_color if use_auto_fulfill_inputs
209
+
210
+ tx = Tapyrus::Tx.new
211
+ set_tpc_change_address
212
+ expand_input(tx)
213
+ @outputs.each { |output| tx.outputs << output }
214
+ add_change_for_colored_coin(tx)
215
+
216
+
217
+ change, provided_utxos = auto_fulfill_inputs_utxos_for_tpc(tx) if use_auto_fulfill_inputs
218
+ add_change_for_tpc(tx, change)
219
+
220
+ add_dummy_output(tx)
221
+ sign(tx, provided_utxos)
222
+ end
223
+
224
+ def change_address(address, color_id = Tapyrus::Color::ColorIdentifier.default)
225
+ if color_id.default?
226
+ super(address)
227
+ else
228
+ script_pubkey = Tapyrus::Script.parse_from_addr(address)
229
+ raise ArgumentError, 'invalid address' if !script_pubkey.p2pkh? && !script_pubkey.p2sh?
230
+ @change_script_pubkeys[color_id] = script_pubkey.add_color(color_id)
231
+ end
232
+ self
233
+ end
234
+
235
+ private :fee
236
+
237
+ def dummy_fee
238
+ fee_estimator.fee(Contract::FeeEstimator.dummy_tx(original_build))
239
+ end
240
+
241
+ private
242
+
243
+ def estimated_fee
244
+ @fee ||= estimate_fee
245
+ end
246
+
247
+ def estimate_fee
248
+ tx = Tapyrus::Tx.new
249
+ expand_input(tx)
250
+ @outputs.each { |output| tx.outputs << output }
251
+ add_change_for_colored_coin(tx)
252
+ fee_estimator.fee(Contract::FeeEstimator.dummy_tx(tx))
253
+ end
254
+
255
+ def sign(tx, extra_utxos)
256
+ utxos = @utxos.map { |u| to_sign_tx_utxo_hash(u) } + (extra_utxos || [])
257
+
258
+ # Sign inputs from sender_wallet
259
+ tx = sender_wallet.sign_tx(tx, utxos)
260
+
261
+ # Sign inputs which is pay to contract output
262
+ @p2c_utxos.each do |utxo|
263
+ tx = sender_wallet
264
+ .sign_to_pay_to_contract_address(tx, utxo, utxo[:payment_base], utxo[:metadata])
265
+ end
266
+
267
+ # Sign inputs from UtxoProvider
268
+ Glueby::UtxoProvider.instance.wallet.sign_tx(tx, utxos) if Glueby.configuration.use_utxo_provider?
269
+
270
+ tx
271
+ end
272
+
273
+ def set_tpc_change_address
274
+ if Glueby.configuration.use_utxo_provider?
275
+ change_address(UtxoProvider.instance.wallet.change_address)
276
+ else
277
+ change_address(@sender_wallet.change_address)
278
+ end
279
+ end
280
+
281
+ def add_change_for_colored_coin(tx)
282
+ @incomings.each do |color_id, in_amount|
283
+ next if color_id.default?
284
+
285
+ out_amount = @outgoings[color_id] || 0
286
+ change = in_amount - out_amount
287
+ next if change <= 0
288
+
289
+ unless @change_script_pubkeys[color_id]
290
+ raise Glueby::ArgumentError, "The change address for color_id #{color_id.to_hex} must be set."
291
+ end
292
+ tx.outputs << Tapyrus::TxOut.new(script_pubkey: @change_script_pubkeys[color_id], value: change)
293
+ end
294
+ tx
295
+ end
296
+
297
+ def add_change_for_tpc(tx, change = nil)
298
+ raise Glueby::ArgumentError, "The change address for TPC must be set." unless @change_script_pubkey
299
+ change ||= begin
300
+ in_amount = @incomings[Tapyrus::Color::ColorIdentifier.default] || 0
301
+ out_amount = @outgoings[Tapyrus::Color::ColorIdentifier.default] || 0
302
+
303
+ in_amount - out_amount - estimated_fee
304
+ end
305
+
306
+ raise Contract::Errors::InsufficientFunds if change < 0
307
+
308
+ change_output = Tapyrus::TxOut.new(script_pubkey: @change_script_pubkey, value: change)
309
+ return tx if change_output.dust?
310
+
311
+ tx.outputs << change_output
312
+
313
+ tx
314
+ end
315
+
316
+ # @return [Integer] The TPC change amount
317
+ # @return [Array<Hash>] The provided UTXOs
318
+ def auto_fulfill_inputs_utxos_for_tpc(tx)
319
+ target_amount = @outgoings[Tapyrus::Color::ColorIdentifier.default] || 0
320
+
321
+ provider = if Glueby.configuration.use_utxo_provider?
322
+ UtxoProvider.instance
323
+ else
324
+ sender_wallet
325
+ end
326
+
327
+ _tx, fee, tpc_amount, provided_utxos = provider.fill_uncolored_inputs(
328
+ tx,
329
+ target_amount: target_amount,
330
+ current_amount: @incomings[Tapyrus::Color::ColorIdentifier.default] || 0,
331
+ fee_estimator: fee_estimator
332
+ )
333
+
334
+ change = tpc_amount - target_amount - fee
335
+ [change, provided_utxos]
336
+ end
337
+
338
+ def auto_fulfill_inputs_utxos_for_color
339
+ # fulfill colored inputs
340
+ @outgoings.each do |color_id, outgoing_amount|
341
+ next if color_id.default?
342
+
343
+ target_amount = outgoing_amount - (@incomings[color_id] || 0) - @issues[color_id]
344
+ next if target_amount <= 0
345
+
346
+ @sender_wallet.collect_colored_outputs(
347
+ color_id,
348
+ target_amount,
349
+ nil,
350
+ use_only_finalized_utxo,
351
+ true
352
+ )[1]
353
+ .each { |utxo| add_utxo(utxo) }
354
+ end
355
+
356
+ end
357
+
358
+ def get_fee_estimator(fee_estimator_name)
359
+ Glueby::Contract::FeeEstimator.get_const("#{fee_estimator_name.capitalize}", false).new
360
+ end
361
+
362
+ def valid_fee_estimator?(fee_estimator)
363
+ [:fixed, :auto].include?(fee_estimator)
364
+ end
365
+
366
+ def use_only_finalized_utxo
367
+ !use_unfinalized_utxo
368
+ end
369
+
370
+ # Set fee estimator
371
+ # @param [Symbol|Glueby::Contract::FeeEstimator] fee_estimator :auto or :fixed
372
+ def set_fee_estimator(fee_estimator)
373
+ if fee_estimator.is_a?(Symbol)
374
+ raise Glueby::ArgumentError, 'fee_estiamtor can be :fixed or :auto' unless valid_fee_estimator?(fee_estimator)
375
+ @fee_estimator = get_fee_estimator(fee_estimator)
376
+ else
377
+ @fee_estimator = fee_estimator
378
+ end
379
+ self
380
+ end
381
+
382
+ # Split the value into the number of split. The last value is added the remainder of the division.
383
+ # It call the block with the value of each split.
384
+ def split_value(value, split)
385
+ if value < split
386
+ split = value
387
+ split_value = 1
388
+ else
389
+ split_value = (value / split).to_i
390
+ end
391
+ (split - 1).times { yield(split_value) }
392
+ yield(value - split_value * (split - 1))
393
+ self
394
+ end
395
+
396
+ # If the tx has no output due to the contract creating a burn token, a dummy output should be added to make
397
+ # the transaction valid.
398
+ def add_dummy_output(tx)
399
+ if @burn_contract && tx.outputs.size == 0
400
+
401
+ tx.outputs << Tapyrus::TxOut.new(
402
+ value: 0,
403
+ script_pubkey: Tapyrus::Script.new << Tapyrus::Opcodes::OP_RETURN
404
+ )
405
+ end
406
+ end
407
+
408
+ # The UTXO format that is used in Tapyrus::TxBuilder
409
+ # @param utxo
410
+ # @option utxo [String] :txid The txid
411
+ # @option utxo [Integer] :vout The index of the output in the tx
412
+ # @option utxo [Integer] :amount The value of the output
413
+ # @option utxo [String] :script_pubkey The hex string of the script pubkey
414
+ # @option utxo [String] :color_id The hex string of the color id
415
+ def to_tapyrusrb_utxo_hash(utxo)
416
+ color_id = if utxo[:color_id]
417
+ Tapyrus::Color::ColorIdentifier.parse_from_payload(utxo[:color_id].htb)
418
+ else
419
+ Tapyrus::Color::ColorIdentifier.default
420
+ end
421
+ {
422
+ script_pubkey: Tapyrus::Script.parse_from_payload(utxo[:script_pubkey].htb),
423
+ txid: utxo[:txid],
424
+ index: utxo[:vout],
425
+ value: utxo[:amount],
426
+ color_id: color_id
427
+ }
428
+ end
429
+
430
+ # The UTXO format that is used in AbstractWalletAdapter#sign_tx
431
+ # @param utxo The return value of #to_tapyrusrb_utxo_hash
432
+ # @option utxo [String] :txid The txid
433
+ # @option utxo [Integer] :index The index of the output in the tx
434
+ # @option utxo [Integer] :amount The value of the output
435
+ # @option utxo [String] :script_pubkey The hex string of the script pubkey
436
+ def to_sign_tx_utxo_hash(utxo)
437
+ {
438
+ scriptPubKey: utxo[:script_pubkey].to_hex,
439
+ txid: utxo[:txid],
440
+ vout: utxo[:index],
441
+ amount: utxo[:value]
442
+ }
443
+ end
444
+
445
+ # The UTXO format that is used in AbstractWalletAdapter#sign_to_pay_to_contract_address
446
+ # @param utxo The return value of #to_tapyrusrb_utxo_hash
447
+ # @option utxo [String] :txid The txid
448
+ # @option utxo [Integer] :index The index of the output in the tx
449
+ # @option utxo [Integer] :amount The value of the output
450
+ # @option utxo [String] :script_pubkey The hex string of the script pubkey
451
+ def to_p2c_sign_tx_utxo_hash(utxo)
452
+ {
453
+ script_pubkey: utxo[:script_pubkey].to_hex,
454
+ txid: utxo[:txid],
455
+ vout: utxo[:index],
456
+ amount: utxo[:value]
457
+ }
458
+ end
459
+ end
460
+ end
461
+ end
462
+
@@ -82,6 +82,8 @@ module Glueby
82
82
  # - vout: [Integer] Output index
83
83
  # - amount: [Integer] Amount of the UTXO as tapyrus unit
84
84
  # - finalized: [Boolean] Whether the UTXO is finalized
85
+ # - color_id: [String] Color id of the UTXO. If it is TPC UTXO, color_id is nil.
86
+ # - script_pubkey: [String] Script pubkey of the UTXO
85
87
  def list_unspent(wallet_id, only_finalized = true, label = nil)
86
88
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
87
89
  end
@@ -49,8 +49,9 @@ module Glueby
49
49
  private
50
50
 
51
51
  def check_dust_output
52
- if !color_id && value < DUST_LIMIT
53
- errors.add(:value, "is less than dust limit(#{DUST_LIMIT})")
52
+ output = Tapyrus::TxOut.new(value: value, script_pubkey: Tapyrus::Script.parse_from_payload(script_pubkey.htb))
53
+ if !color_id && output.dust?
54
+ errors.add(:value, "is less than dust limit(#{output.send(:dust_threshold)})")
54
55
  end
55
56
  end
56
57
  end
@@ -146,20 +146,74 @@ module Glueby
146
146
  wallet_adapter.create_pubkey(id)
147
147
  end
148
148
 
149
- def collect_uncolored_outputs(amount, label = nil, only_finalized = true, shuffle = false)
150
- utxos = list_unspent(only_finalized, label)
151
- utxos.shuffle! if shuffle
152
-
153
- utxos.inject([0, []]) do |sum, output|
154
- next sum if output[:color_id]
155
-
156
- new_sum = sum[0] + output[:amount]
157
- new_outputs = sum[1] << output
158
- return [new_sum, new_outputs] if new_sum >= amount
149
+ # Collect TPC UTXOs up to the specified amount.
150
+ # @param [Integer] amount The amount of colored token to collect. If the amount is nil, return all TPC UTXOs
151
+ # @param [String] label The label of UTXO to collect
152
+ # @param [Boolean] only_finalized The flag to collect only finalized UTXO
153
+ # @param [Boolean] shuffle The flag to shuffle UTXO before collecting
154
+ # @param [Boolean] lock_utxos The flag to lock returning UTXOs to prevent to be used from other threads or processes
155
+ # @param [Array<Hash] excludes The UTXO list to exclude the method result. Each hash must hub keys that are :txid and :vout
156
+ # @return [Integer] The sum of return UTXOs
157
+ # @return [Array<Hash>] An array of UTXO hash
158
+ #
159
+ # ## The UTXO structure
160
+ #
161
+ # - txid: [String] Transaction id
162
+ # - vout: [Integer] Output index
163
+ # - amount: [Integer] Amount of the UTXO as tapyrus unit
164
+ # - finalized: [Boolean] Whether the UTXO is finalized
165
+ # - script_pubkey: [String] ScriptPubkey of the UTXO
166
+ def collect_uncolored_outputs(
167
+ amount = nil,
168
+ label = nil,
169
+ only_finalized = true,
170
+ shuffle = false,
171
+ lock_utxos = false,
172
+ excludes = []
173
+ )
174
+ collect_utxos(amount, label, only_finalized, shuffle, lock_utxos, excludes) do |output|
175
+ next false unless output[:color_id].nil?
176
+ next yield(output) if block_given?
177
+
178
+ true
179
+ end
180
+ end
159
181
 
160
- [new_sum, new_outputs]
182
+ # Collect colored coin UTXOs up to the specified amount.
183
+ # @param [Tapyrus::Color::ColorIdentifier] color_id The color identifier of colored token
184
+ # @param [Integer] amount The amount of colored token to collect. If the amount is nil, return all UTXOs with color_id
185
+ # @param [String] label The label of UTXO to collect
186
+ # @param [Boolean] only_finalized The flag to collect only finalized UTXO
187
+ # @param [Boolean] shuffle The flag to shuffle UTXO before collecting
188
+ # @param [Boolean] lock_utxos The flag to lock returning UTXOs to prevent to be used from other threads or processes
189
+ # @param [Array<Hash] excludes The UTXO list to exclude the method result. Each hash must hub keys that are :txid and :vout
190
+ # @return [Integer] The sum of return UTXOs
191
+ # @return [Array<Hash>] An array of UTXO hash
192
+ #
193
+ # ## The UTXO structure
194
+ #
195
+ # - txid: [String] Transaction id
196
+ # - vout: [Integer] Output index
197
+ # - amount: [Integer] Amount of the UTXO as tapyrus unit
198
+ # - finalized: [Boolean] Whether the UTXO is finalized
199
+ # - script_pubkey: [String] ScriptPubkey of the UTXO
200
+ def collect_colored_outputs(
201
+ color_id,
202
+ amount = nil,
203
+ label = nil,
204
+ only_finalized = true,
205
+ shuffle = false,
206
+ lock_utxos = false,
207
+ excludes = []
208
+ )
209
+ collect_utxos(amount, label, only_finalized, shuffle, lock_utxos, excludes) do |output|
210
+ next false unless output[:color_id] == color_id.to_hex
211
+ next yield(output) if block_given?
212
+
213
+ true
161
214
  end
162
- raise Glueby::Contract::Errors::InsufficientFunds
215
+ rescue Glueby::Contract::Errors::InsufficientFunds
216
+ raise Glueby::Contract::Errors::InsufficientTokens
163
217
  end
164
218
 
165
219
  def get_addresses(label = nil)
@@ -182,11 +236,94 @@ module Glueby
182
236
  wallet_adapter.has_address?(id, address)
183
237
  end
184
238
 
239
+ # Fill inputs in the tx up to target_amount of TPC from UTXOs in the wallet.
240
+ # This method should be called before adding change output and script_sig in outputs.
241
+ # FeeEstimator.dummy_tx returns fee amount to the TX that will be added one TPC input, a change TPC output and
242
+ # script sigs in outputs.
243
+ # @param [Tapyrus::Tx] tx The tx that will be filled the inputs
244
+ # @param [Integer] target_amount The tapyrus amount the tx is expected to be added in this method
245
+ # @param [Integer] current_amount The tapyrus amount the tx already has in its inputs
246
+ # @param [Glueby::Contract::FeeEstimator] fee_estimator
247
+ # @return [Tapyrus::Tx] tx The tx that is added inputs
248
+ # @return [Integer] fee The final fee after the inputs are filled
249
+ # @return [Integer] current_amount The final amount of the tx inputs
250
+ # @return [Array<Hash>] provided_utxos The utxos that are added to the tx inputs
251
+ def fill_uncolored_inputs(
252
+ tx,
253
+ target_amount: ,
254
+ current_amount: 0,
255
+ fee_estimator: Contract::FeeEstimator::Fixed.new,
256
+ &block
257
+ )
258
+ fee = fee_estimator.fee(Contract::FeeEstimator.dummy_tx(tx, dummy_input_count: 0))
259
+ provided_utxos = []
260
+
261
+ while current_amount - fee < target_amount
262
+ sum, utxos = collect_uncolored_outputs(
263
+ fee + target_amount - current_amount,
264
+ nil, nil, true, true,
265
+ provided_utxos,
266
+ &block
267
+ )
268
+
269
+ utxos.each do |utxo|
270
+ tx.inputs << Tapyrus::TxIn.new(out_point: Tapyrus::OutPoint.from_txid(utxo[:txid], utxo[:vout]))
271
+ provided_utxos << utxo
272
+ end
273
+ current_amount += sum
274
+
275
+ new_fee = fee_estimator.fee(Contract::FeeEstimator.dummy_tx(tx, dummy_input_count: 0))
276
+ fee = new_fee
277
+ end
278
+
279
+ [tx, fee, current_amount, provided_utxos]
280
+ end
281
+
185
282
  private
186
283
 
187
284
  def wallet_adapter
188
285
  self.class.wallet_adapter
189
286
  end
287
+
288
+ def collect_utxos(
289
+ amount,
290
+ label,
291
+ only_finalized,
292
+ shuffle = true,
293
+ lock_utxos = false,
294
+ excludes = []
295
+ )
296
+ collect_all = amount.nil?
297
+
298
+ raise Glueby::ArgumentError, 'amount must be positive' unless collect_all || amount.positive?
299
+ utxos = list_unspent(only_finalized, label)
300
+ utxos = utxos.shuffle if shuffle
301
+
302
+ r = utxos.inject([0, []]) do |(sum, outputs), output|
303
+ if excludes.is_a?(Array) &&
304
+ !excludes.empty? &&
305
+ excludes.find { |i| i[:txid] == output[:txid] && i[:vout] == output[:vout] }
306
+ next [sum, outputs]
307
+ end
308
+
309
+ if block_given?
310
+ next [sum, outputs] unless yield(output)
311
+ end
312
+
313
+ if lock_utxos
314
+ next [sum, outputs] unless lock_unspent(output)
315
+ end
316
+
317
+ sum += output[:amount]
318
+ outputs << output
319
+ return [sum, outputs] unless collect_all || sum < amount
320
+
321
+ [sum, outputs]
322
+ end
323
+ raise Glueby::Contract::Errors::InsufficientFunds unless collect_all
324
+
325
+ r
326
+ end
190
327
  end
191
328
  end
192
329
  end
@@ -2,5 +2,6 @@ module Glueby
2
2
  module Internal
3
3
  autoload :Wallet, 'glueby/internal/wallet'
4
4
  autoload :RPC, 'glueby/internal/rpc'
5
+ autoload :ContractBuilder, 'glueby/internal/contract_builder'
5
6
  end
6
7
  end
@@ -1,8 +1,6 @@
1
1
  module Glueby
2
2
  class UtxoProvider
3
3
  class Tasks
4
- include Glueby::Contract::TxBuilder
5
-
6
4
  attr_reader :utxo_provider
7
5
 
8
6
  STATUS = {