btcruby 0.0.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +18 -0
- data/.travis.yml +7 -0
- data/FAQ.md +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +18 -0
- data/HOWTO.md +17 -0
- data/LICENSE +19 -0
- data/README.md +59 -0
- data/Rakefile +6 -0
- data/TODO.txt +40 -0
- data/bin/console +19 -0
- data/btcruby.gemspec +20 -0
- data/documentation/address.md +73 -0
- data/documentation/base58.md +52 -0
- data/documentation/block.md +127 -0
- data/documentation/block_header.md +120 -0
- data/documentation/constants.md +88 -0
- data/documentation/data.md +54 -0
- data/documentation/diagnostics.md +90 -0
- data/documentation/extensions.md +76 -0
- data/documentation/hash_functions.md +58 -0
- data/documentation/hash_id.md +22 -0
- data/documentation/index.md +230 -0
- data/documentation/key.md +177 -0
- data/documentation/keychain.md +180 -0
- data/documentation/network.md +75 -0
- data/documentation/opcode.md +220 -0
- data/documentation/openssl.md +7 -0
- data/documentation/p2pkh.md +71 -0
- data/documentation/p2sh.md +64 -0
- data/documentation/proof_of_work.md +84 -0
- data/documentation/script.md +280 -0
- data/documentation/signature.md +71 -0
- data/documentation/transaction.md +213 -0
- data/documentation/transaction_builder.md +188 -0
- data/documentation/transaction_input.md +133 -0
- data/documentation/transaction_output.md +130 -0
- data/documentation/wif.md +72 -0
- data/documentation/wire_format.md +70 -0
- data/lib/btcruby/address.rb +296 -0
- data/lib/btcruby/base58.rb +108 -0
- data/lib/btcruby/big_number.rb +47 -0
- data/lib/btcruby/block.rb +170 -0
- data/lib/btcruby/block_header.rb +231 -0
- data/lib/btcruby/constants.rb +59 -0
- data/lib/btcruby/currency_formatter.rb +64 -0
- data/lib/btcruby/data.rb +98 -0
- data/lib/btcruby/diagnostics.rb +92 -0
- data/lib/btcruby/errors.rb +8 -0
- data/lib/btcruby/extensions.rb +65 -0
- data/lib/btcruby/hash_functions.rb +54 -0
- data/lib/btcruby/hash_id.rb +18 -0
- data/lib/btcruby/key.rb +517 -0
- data/lib/btcruby/keychain.rb +464 -0
- data/lib/btcruby/network.rb +73 -0
- data/lib/btcruby/opcode.rb +197 -0
- data/lib/btcruby/open_assets/asset.rb +35 -0
- data/lib/btcruby/open_assets/asset_address.rb +49 -0
- data/lib/btcruby/open_assets/asset_definition.rb +75 -0
- data/lib/btcruby/open_assets/asset_id.rb +24 -0
- data/lib/btcruby/open_assets/asset_marker.rb +94 -0
- data/lib/btcruby/open_assets/asset_processor.rb +377 -0
- data/lib/btcruby/open_assets/asset_transaction.rb +184 -0
- data/lib/btcruby/open_assets/asset_transaction_builder/errors.rb +15 -0
- data/lib/btcruby/open_assets/asset_transaction_builder/provider.rb +32 -0
- data/lib/btcruby/open_assets/asset_transaction_builder/result.rb +47 -0
- data/lib/btcruby/open_assets/asset_transaction_builder.rb +418 -0
- data/lib/btcruby/open_assets/asset_transaction_input.rb +64 -0
- data/lib/btcruby/open_assets/asset_transaction_output.rb +140 -0
- data/lib/btcruby/open_assets.rb +26 -0
- data/lib/btcruby/openssl.rb +536 -0
- data/lib/btcruby/proof_of_work.rb +110 -0
- data/lib/btcruby/safety.rb +26 -0
- data/lib/btcruby/script.rb +733 -0
- data/lib/btcruby/signature_hashtype.rb +37 -0
- data/lib/btcruby/transaction.rb +511 -0
- data/lib/btcruby/transaction_builder/errors.rb +15 -0
- data/lib/btcruby/transaction_builder/provider.rb +54 -0
- data/lib/btcruby/transaction_builder/result.rb +73 -0
- data/lib/btcruby/transaction_builder/signer.rb +28 -0
- data/lib/btcruby/transaction_builder.rb +520 -0
- data/lib/btcruby/transaction_input.rb +298 -0
- data/lib/btcruby/transaction_outpoint.rb +30 -0
- data/lib/btcruby/transaction_output.rb +315 -0
- data/lib/btcruby/version.rb +3 -0
- data/lib/btcruby/wif.rb +118 -0
- data/lib/btcruby/wire_format.rb +362 -0
- data/lib/btcruby.rb +44 -2
- data/sample_code/creating_a_p2sh_multisig_address.rb +21 -0
- data/sample_code/creating_a_transaction_manually.rb +44 -0
- data/sample_code/generating_an_address.rb +20 -0
- data/sample_code/using_transaction_builder.rb +49 -0
- data/spec/address_spec.rb +206 -0
- data/spec/all.rb +6 -0
- data/spec/base58_spec.rb +83 -0
- data/spec/block_header_spec.rb +18 -0
- data/spec/block_spec.rb +18 -0
- data/spec/currency_formatter_spec.rb +46 -0
- data/spec/data_spec.rb +50 -0
- data/spec/diagnostics_spec.rb +41 -0
- data/spec/key_spec.rb +205 -0
- data/spec/keychain_spec.rb +261 -0
- data/spec/network_spec.rb +48 -0
- data/spec/open_assets/asset_address_spec.rb +33 -0
- data/spec/open_assets/asset_id_spec.rb +15 -0
- data/spec/open_assets/asset_marker_spec.rb +47 -0
- data/spec/open_assets/asset_processor_spec.rb +567 -0
- data/spec/open_assets/asset_transaction_builder_spec.rb +273 -0
- data/spec/open_assets/asset_transaction_spec.rb +70 -0
- data/spec/proof_of_work_spec.rb +53 -0
- data/spec/script_spec.rb +66 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/transaction_builder_spec.rb +338 -0
- data/spec/transaction_spec.rb +162 -0
- data/spec/wire_format_spec.rb +283 -0
- metadata +141 -7
@@ -0,0 +1,418 @@
|
|
1
|
+
# Note: additional files are required in the bottom of the file.
|
2
|
+
|
3
|
+
# High-level Transaction Builder for OpenAssets protocol.
|
4
|
+
# https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki
|
5
|
+
module BTC
|
6
|
+
# TODO:
|
7
|
+
# - provide Asset ID(s) as input, or raw AssetTransactionInput objects
|
8
|
+
# - provide raw unspents to pay mining fees
|
9
|
+
# - provide asset change address
|
10
|
+
# - provide btc change address
|
11
|
+
# - provide issuance API, transfer API, payment API (plain btc outputs)
|
12
|
+
# Use TransactionBuilder internally.
|
13
|
+
class AssetTransactionBuilder
|
14
|
+
|
15
|
+
# Network to validate provided addresses against.
|
16
|
+
# Default value is `Network.default`.
|
17
|
+
attr_accessor :network
|
18
|
+
|
19
|
+
# Must be a subclass of a BTC::BitcoinPaymentAddress
|
20
|
+
attr_accessor :bitcoin_change_address
|
21
|
+
|
22
|
+
# Must be a subclass of a BTC::AssetAddress
|
23
|
+
attr_accessor :asset_change_address
|
24
|
+
|
25
|
+
# Enumerable yielding BTC::TransactionOutput instances with valid `transaction_hash` and `index` properties.
|
26
|
+
# If not specified, `bitcoin_unspent_outputs_provider` is used if possible.
|
27
|
+
attr_accessor :bitcoin_unspent_outputs
|
28
|
+
|
29
|
+
# Enumerable yielding BTC::AssetTransactionOutput instances with valid `transaction_hash` and `index` properties.
|
30
|
+
attr_accessor :asset_unspent_outputs
|
31
|
+
|
32
|
+
# Provider of the pure bitcoin unspent outputs adopting TransactionBuilder::Provider.
|
33
|
+
# If not specified, `bitcoin_unspent_outputs` must be provided.
|
34
|
+
attr_accessor :bitcoin_provider
|
35
|
+
|
36
|
+
# Provider of the pure bitcoin unspent outputs adopting AssetTransactionBuilder::Provider.
|
37
|
+
# If not specified, `asset_unspent_outputs` must be provided.
|
38
|
+
attr_accessor :asset_provider
|
39
|
+
|
40
|
+
# TransactionBuilder::Signer for all the inputs (bitcoins and assets).
|
41
|
+
# If not provided, inputs will be left unsigned and `result.unsigned_input_indexes` will contain indexes of these inputs.
|
42
|
+
attr_accessor :signer
|
43
|
+
|
44
|
+
# Miner's fee per kilobyte (1000 bytes).
|
45
|
+
# Default is Transaction::DEFAULT_FEE_RATE
|
46
|
+
attr_accessor :fee_rate
|
47
|
+
|
48
|
+
# Metadata to embed in the marker output. Default is nil (empty string).
|
49
|
+
attr_accessor :metadata
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
end
|
53
|
+
|
54
|
+
def asset_unspent_outputs=(unspents)
|
55
|
+
self.asset_provider = Provider.new{|atxbuilder| unspents }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Adds an issuance of some assets.
|
59
|
+
# If `script` is specified, it is used to create an intermediate base transaction.
|
60
|
+
# If `output` is specified, it must be a valid spendable output with `transaction_id` and `index`.
|
61
|
+
# It can be regular TransactionOutput or verified AssetTransactionOutput.
|
62
|
+
# `amount` must be > 0 - number of units to be issued
|
63
|
+
def issue_asset(source_script: nil, source_output: nil,
|
64
|
+
amount: nil,
|
65
|
+
script: nil, address: nil)
|
66
|
+
raise ArgumentError, "Either `source_script` or `source_output` must be specified" if !source_script && !source_output
|
67
|
+
raise ArgumentError, "Both `source_script` and `source_output` cannot be specified" if source_script && source_output
|
68
|
+
raise ArgumentError, "Either `script` or `address` must be specified" if !script && !address
|
69
|
+
raise ArgumentError, "Both `script` and `address` cannot be specified" if script && address
|
70
|
+
raise ArgumentError, "Amount must be greater than zero" if !amount || amount <= 0
|
71
|
+
if source_output && (!source_output.index || !source_output.transaction_hash)
|
72
|
+
raise ArgumentError, "If `source_output` is specified, it must have valid `transaction_hash` and `index` attributes"
|
73
|
+
end
|
74
|
+
script ||= AssetAddress.parse(address).script
|
75
|
+
|
76
|
+
# Ensure source output is a verified asset output.
|
77
|
+
if source_output
|
78
|
+
if source_output.is_a?(AssetTransactionOutput)
|
79
|
+
raise ArgumentError, "Must be verified asset output to spend" if !source_output.verified?
|
80
|
+
else
|
81
|
+
source_output = AssetTransactionOutput.new(transaction_output: source_output, verified: true)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Set either the script or output only once.
|
86
|
+
# All the remaining issuances must use the same script or output.
|
87
|
+
if !self.issuing_asset_script && !self.issuing_asset_output
|
88
|
+
self.issuing_asset_script = source_script
|
89
|
+
self.issuing_asset_output = source_output
|
90
|
+
else
|
91
|
+
if self.issuing_asset_script != source_script || self.issuing_asset_output != source_output
|
92
|
+
raise ArgumentError, "Can't issue more assets from a different source script or source output"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
self.issued_assets << {amount: amount, script: script}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Adds a transfer output.
|
99
|
+
# May override per-builder unspents/provider/change address to allow multi-user swaps.
|
100
|
+
def transfer_asset(asset_id: nil,
|
101
|
+
amount: nil,
|
102
|
+
script: nil, address: nil,
|
103
|
+
provider: nil, unspent_outputs: nil, change_address: nil)
|
104
|
+
raise ArgumentError, "AssetID must be provided" if !asset_id
|
105
|
+
raise ArgumentError, "Either `script` or `address` must be specified" if !script && !address
|
106
|
+
raise ArgumentError, "Both `script` and `address` cannot be specified" if script && address
|
107
|
+
raise ArgumentError, "Amount must be greater than zero" if !amount || amount <= 0
|
108
|
+
|
109
|
+
provider = Provider.new{|atxbuilder| unspent_outputs } if unspent_outputs
|
110
|
+
change_address = AssetAddress.parse(change_address) if change_address
|
111
|
+
|
112
|
+
asset_id = AssetID.parse(asset_id)
|
113
|
+
script ||= AssetAddress.parse(address).script
|
114
|
+
|
115
|
+
self.transferred_assets << {asset_id: asset_id, amount: amount, script: script, provider: provider, change_address: change_address}
|
116
|
+
end
|
117
|
+
|
118
|
+
# Adds a normal payment output. Typically used for transfer
|
119
|
+
def send_bitcoin(output: nil, amount: nil, script: nil, address: nil)
|
120
|
+
if !output
|
121
|
+
raise ArgumentError, "Either `script` or `address` must be specified" if !script && !address
|
122
|
+
raise ArgumentError, "Amount must be specified (>= 0)" if (!amount || amount < 0)
|
123
|
+
script ||= address.public_address.script if address
|
124
|
+
output = TransactionOutput.new(value: amount, script: script)
|
125
|
+
end
|
126
|
+
self.bitcoin_outputs << output
|
127
|
+
end
|
128
|
+
|
129
|
+
def bitcoin_change_address=(addr)
|
130
|
+
@bitcoin_change_address = BitcoinPaymentAddress.parse(addr)
|
131
|
+
end
|
132
|
+
|
133
|
+
def asset_change_address=(addr)
|
134
|
+
@asset_change_address = AssetAddress.parse(addr)
|
135
|
+
end
|
136
|
+
|
137
|
+
def build
|
138
|
+
|
139
|
+
validate_bitcoin_change_address!
|
140
|
+
validate_asset_change_address!
|
141
|
+
|
142
|
+
result = Result.new
|
143
|
+
|
144
|
+
# We don't count assets_cost because outputs of these txs
|
145
|
+
# will be consumed in the next transaction. Only add the fees.
|
146
|
+
if self.issuing_asset_script
|
147
|
+
issuing_tx, unsigned_input_indexes = make_transaction_for_issues
|
148
|
+
result.fee = issuing_tx.fee
|
149
|
+
result.transactions << issuing_tx
|
150
|
+
result.unsigned_input_indexes << unsigned_input_indexes
|
151
|
+
self.issuing_asset_output = AssetTransactionOutput.new(transaction_output: issuing_tx.outputs.first, verified: true)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Prepare the target transaction
|
155
|
+
txbuilder = make_transaction_builder
|
156
|
+
if result.transactions.size > 0
|
157
|
+
txbuilder.parent_transactions = [result.transactions.last]
|
158
|
+
end
|
159
|
+
txbuilder.prepended_unspent_outputs ||= []
|
160
|
+
txbuilder.outputs = []
|
161
|
+
|
162
|
+
# Add issuance input and outputs first.
|
163
|
+
|
164
|
+
consumed_asset_outputs = [] # used in inputs
|
165
|
+
issue_outputs = []
|
166
|
+
transfer_outputs = []
|
167
|
+
|
168
|
+
if self.issued_assets.size > 0
|
169
|
+
txbuilder.prepended_unspent_outputs << self.issuing_asset_output.transaction_output
|
170
|
+
self.issued_assets.each do |issue|
|
171
|
+
atxo = make_asset_transaction_output(asset_id: issuing_asset_id, amount: issue[:amount], script: issue[:script])
|
172
|
+
issue_outputs << atxo
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Move all transfers of the asset ID used on the issuing output to the top of the list.
|
177
|
+
asset_id_on_the_issuing_input = self.issuing_asset_output ? self.issuing_asset_output.asset_id : nil
|
178
|
+
if asset_id_on_the_issuing_input
|
179
|
+
aid = asset_id_on_the_issuing_input.to_s
|
180
|
+
self.transferred_assets = self.transferred_assets.sort do |a,b|
|
181
|
+
# move the asset id used in the issue input to the top
|
182
|
+
if a[:asset_id].to_s == aid
|
183
|
+
-1
|
184
|
+
elsif b[:asset_id] == aid
|
185
|
+
1
|
186
|
+
else
|
187
|
+
0 # keep the order
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
consumed_outpoints = {} # "txid:index" => true
|
193
|
+
|
194
|
+
self.transferred_assets.each do |transfer| # |aid,transfers|
|
195
|
+
asset_id = transfer[:asset_id]
|
196
|
+
transfers = [transfer]
|
197
|
+
amount_required = 0
|
198
|
+
amount_provided = 0
|
199
|
+
if asset_id_on_the_issuing_input.to_s == asset_id.to_s
|
200
|
+
amount_provided = self.issuing_asset_output.value
|
201
|
+
end
|
202
|
+
|
203
|
+
atxo = make_asset_transaction_output(asset_id: transfer[:asset_id],
|
204
|
+
amount: transfer[:amount],
|
205
|
+
script: transfer[:script])
|
206
|
+
amount_required += atxo.value
|
207
|
+
transfer_outputs << atxo
|
208
|
+
|
209
|
+
# Fill in enough unspent assets for this asset_id.
|
210
|
+
# Use per-transfer provider if it's specified. Otherwise use global provider.
|
211
|
+
provider = transfer[:provider] || self.asset_provider
|
212
|
+
unspents = provider.asset_unspent_outputs(asset_id: asset_id, amount: amount_required).dup
|
213
|
+
|
214
|
+
while amount_provided < amount_required
|
215
|
+
autxo = unspents.shift
|
216
|
+
if !autxo
|
217
|
+
raise InsufficientFundsError, "Not enough outputs for asset #{asset_id.to_s} (#{amount_provided} available < #{amount_required} required)"
|
218
|
+
end
|
219
|
+
if !consumed_outpoints[oid = autxo.transaction_output.outpoint_id]
|
220
|
+
# Only apply outputs with matching asset ids.
|
221
|
+
if autxo.asset_id == asset_id
|
222
|
+
raise ArgumentError, "Must be verified asset outputs to spend" if !autxo.verified?
|
223
|
+
consumed_outpoints[oid] = true
|
224
|
+
amount_provided += autxo.value
|
225
|
+
consumed_asset_outputs << autxo
|
226
|
+
txbuilder.prepended_unspent_outputs << autxo.transaction_output
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# If the difference is > 0, add a change output
|
232
|
+
change = amount_provided - amount_required
|
233
|
+
if change > 0
|
234
|
+
# Use per-transfer change address if it's specified. Otherwise use global change address.
|
235
|
+
change_addr = transfer[:change_address] || self.asset_change_address
|
236
|
+
atxo = make_asset_transaction_output(asset_id: asset_id,
|
237
|
+
amount: change,
|
238
|
+
script: change_addr.script)
|
239
|
+
transfer_outputs << atxo
|
240
|
+
end
|
241
|
+
|
242
|
+
end # each transfer
|
243
|
+
|
244
|
+
# If we have an asset on the issuance input and it is never used in any transfer,
|
245
|
+
# then we need to create a change output just for it.
|
246
|
+
if asset_id_on_the_issuing_input && self.issuing_asset_output.value > 0
|
247
|
+
if !self.transferred_assets.map{|dict| dict[:asset_id].to_s }.uniq.include?(asset_id_on_the_issuing_input.to_s)
|
248
|
+
atxo = make_asset_transaction_output(asset_id: self.issuing_asset_output.asset_id,
|
249
|
+
amount: self.issuing_asset_output.value,
|
250
|
+
script: self.asset_change_address.script)
|
251
|
+
transfer_outputs << atxo
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
all_asset_outputs = (issue_outputs + transfer_outputs)
|
256
|
+
result.assets_cost = all_asset_outputs.inject(0){|sum, atxo| sum + atxo.transaction_output.value }
|
257
|
+
marker = AssetMarker.new(quantities: all_asset_outputs.map{|atxo| atxo.value }, metadata: self.metadata)
|
258
|
+
|
259
|
+
# Now, add underlying issues, marker and transfer outputs
|
260
|
+
issue_outputs.each do |atxo|
|
261
|
+
txbuilder.outputs << atxo.transaction_output
|
262
|
+
end
|
263
|
+
txbuilder.outputs << marker.output
|
264
|
+
transfer_outputs.each do |atxo|
|
265
|
+
txbuilder.outputs << atxo.transaction_output
|
266
|
+
end
|
267
|
+
|
268
|
+
txresult = txbuilder.build
|
269
|
+
tx = txresult.transaction
|
270
|
+
atx = AssetTransaction.new(transaction: tx)
|
271
|
+
|
272
|
+
BTC::Invariant(atx.outputs.size == all_asset_outputs.size + 1 + (txresult.change_amount > 0 ? 1 : 0),
|
273
|
+
"Must have all asset outputs (with marker output and optional change output)");
|
274
|
+
|
275
|
+
if txresult.change_amount > 0
|
276
|
+
plain_change_output = atx.outputs.last
|
277
|
+
BTC::Invariant(!plain_change_output.verified?, "Must have plain change output not verified");
|
278
|
+
BTC::Invariant(!plain_change_output.asset_id, "Must have plain change output not have asset id");
|
279
|
+
BTC::Invariant(!plain_change_output.value, "Must have plain change output not have asset amount");
|
280
|
+
plain_change_output.verified = true # to match the rest of outputs.
|
281
|
+
end
|
282
|
+
|
283
|
+
# Provide color info for each input
|
284
|
+
consumed_asset_outputs.each_with_index do |atxo, i|
|
285
|
+
atx.inputs[i].asset_id = atxo.asset_id
|
286
|
+
atx.inputs[i].value = atxo.value
|
287
|
+
atx.inputs[i].verified = true
|
288
|
+
end
|
289
|
+
atx.inputs[consumed_asset_outputs.size..-1].each do |input|
|
290
|
+
input.asset_id = nil
|
291
|
+
input.value = nil
|
292
|
+
input.verified = true
|
293
|
+
end
|
294
|
+
|
295
|
+
# Provide color info for each output
|
296
|
+
issue_outputs.each_with_index do |aout1, i|
|
297
|
+
aout = atx.outputs[i]
|
298
|
+
aout.asset_id = aout1.asset_id
|
299
|
+
aout.value = aout1.value
|
300
|
+
aout.verified = true
|
301
|
+
end
|
302
|
+
atx.outputs[issue_outputs.size].verified = true # make marker verified
|
303
|
+
offset = 1 + issue_outputs.size # +1 for marker
|
304
|
+
transfer_outputs.each_with_index do |aout1, i|
|
305
|
+
aout = atx.outputs[i + offset]
|
306
|
+
aout.asset_id = aout1.asset_id
|
307
|
+
aout.value = aout1.value
|
308
|
+
aout.verified = true
|
309
|
+
end
|
310
|
+
offset = 1 + issue_outputs.size + transfer_outputs.size # +1 for marker
|
311
|
+
atx.outputs[offset..-1].each do |aout|
|
312
|
+
aout.asset_id = nil
|
313
|
+
aout.value = nil
|
314
|
+
aout.verified = true
|
315
|
+
end
|
316
|
+
|
317
|
+
if txresult.change_amount == 0
|
318
|
+
atx.outputs.each do |aout|
|
319
|
+
if !aout.marker?
|
320
|
+
BTC::Invariant(aout.verified?, "Must be verified");
|
321
|
+
BTC::Invariant(!!aout.asset_id, "Must have asset id");
|
322
|
+
BTC::Invariant(aout.value && aout.value > 0, "Must have some asset amount");
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
result.unsigned_input_indexes << txresult.unsigned_input_indexes
|
328
|
+
result.transactions << tx
|
329
|
+
result.asset_transaction = atx
|
330
|
+
|
331
|
+
result
|
332
|
+
end
|
333
|
+
|
334
|
+
# Helpers
|
335
|
+
|
336
|
+
def issuing_asset_id
|
337
|
+
@issuing_asset_id ||= AssetID.new(script: @issuing_asset_script || @issuing_asset_output.transaction_output.script)
|
338
|
+
end
|
339
|
+
|
340
|
+
def make_asset_transaction_output(asset_id: nil, amount: nil, script: nil)
|
341
|
+
txout = make_output_for_asset_script(script)
|
342
|
+
AssetTransactionOutput.new(transaction_output: txout, asset_id: asset_id, value: amount, verified: true)
|
343
|
+
end
|
344
|
+
|
345
|
+
def make_transaction_for_issues
|
346
|
+
raise "Sanity check" if !self.issuing_asset_script
|
347
|
+
txbuilder = make_transaction_builder
|
348
|
+
txbuilder.outputs = [
|
349
|
+
make_output_for_asset_script(self.issuing_asset_script)
|
350
|
+
]
|
351
|
+
result = txbuilder.build
|
352
|
+
[result.transaction, result.unsigned_input_indexes]
|
353
|
+
end
|
354
|
+
|
355
|
+
def make_output_for_asset_script(script)
|
356
|
+
txout = BTC::TransactionOutput.new(value: MAX_MONEY, script: script)
|
357
|
+
txout.value = txout.dust_limit
|
358
|
+
raise RuntimeError, "Sanity check: txout value must not be zero" if txout.value <= 0
|
359
|
+
txout
|
360
|
+
end
|
361
|
+
|
362
|
+
def make_transaction_builder
|
363
|
+
txbuilder = TransactionBuilder.new
|
364
|
+
txbuilder.change_address = self.bitcoin_change_address
|
365
|
+
txbuilder.signer = self.signer
|
366
|
+
txbuilder.provider = self.internal_bitcoin_provider
|
367
|
+
txbuilder.unspent_outputs = self.bitcoin_unspent_outputs
|
368
|
+
txbuilder.fee_rate = self.fee_rate
|
369
|
+
txbuilder
|
370
|
+
end
|
371
|
+
|
372
|
+
def internal_bitcoin_provider
|
373
|
+
@internal_bitcoin_provider ||= (self.bitcoin_provider || TransactionBuilder::Provider.new{|txb| []})
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
# Validation Methods
|
378
|
+
|
379
|
+
def validate_bitcoin_change_address!
|
380
|
+
addr = self.bitcoin_change_address
|
381
|
+
raise ArgumentError, "Missing bitcoin_change_address" if !addr
|
382
|
+
raise ArgumentError, "bitcoin_change_address must be an instance of BTC::Address" if !addr.is_a?(Address)
|
383
|
+
raise ArgumentError, "bitcoin_change_address must not be an instance of BTC::AssetAddress" if addr.is_a?(AssetAddress)
|
384
|
+
end
|
385
|
+
|
386
|
+
def validate_asset_change_address!
|
387
|
+
self.transferred_assets.each do |dict|
|
388
|
+
addr = dict[:change_address] || self.asset_change_address
|
389
|
+
raise ArgumentError, "Missing asset_change_address" if !addr
|
390
|
+
raise ArgumentError, "asset_change_address must be an instance of BTC::AssetAddress" if !addr.is_a?(AssetAddress)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
protected
|
395
|
+
attr_accessor :issuing_asset_script
|
396
|
+
attr_accessor :issuing_asset_output
|
397
|
+
attr_accessor :issued_assets
|
398
|
+
attr_accessor :transferred_assets
|
399
|
+
attr_accessor :bitcoin_outputs
|
400
|
+
|
401
|
+
def issued_assets
|
402
|
+
@issued_assets ||= []
|
403
|
+
end
|
404
|
+
|
405
|
+
def transferred_assets
|
406
|
+
@transferred_assets ||= []
|
407
|
+
end
|
408
|
+
|
409
|
+
def bitcoin_outputs
|
410
|
+
@bitcoin_outputs ||= []
|
411
|
+
end
|
412
|
+
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
require_relative 'asset_transaction_builder/errors.rb'
|
417
|
+
require_relative 'asset_transaction_builder/result.rb'
|
418
|
+
require_relative 'asset_transaction_builder/provider.rb'
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Implementation of OpenAssets protocol.
|
2
|
+
# https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki
|
3
|
+
module BTC
|
4
|
+
class AssetTransactionInput
|
5
|
+
attr_accessor :index
|
6
|
+
attr_accessor :transaction_input # BTC::TransactionInput
|
7
|
+
|
8
|
+
# Verified inputs with `asset_id == nil` are *uncolored*.
|
9
|
+
# Non-verified inputs are not known to have assets associated with them.
|
10
|
+
attr_accessor :asset_id
|
11
|
+
attr_accessor :value
|
12
|
+
attr_accessor :verified # true if asset_id and value are verified and can be used.
|
13
|
+
|
14
|
+
def initialize(transaction_input: nil, asset_id: nil, value: nil, verified: false)
|
15
|
+
raise ArgumentError, "No transaction_input provided" if !transaction_input
|
16
|
+
@transaction_input = transaction_input
|
17
|
+
@index = transaction_input ? transaction_input.index : nil
|
18
|
+
@asset_id = asset_id
|
19
|
+
@value = value
|
20
|
+
@verified = !!verified
|
21
|
+
end
|
22
|
+
|
23
|
+
# Input is verified when we know for sure if it's colored or not and if it is,
|
24
|
+
# what asset and how much is associated with it.
|
25
|
+
def verified?
|
26
|
+
!!@verified
|
27
|
+
end
|
28
|
+
|
29
|
+
# Input is colored when it has AssetID and a positive value.
|
30
|
+
def colored?
|
31
|
+
!!@asset_id && @value && @value > 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def index
|
35
|
+
@index ||= @transaction_input.index
|
36
|
+
end
|
37
|
+
|
38
|
+
def assets_data
|
39
|
+
data = "".b
|
40
|
+
data << WireFormat.encode_uint8(@verified ? 1 : 0)
|
41
|
+
data << WireFormat.encode_string(@asset_id ? @asset_id.hash : "".b)
|
42
|
+
data << WireFormat.encode_int64le(@value.to_i)
|
43
|
+
data
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns total length read
|
47
|
+
def parse_assets_data(data, offset: 0)
|
48
|
+
v, len = WireFormat.read_uint8(data: data, offset: offset)
|
49
|
+
raise ArgumentError, "Invalid data: verified flag" if !v
|
50
|
+
asset_hash, len = WireFormat.read_string(data: data, offset: len) # empty or 20 bytes
|
51
|
+
raise ArgumentError, "Invalid data: asset hash" if !asset_hash
|
52
|
+
value, len = WireFormat.read_int64le(data: data, offset: len)
|
53
|
+
raise ArgumentError, "Invalid data: value" if !value
|
54
|
+
@verified = (v == 1)
|
55
|
+
@asset_id = asset_hash.bytesize > 0 ? AssetID.new(hash: asset_hash) : nil
|
56
|
+
@value = value
|
57
|
+
len
|
58
|
+
end
|
59
|
+
|
60
|
+
def ==(other)
|
61
|
+
super(other) || self.transaction_input == other.transaction_input
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# Implementation of OpenAssets protocol.
|
2
|
+
# https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki
|
3
|
+
module BTC
|
4
|
+
class AssetTransactionOutput
|
5
|
+
KIND_ISSUE = :issue
|
6
|
+
KIND_TRANSFER = :transfer
|
7
|
+
KIND_MARKER = :marker
|
8
|
+
|
9
|
+
attr_reader :index
|
10
|
+
attr_accessor :transaction_output # BTC::TransactionOutput
|
11
|
+
|
12
|
+
attr_accessor :kind
|
13
|
+
attr_accessor :issue # if true, then it's an issue output
|
14
|
+
attr_accessor :transfer # if true, then it's a transfer output
|
15
|
+
attr_accessor :marker # if true, then it's a marker output
|
16
|
+
|
17
|
+
# Verified outputs with `asset_id == nil` are *uncolored*.
|
18
|
+
# Non-verified outputs are not known to have assets associated with them.
|
19
|
+
attr_accessor :asset_id
|
20
|
+
attr_accessor :value
|
21
|
+
attr_accessor :verified # true if asset_id and value are verified and can be used.
|
22
|
+
|
23
|
+
def initialize(transaction_output: nil, asset_id: nil, value: nil, verified: false, issue: nil, transfer: nil, marker: nil)
|
24
|
+
raise ArgumentError, "No transaction_output provided" if !transaction_output
|
25
|
+
@transaction_output = transaction_output
|
26
|
+
@index = transaction_output ? transaction_output.index : nil
|
27
|
+
@asset_id = asset_id
|
28
|
+
@value = value
|
29
|
+
@verified = !!verified
|
30
|
+
self.issue = issue if issue
|
31
|
+
self.transfer = transfer if transfer
|
32
|
+
self.marker = marker if marker
|
33
|
+
end
|
34
|
+
|
35
|
+
# Output is verified when we know for sure if it's colored or not and if it is,
|
36
|
+
# what asset and how much is associated with it.
|
37
|
+
def verified?
|
38
|
+
!!@verified
|
39
|
+
end
|
40
|
+
|
41
|
+
# Output is colored when it has AssetID and a positive value.
|
42
|
+
def colored?
|
43
|
+
!!@asset_id && has_value?
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_value?
|
47
|
+
!!@value && @value > 0
|
48
|
+
end
|
49
|
+
|
50
|
+
def issue=(issue)
|
51
|
+
raise ArgumentError, "Can only set `issue` to true" if !issue
|
52
|
+
self.kind = KIND_ISSUE
|
53
|
+
issue
|
54
|
+
end
|
55
|
+
|
56
|
+
def transfer=(transfer)
|
57
|
+
raise ArgumentError, "Can only set `transfer` to true" if !transfer
|
58
|
+
self.kind = KIND_TRANSFER
|
59
|
+
transfer
|
60
|
+
end
|
61
|
+
|
62
|
+
def marker=(marker)
|
63
|
+
raise ArgumentError, "Can only set `marker` to true" if !marker
|
64
|
+
self.kind = KIND_MARKER
|
65
|
+
marker
|
66
|
+
end
|
67
|
+
|
68
|
+
def kind=(kind)
|
69
|
+
@kind = kind
|
70
|
+
@marker = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns `true` if this output may issue new amount of some asset.
|
74
|
+
def issue
|
75
|
+
@kind == KIND_ISSUE
|
76
|
+
end
|
77
|
+
alias :issue? :issue
|
78
|
+
|
79
|
+
# Returns `true` if this output may transfer new amount of some asset.
|
80
|
+
def transfer
|
81
|
+
@kind == KIND_TRANSFER
|
82
|
+
end
|
83
|
+
alias :transfer? :transfer
|
84
|
+
|
85
|
+
# Returns `true` if this output corresponds to a marker output (always uncolored)
|
86
|
+
def marker
|
87
|
+
@marker ||= (@kind == KIND_MARKER ? AssetMarker.new(output: @transaction_output) : nil)
|
88
|
+
end
|
89
|
+
|
90
|
+
def marker?
|
91
|
+
!!marker
|
92
|
+
end
|
93
|
+
|
94
|
+
def index
|
95
|
+
@index ||= @transaction_output.index
|
96
|
+
end
|
97
|
+
|
98
|
+
def transaction_hash
|
99
|
+
@transaction_output.transaction_hash
|
100
|
+
end
|
101
|
+
|
102
|
+
def transaction_id
|
103
|
+
@transaction_output.transaction_id
|
104
|
+
end
|
105
|
+
|
106
|
+
def outpoint
|
107
|
+
@transaction_output.outpoint
|
108
|
+
end
|
109
|
+
|
110
|
+
def outpoint_id
|
111
|
+
@transaction_output.outpoint_id
|
112
|
+
end
|
113
|
+
|
114
|
+
def assets_data
|
115
|
+
data = "".b
|
116
|
+
data << WireFormat.encode_uint8(@verified ? 1 : 0)
|
117
|
+
data << WireFormat.encode_string(@asset_id ? @asset_id.hash : "".b)
|
118
|
+
data << WireFormat.encode_int64le(@value.to_i)
|
119
|
+
data
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns total length read
|
123
|
+
def parse_assets_data(data, offset: 0)
|
124
|
+
v, len = WireFormat.read_uint8(data: data, offset: offset)
|
125
|
+
raise ArgumentError, "Invalid data: verified flag" if !v
|
126
|
+
asset_hash, len = WireFormat.read_string(data: data, offset: len) # empty or 20 bytes
|
127
|
+
raise ArgumentError, "Invalid data: asset hash" if !asset_hash
|
128
|
+
value, len = WireFormat.read_int64le(data: data, offset: len)
|
129
|
+
raise ArgumentError, "Invalid data: value" if !value
|
130
|
+
@verified = (v == 1)
|
131
|
+
@asset_id = asset_hash.bytesize > 0 ? AssetID.new(hash: asset_hash) : nil
|
132
|
+
@value = value
|
133
|
+
len
|
134
|
+
end
|
135
|
+
|
136
|
+
def ==(other)
|
137
|
+
super(other) || self.transaction_output == other.transaction_output
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Implementation of [OpenAssets protocol](https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki).
|
2
|
+
# * AssetID uniquely identifies an asset and conditions of issuance.
|
3
|
+
# * AssetAddress is an address format that allows sending and receiving assets.
|
4
|
+
# * Asset represents an asset source (includes AssetID and output).
|
5
|
+
# * AssetMarker represents a marker specifying asset issuance and transfer.
|
6
|
+
#
|
7
|
+
# Tasks:
|
8
|
+
# * Issuing a new asset.
|
9
|
+
# * Issuing more units of a given asset. If the issuing address holds some assets, they must be re-created.
|
10
|
+
# * Tracking asset origin via proofchain of transactions with block merkle paths.
|
11
|
+
# * Transferring assets.
|
12
|
+
# * Transferring assets with payment in the same transaction.
|
13
|
+
# * Non-interactive decentralized order book.
|
14
|
+
# * Supporting issue-once assets (anchored to a genesis transaction ID).
|
15
|
+
# * Supporting stock split (AssetV2 is issued by consuming AssetV1, validating software must validate AssetV2 accordingly).
|
16
|
+
# * Supporting key rotation (can be done using the same technique as with stock split). Maybe use metadata to link to a previous asset.
|
17
|
+
require_relative 'open_assets/asset_id.rb'
|
18
|
+
require_relative 'open_assets/asset_address.rb'
|
19
|
+
require_relative 'open_assets/asset.rb'
|
20
|
+
require_relative 'open_assets/asset_marker.rb'
|
21
|
+
require_relative 'open_assets/asset_transaction.rb'
|
22
|
+
require_relative 'open_assets/asset_transaction_input.rb'
|
23
|
+
require_relative 'open_assets/asset_transaction_output.rb'
|
24
|
+
require_relative 'open_assets/asset_processor.rb'
|
25
|
+
require_relative 'open_assets/asset_transaction_builder.rb'
|
26
|
+
require_relative 'open_assets/asset_definition.rb'
|