Class: BTC::AssetTransactionBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/btcruby/open_assets/asset_transaction_builder.rb,
lib/btcruby/open_assets/asset_transaction_builder/errors.rb,
lib/btcruby/open_assets/asset_transaction_builder/result.rb,
lib/btcruby/open_assets/asset_transaction_builder/provider.rb

Overview

TODO:

  • provide Asset ID(s) as input, or raw AssetTransactionInput objects
  • provide raw unspents to pay mining fees
  • provide asset change address
  • provide btc change address
  • provide issuance API, transfer API, payment API (plain btc outputs) Use TransactionBuilder internally.

Defined Under Namespace

Modules: Provider Classes: Error, InsufficientFundsError, MissingChangeAddressError, MissingUnspentOutputsError, Result

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeAssetTransactionBuilder

Returns a new instance of AssetTransactionBuilder.



51
52
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 51

def initialize
end

Instance Attribute Details

#asset_change_addressObject

Must be a subclass of a BTC::AssetAddress



23
24
25
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 23

def asset_change_address
  @asset_change_address
end

#asset_providerObject

Provider of the pure bitcoin unspent outputs adopting AssetTransactionBuilder::Provider. If not specified, asset_unspent_outputs must be provided.



38
39
40
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 38

def asset_provider
  @asset_provider
end

#asset_unspent_outputsObject

Enumerable yielding BTC::AssetTransactionOutput instances with valid transaction_hash and index properties.



30
31
32
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 30

def asset_unspent_outputs
  @asset_unspent_outputs
end

#bitcoin_change_addressObject

Must be a subclass of a BTC::BitcoinPaymentAddress



20
21
22
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 20

def bitcoin_change_address
  @bitcoin_change_address
end

#bitcoin_providerObject

Provider of the pure bitcoin unspent outputs adopting TransactionBuilder::Provider. If not specified, bitcoin_unspent_outputs must be provided.



34
35
36
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 34

def bitcoin_provider
  @bitcoin_provider
end

#bitcoin_unspent_outputsObject

Enumerable yielding BTC::TransactionOutput instances with valid transaction_hash and index properties. If not specified, bitcoin_unspent_outputs_provider is used if possible.



27
28
29
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 27

def bitcoin_unspent_outputs
  @bitcoin_unspent_outputs
end

#fee_rateObject

Miner's fee per kilobyte (1000 bytes). Default is Transaction::DEFAULT_FEE_RATE



46
47
48
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 46

def fee_rate
  @fee_rate
end

#metadataObject

Metadata to embed in the marker output. Default is nil (empty string).



49
50
51
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 49

def 
  
end

#networkObject

Network to validate provided addresses against. Default value is Network.default.



17
18
19
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 17

def network
  @network
end

#signerObject

TransactionBuilder::Signer for all the inputs (bitcoins and assets). If not provided, inputs will be left unsigned and result.unsigned_input_indexes will contain indexes of these inputs.



42
43
44
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 42

def signer
  @signer
end

Instance Method Details

#buildObject



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 137

