glueby 1.1.1 → 1.2.0.beta.1

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
@@ -135,6 +137,15 @@ module Glueby
135
137
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
136
138
  end
137
139
 
140
+ # Returns information for the addresses
141
+ #
142
+ # @param [String] address - The p2pkh address to get information about
143
+ # @return [Array<Hash>] The array of hash instance which has keys wallet_id, label and purpose.
144
+ # Returns blank array if the key correspond with the address is not exist.
145
+ def get_addresses_info(addresses)
146
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
147
+ end
148
+
138
149
  # Returns a new public key.
139
150
  #
140
151
  # This method is expected to returns a new public key. The key would generate internally. This key is provided
@@ -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
@@ -153,6 +153,36 @@ module Glueby
153
153
  keys.map(&:address)
154
154
  end
155
155
 
156
+ def get_addresses_info(addresses)
157
+ unless addresses.is_a?(Array)
158
+ addresses = [addresses]
159
+ end
160
+
161
+ script_pubkeys = addresses.map do |address|
162
+ Tapyrus::Script.parse_from_addr(address).to_hex
163
+ rescue ::ArgumentError => e
164
+ raise Glueby::ArgumentError, "\"#{address}\" is invalid address. #{e.message}"
165
+ rescue RuntimeError => e
166
+ if e.message == 'Invalid version bytes.' || e.message == 'Invalid address.'
167
+ raise Glueby::ArgumentError, "\"#{address}\" is invalid address. #{e.message}"
168
+ else
169
+ raise e
170
+ end
171
+ end
172
+
173
+ keys = AR::Key.where(script_pubkey: script_pubkeys)
174
+ keys.map do |key|
175
+ {
176
+ address: key.address,
177
+ public_key: key.public_key,
178
+ wallet_id: key.wallet.wallet_id,
179
+ label: key.label,
180
+ purpose: key.purpose,
181
+ script_pubkey: key.script_pubkey
182
+ }
183
+ end
184
+ end
185
+
156
186
  def create_pay_to_contract_address(wallet_id, contents)
157
187
  pubkey = create_pubkey(wallet_id)
158
188
  [pay_to_contract_key(wallet_id, pubkey, contents).to_p2pkh, pubkey.pubkey]