glueby 1.1.1 → 1.2.0.beta.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,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]