def build

  validate_bitcoin_change_address!
  validate_asset_change_address!

  result = Result.new

  # We don't count assets_cost because outputs of these txs
  # will be consumed in the next transaction. Only add the fees.
  if self.issuing_asset_script
    issuing_tx, unsigned_input_indexes = make_transaction_for_issues
    result.fee = issuing_tx.fee
    result.transactions << issuing_tx
    result.unsigned_input_indexes << unsigned_input_indexes
    self.issuing_asset_output = AssetTransactionOutput.new(transaction_output: issuing_tx.outputs.first, verified: true)
  end

  # Prepare the target transaction
  txbuilder = make_transaction_builder
  if result.transactions.size > 0
    txbuilder.parent_transactions = [result.transactions.last]
  end
  txbuilder.prepended_unspent_outputs ||= []
  txbuilder.outputs = []

  # Add issuance input and outputs first.

  consumed_asset_outputs = [] # used in inputs
  issue_outputs = []
  transfer_outputs = []

  if self.issued_assets.size > 0
    txbuilder.prepended_unspent_outputs << self.issuing_asset_output.transaction_output
    self.issued_assets.each do |issue|
      atxo = make_asset_transaction_output(asset_id: issuing_asset_id, amount: issue[:amount], script: issue[:script])
      issue_outputs << atxo
    end
  end

  # Move all transfers of the asset ID used on the issuing output to the top of the list.
  asset_id_on_the_issuing_input = self.issuing_asset_output ? self.issuing_asset_output.asset_id : nil
  if asset_id_on_the_issuing_input
    aid = asset_id_on_the_issuing_input.to_s
    self.transferred_assets = self.transferred_assets.sort do |a,b|
      # move the asset id used in the issue input to the top
      if a[:asset_id].to_s == aid
        -1
      elsif b[:asset_id] == aid
        1
      else
        0 # keep the order
      end
    end
  end

  consumed_outpoints = {} # "txid:index" => true

  self.transferred_assets.each do |transfer| # |aid,transfers|
    asset_id = transfer[:asset_id]
    transfers = [transfer]
    amount_required = 0
    amount_provided = 0
    if asset_id_on_the_issuing_input.to_s == asset_id.to_s
      amount_provided = self.issuing_asset_output.value
    end

    atxo = make_asset_transaction_output(asset_id: transfer[:asset_id],
                                         amount: transfer[:amount],
                                         script: transfer[:script])
    amount_required += atxo.value
    transfer_outputs << atxo

    # Fill in enough unspent assets for this asset_id.
    # Use per-transfer provider if it's specified. Otherwise use global provider.
    provider = transfer[:provider] || self.asset_provider
    unspents = provider.asset_unspent_outputs(asset_id: asset_id, amount: amount_required).dup

    while amount_provided < amount_required
      autxo = unspents.shift
      if !autxo
        raise InsufficientFundsError, "Not enough outputs for asset #{asset_id.to_s} (#{amount_provided} available < #{amount_required} required)"
      end
      if !consumed_outpoints[oid = autxo.transaction_output.outpoint_id]
        # Only apply outputs with matching asset ids.
        if autxo.asset_id == asset_id
          raise ArgumentError, "Must be verified asset outputs to spend" if !autxo.verified?
          consumed_outpoints[oid] = true
          amount_provided += autxo.value
          consumed_asset_outputs << autxo
          txbuilder.prepended_unspent_outputs << autxo.transaction_output
        end
      end
    end

    # If the difference is > 0, add a change output
    change = amount_provided - amount_required
    if change > 0
      # Use per-transfer change address if it's specified. Otherwise use global change address.
      change_addr = transfer[:change_address] || self.asset_change_address
      atxo = make_asset_transaction_output(asset_id: asset_id,
                                           amount: change,
                                           script: change_addr.script)
      transfer_outputs << atxo
    end

  end # each transfer

  # If we have an asset on the issuance input and it is never used in any transfer,
  # then we need to create a change output just for it.
  if asset_id_on_the_issuing_input && self.issuing_asset_output.value > 0
    if !self.transferred_assets.map{|dict| dict[:asset_id].to_s }.uniq.include?(asset_id_on_the_issuing_input.to_s)
      atxo = make_asset_transaction_output(asset_id: self.issuing_asset_output.asset_id,
                                           amount: self.issuing_asset_output.value,
                                           script: self.asset_change_address.script)
      transfer_outputs << atxo
    end
  end

  all_asset_outputs = (issue_outputs + transfer_outputs)
  result.assets_cost = all_asset_outputs.inject(0){|sum, atxo| sum + atxo.transaction_output.value }
  marker = AssetMarker.new(quantities: all_asset_outputs.map{|atxo| atxo.value }, metadata: self.)

  # Now, add underlying issues, marker and transfer outputs
  issue_outputs.each do |atxo|
    txbuilder.outputs << atxo.transaction_output
  end
  txbuilder.outputs << marker.output
  transfer_outputs.each do |atxo|
    txbuilder.outputs << atxo.transaction_output
  end

  txresult = txbuilder.build
  tx = txresult.transaction
  atx = AssetTransaction.new(transaction: tx)

  BTC::Invariant(atx.outputs.size == all_asset_outputs.size + 1 + (txresult.change_amount > 0 ? 1 : 0),
    "Must have all asset outputs (with marker output and optional change output)");

  if txresult.change_amount > 0
    plain_change_output = atx.outputs.last
    BTC::Invariant(!plain_change_output.verified?, "Must have plain change output not verified");
    BTC::Invariant(!plain_change_output.asset_id, "Must have plain change output not have asset id");
    BTC::Invariant(!plain_change_output.value, "Must have plain change output not have asset amount");
    plain_change_output.verified = true # to match the rest of outputs.
  end

  # Provide color info for each input
  consumed_asset_outputs.each_with_index do |atxo, i|
    atx.inputs[i].asset_id = atxo.asset_id
    atx.inputs[i].value = atxo.value
    atx.inputs[i].verified = true
  end
  atx.inputs[consumed_asset_outputs.size..-1].each do |input|
    input.asset_id = nil
    input.value = nil
    input.verified = true
  end

  # Provide color info for each output
  issue_outputs.each_with_index do |aout1, i|
    aout = atx.outputs[i]
    aout.asset_id = aout1.asset_id
    aout.value = aout1.value
    aout.verified = true
  end
  atx.outputs[issue_outputs.size].verified = true # make marker verified
  offset = 1 + issue_outputs.size # +1 for marker
  transfer_outputs.each_with_index do |aout1, i|
    aout = atx.outputs[i + offset]
    aout.asset_id = aout1.asset_id
    aout.value = aout1.value
    aout.verified = true
  end
  offset = 1 + issue_outputs.size + transfer_outputs.size # +1 for marker
  atx.outputs[offset..-1].each do |aout|
    aout.asset_id = nil
    aout.value = nil
    aout.verified = true
  end

  if txresult.change_amount == 0
    atx.outputs.each do |aout|
      if !aout.marker?
        BTC::Invariant(aout.verified?, "Must be verified");
        BTC::Invariant(!!aout.asset_id, "Must have asset id");
        BTC::Invariant(aout.value && aout.value > 0, "Must have some asset amount");
      end
    end
  end

  result.unsigned_input_indexes << txresult.unsigned_input_indexes
  result.transactions << tx
  result.asset_transaction = atx

  result
end

#internal_bitcoin_providerObject



372
373
374
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 372

def internal_bitcoin_provider
  @internal_bitcoin_provider ||= (self.bitcoin_provider || TransactionBuilder::Provider.new{|txb| []})
end

#issue_asset(source_script: nil, source_output: nil, amount: nil, script: nil, address: nil) ⇒ Object

Adds an issuance of some assets. If script is specified, it is used to create an intermediate base transaction. If output is specified, it must be a valid spendable output with transaction_id and index. It can be regular TransactionOutput or verified AssetTransactionOutput. amount must be > 0 - number of units to be issued

Raises:

  • (ArgumentError)


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 63

def issue_asset(source_script: nil, source_output: nil,
                amount: nil,
                script: nil, address: nil)
  raise ArgumentError, "Either `source_script` or `source_output` must be specified" if !source_script && !source_output
  raise ArgumentError, "Both `source_script` and `source_output` cannot be specified" if source_script && source_output
  raise ArgumentError, "Either `script` or `address` must be specified" if !script && !address
  raise ArgumentError, "Both `script` and `address` cannot be specified" if script && address
  raise ArgumentError, "Amount must be greater than zero" if !amount || amount <= 0
  if source_output && (!source_output.index || !source_output.transaction_hash)
    raise ArgumentError, "If `source_output` is specified, it must have valid `transaction_hash` and `index` attributes"
  end
  script ||= AssetAddress.parse(address).script

  # Ensure source output is a verified asset output.
  if source_output
    if source_output.is_a?(AssetTransactionOutput)
      raise ArgumentError, "Must be verified asset output to spend" if !source_output.verified?
    else
      source_output = AssetTransactionOutput.new(transaction_output: source_output, verified: true)
    end
  end

  # Set either the script or output only once.
  # All the remaining issuances must use the same script or output.
  if !self.issuing_asset_script && !self.issuing_asset_output
    self.issuing_asset_script = source_script
    self.issuing_asset_output = source_output
  else
    if self.issuing_asset_script != source_script || self.issuing_asset_output != source_output
      raise ArgumentError, "Can't issue more assets from a different source script or source output"
    end
  end
  self.issued_assets << {amount: amount, script: script}
end

#issuing_asset_idObject

Helpers



336
337
338
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 336

def issuing_asset_id
  @issuing_asset_id ||= AssetID.new(script: @issuing_asset_script || @issuing_asset_output.transaction_output.script)
end

#make_asset_transaction_output(asset_id: nil, amount: nil, script: nil) ⇒ Object



340
341
342
343
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 340

def make_asset_transaction_output(asset_id: nil, amount: nil, script: nil)
  txout = make_output_for_asset_script(script)
  AssetTransactionOutput.new(transaction_output: txout, asset_id: asset_id, value: amount, verified: true)
end

#make_output_for_asset_script(script) ⇒ Object

Raises:

  • (RuntimeError)


355
356
357
358
359
360
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 355

def make_output_for_asset_script(script)
  txout = BTC::TransactionOutput.new(value: MAX_MONEY, script: script)
  txout.value = txout.dust_limit
  raise RuntimeError, "Sanity check: txout value must not be zero" if txout.value <= 0
  txout
end

#make_transaction_builderObject



362
363
364
365
366
367
368
369
370
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 362

def make_transaction_builder
  txbuilder = TransactionBuilder.new
  txbuilder.change_address = self.bitcoin_change_address
  txbuilder.signer = self.signer
  txbuilder.provider = self.internal_bitcoin_provider
  txbuilder.unspent_outputs = self.bitcoin_unspent_outputs
  txbuilder.fee_rate = self.fee_rate
  txbuilder
end

#make_transaction_for_issuesObject



345
346
347
348
349
350
351
352
353
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 345

def make_transaction_for_issues
  raise "Sanity check" if !self.issuing_asset_script
  txbuilder = make_transaction_builder
  txbuilder.outputs = [
    make_output_for_asset_script(self.issuing_asset_script)
  ]
  result = txbuilder.build
  [result.transaction, result.unsigned_input_indexes]
end

#send_bitcoin(output: nil, amount: nil, script: nil, address: nil) ⇒ Object

Adds a normal payment output. Typically used for transfer



119
120
121
122
123
124
125
126
127
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 119

def send_bitcoin(output: nil, amount: nil, script: nil, address: nil)
  if !output
    raise ArgumentError, "Either `script` or `address` must be specified" if !script && !address
    raise ArgumentError, "Amount must be specified (>= 0)" if (!amount || amount < 0)
    script ||= address.public_address.script if address
    output = TransactionOutput.new(value: amount, script: script)
  end
  self.bitcoin_outputs << output
end

#transfer_asset(asset_id: nil, amount: nil, script: nil, address: nil, provider: nil, unspent_outputs: nil, change_address: nil) ⇒ Object

Adds a transfer output. May override per-builder unspents/provider/change address to allow multi-user swaps.

Raises:

  • (ArgumentError)


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 100

def transfer_asset(asset_id: nil,
                   amount: nil,
                   script: nil, address: nil,
                   provider: nil, unspent_outputs: nil, change_address: nil)
  raise ArgumentError, "AssetID must be provided" if !asset_id
  raise ArgumentError, "Either `script` or `address` must be specified" if !script && !address
  raise ArgumentError, "Both `script` and `address` cannot be specified" if script && address
  raise ArgumentError, "Amount must be greater than zero" if !amount || amount <= 0

  provider = Provider.new{|atxbuilder| unspent_outputs } if unspent_outputs
  change_address = AssetAddress.parse(change_address) if change_address

  asset_id = AssetID.parse(asset_id)
  script ||= AssetAddress.parse(address).script

  self.transferred_assets << {asset_id: asset_id, amount: amount, script: script, provider: provider, change_address: change_address}
end

#validate_asset_change_address!Object



386
387
388
389
390
391
392
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 386

def validate_asset_change_address!
  self.transferred_assets.each do |dict|
    addr = dict[:change_address] || self.asset_change_address
    raise ArgumentError, "Missing asset_change_address" if !addr
    raise ArgumentError, "asset_change_address must be an instance of BTC::AssetAddress" if !addr.is_a?(AssetAddress)
  end
end

#validate_bitcoin_change_address!Object

Validation Methods

Raises:

  • (ArgumentError)


379
380
381
382
383
384
# File 'lib/btcruby/open_assets/asset_transaction_builder.rb', line 379

def validate_bitcoin_change_address!
  addr = self.bitcoin_change_address
  raise ArgumentError, "Missing bitcoin_change_address" if !addr
  raise ArgumentError, "bitcoin_change_address must be an instance of BTC::Address" if !addr.is_a?(Address)
  raise ArgumentError, "bitcoin_change_address must not be an instance of BTC::AssetAddress" if addr.is_a?(AssetAddress)
end