openassets 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +11 -0
  7. data/CODE_OF_CONDUCT.md +13 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE +23 -0
  10. data/README.md +423 -0
  11. data/Rakefile +5 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +7 -0
  14. data/exe/openassets +66 -0
  15. data/lib/openassets.rb +23 -0
  16. data/lib/openassets/api.rb +430 -0
  17. data/lib/openassets/cache.rb +8 -0
  18. data/lib/openassets/cache/output_cache.rb +43 -0
  19. data/lib/openassets/cache/sqlite_base.rb +26 -0
  20. data/lib/openassets/cache/ssl_certificate_cache.rb +44 -0
  21. data/lib/openassets/cache/transaction_cache.rb +33 -0
  22. data/lib/openassets/error.rb +5 -0
  23. data/lib/openassets/medhod_filter.rb +55 -0
  24. data/lib/openassets/protocol.rb +10 -0
  25. data/lib/openassets/protocol/asset_definition.rb +118 -0
  26. data/lib/openassets/protocol/asset_definition_loader.rb +45 -0
  27. data/lib/openassets/protocol/http_asset_definition_loader.rb +27 -0
  28. data/lib/openassets/protocol/marker_output.rb +137 -0
  29. data/lib/openassets/protocol/output_type.rb +27 -0
  30. data/lib/openassets/protocol/transaction_output.rb +136 -0
  31. data/lib/openassets/provider.rb +7 -0
  32. data/lib/openassets/provider/api_error.rb +9 -0
  33. data/lib/openassets/provider/bitcoin_core_provider.rb +103 -0
  34. data/lib/openassets/provider/block_chain_provider_base.rb +22 -0
  35. data/lib/openassets/send_asset_param.rb +17 -0
  36. data/lib/openassets/send_bitcoin_param.rb +13 -0
  37. data/lib/openassets/transaction.rb +11 -0
  38. data/lib/openassets/transaction/dust_output_error.rb +10 -0
  39. data/lib/openassets/transaction/insufficient_asset_quantity_error.rb +7 -0
  40. data/lib/openassets/transaction/insufficient_funds_error.rb +10 -0
  41. data/lib/openassets/transaction/spendable_output.rb +39 -0
  42. data/lib/openassets/transaction/transaction_build_error.rb +9 -0
  43. data/lib/openassets/transaction/transaction_builder.rb +312 -0
  44. data/lib/openassets/transaction/transfer_parameters.rb +41 -0
  45. data/lib/openassets/util.rb +165 -0
  46. data/lib/openassets/version.rb +3 -0
  47. data/openassets.gemspec +33 -0
  48. metadata +244 -0
@@ -0,0 +1,27 @@
1
+ # Transaction output type enum
2
+ module OpenAssets
3
+ module Protocol
4
+ module OutputType
5
+ UNCOLORED = 0
6
+ MARKER_OUTPUT = 1
7
+ ISSUANCE = 2
8
+ TRANSFER = 3
9
+
10
+ # get all enum.
11
+ def self.all
12
+ self.constants.map{|name|self.const_get(name)}
13
+ end
14
+
15
+ def self.output_type_label(type)
16
+ case type
17
+ when UNCOLORED then 'uncolored'
18
+ when MARKER_OUTPUT then 'marker'
19
+ when ISSUANCE then 'issuance'
20
+ when TRANSFER then 'transfer'
21
+ else 'uncolored'
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,136 @@
1
+ module OpenAssets
2
+ module Protocol
3
+
4
+ # Represents a transaction output and its asset ID and asset quantity.
5
+ class TransactionOutput
6
+
7
+ include OpenAssets::Util
8
+
9
+ attr_accessor :value
10
+ attr_accessor :script
11
+ attr_accessor :asset_id
12
+ attr_accessor :asset_quantity
13
+ attr_accessor :output_type
14
+
15
+ attr_accessor :account
16
+ attr_accessor :metadata
17
+ attr_accessor :asset_definition_url
18
+ attr_accessor :asset_definition
19
+
20
+
21
+ # @param [Integer] value The satoshi value of the output.
22
+ # @param [Bitcoin::Script] script The script controlling redemption of the output.
23
+ # @param [String] asset_id The asset ID of the output.
24
+ # @param [Integer] asset_quantity The asset quantity of the output.
25
+ # @param [OpenAssets::Transaction::OutPutType] output_type The type of the output.
26
+ def initialize(value, script, asset_id = nil, asset_quantity = 0, output_type = OutputType::UNCOLORED, metadata = '')
27
+ raise ArgumentError, "invalid output_type : #{output_type}" unless OutputType.all.include?(output_type)
28
+ raise ArgumentError, "invalid asset_quantity. asset_quantity should be unsignd integer. " unless asset_quantity.between?(0, MarkerOutput::MAX_ASSET_QUANTITY)
29
+ @value = value
30
+ @script = script
31
+ @asset_id = asset_id
32
+ @asset_quantity = asset_quantity
33
+ @output_type = output_type
34
+ @metadata = metadata
35
+ load_asset_definition_url
36
+ end
37
+
38
+ # calculate asset amount.
39
+ # asset amount is the value obtained by converting the asset quantity to the unit of divisibility that are defined in the Asset definition file.
40
+ def asset_amount
41
+ d = divisibility
42
+ d > 0 ? (@asset_quantity.to_f / (10 ** d)).to_f : @asset_quantity
43
+ end
44
+
45
+ # get divisibility defined by asset definition file.
46
+ def divisibility
47
+ return 0 if !valid_asset_definition? || @asset_definition.divisibility.nil?
48
+ @asset_definition.divisibility
49
+ end
50
+
51
+ # Verify proof of authenticity.
52
+ def proof_of_authenticity
53
+ valid_asset_definition? ? @asset_definition.proof_of_authenticity : false
54
+ end
55
+
56
+ # convert to hash object.
57
+ def to_hash
58
+ {
59
+ 'address' => address,
60
+ 'oa_address' => oa_address,
61
+ 'script' => @script.to_payload.bth,
62
+ 'script_type' => script_type,
63
+ 'amount' => satoshi_to_coin(@value),
64
+ 'asset_id' => @asset_id,
65
+ 'asset_quantity' => @asset_quantity.to_s,
66
+ 'asset_amount' => asset_amount.to_s,
67
+ 'account' => @account,
68
+ 'asset_definition_url' => @asset_definition_url,
69
+ 'proof_of_authenticity' => proof_of_authenticity,
70
+ 'output_type' => OpenAssets::Protocol::OutputType.output_type_label(@output_type)
71
+ }
72
+ end
73
+
74
+ def address
75
+ return @script.addresses.first if @script.p2pkh? || @script.p2sh? || @script.p2wpkh? || @script.p2wsh?
76
+ nil # TODO Bitcoin::Script#to_addr after it enable
77
+ end
78
+
79
+ def oa_address
80
+ a = address
81
+ return nil if a.nil?
82
+ address_to_oa_address(a)
83
+ end
84
+
85
+ # get pubkey script type
86
+ def script_type
87
+ return 'pubkeyhash' if script.p2pkh?
88
+ return 'scripthash' if script.p2sh?
89
+ return 'nulldata' if script.standard_op_return?
90
+ return 'multisig' if script.multisig?
91
+ return 'witness_v0_keyhash' if script.p2wpkh?
92
+ return 'witness_v0_scripthash' if script.p2wsh?
93
+ 'nonstandard'
94
+ end
95
+
96
+ private
97
+
98
+ @@definition_cache = {}
99
+
100
+ # get Asset definition url that is included metadata.
101
+ def load_asset_definition_url
102
+ @asset_definition_url = ''
103
+ return if @metadata.nil? || @metadata.length == 0
104
+ if @metadata.start_with?('u=')
105
+ @asset_definition = load_asset_definition(metadata_url)
106
+ if valid_asset_definition?
107
+ @asset_definition_url = metadata_url
108
+ else
109
+ @asset_definition_url = "The asset definition is invalid. #{metadata_url}"
110
+ end
111
+ else
112
+ @asset_definition_url = 'Invalid metadata format.'
113
+ end
114
+ end
115
+
116
+ def metadata_url
117
+ unless @metadata.nil?
118
+ @metadata.slice(2..-1)
119
+ end
120
+ end
121
+
122
+ def valid_asset_definition?
123
+ !@asset_definition.nil? && @asset_definition.include_asset_id?(@asset_id)
124
+ end
125
+
126
+ def load_asset_definition(url)
127
+ unless @@definition_cache.has_key?(url)
128
+ loader = AssetDefinitionLoader.new(metadata_url)
129
+ @@definition_cache[url] = loader.load_definition
130
+ end
131
+ @@definition_cache[url]
132
+ end
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,7 @@
1
+ module OpenAssets
2
+ module Provider
3
+ autoload :BlockChainProviderBase, 'openassets/provider/block_chain_provider_base'
4
+ autoload :BitcoinCoreProvider, 'openassets/provider/bitcoin_core_provider'
5
+ autoload :ApiError, 'openassets/provider/api_error'
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module OpenAssets
2
+ module Provider
3
+
4
+ class ApiError < OpenAssets::Error
5
+
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,103 @@
1
+ require 'rest-client'
2
+
3
+ module OpenAssets
4
+ module Provider
5
+
6
+ # The implementation of BlockChain provider using Bitcoin Core.
7
+ class BitcoinCoreProvider < BlockChainProviderBase
8
+
9
+ attr_reader :config
10
+
11
+ def initialize(config)
12
+ @config = config
13
+
14
+ commands = request(:help).split("\n").inject([]) do |memo_ary, line|
15
+ if !line.empty? && !line.start_with?('==')
16
+ memo_ary << line.split(' ').first.to_sym
17
+ end
18
+ memo_ary
19
+ end
20
+ BitcoinCoreProvider.class_eval do
21
+ commands.each do |command|
22
+ define_method(command) do |*params|
23
+ request(command, *params)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # Get an array of unspent transaction outputs belonging to this wallet.
30
+ # @param [Array] addresses If present, only outputs which pay an address in this array will be returned.
31
+ # @param [Integer] min The minimum number of confirmations the transaction containing an output must have in order to be returned. Default is 1.
32
+ # @param [Integer] max The maximum number of confirmations the transaction containing an output may have in order to be returned. Default is 9999999.
33
+ def list_unspent(addresses = [], min = 1 , max = 9999999)
34
+ listunspent(min, max, addresses)
35
+ end
36
+
37
+ # Get raw transaction.
38
+ # @param [String] transaction_hash The transaction hash.
39
+ # @param [String] verbose Whether to get the serialized or decoded transaction. 0: serialized transaction (Default). 1: decode transaction.
40
+ # @return [String] (if verbose=0)—the serialized transaction. (if verbose=1)—the decoded transaction. (if transaction not found)—nil.
41
+ def get_transaction(transaction_hash, verbose = 0)
42
+ begin
43
+ getrawtransaction(transaction_hash, verbose)
44
+ rescue OpenAssets::Provider::ApiError => e
45
+ nil
46
+ end
47
+ end
48
+
49
+ # Signs a transaction in the serialized transaction format using private keys.
50
+ # @param [String] tx The serialized format transaction.
51
+ # @return [Bitcoin::Tx] The signed transaction.
52
+ def sign_transaction(tx)
53
+ signed_tx = respond_to?(:signrawtransactionwithwallet) ? signrawtransactionwithwallet(tx) : signrawtransaction(tx)
54
+ raise OpenAssets::Error, 'Could not sign the transaction.' unless signed_tx['complete']
55
+ Bitcoin::Tx.parse_from_payload(signed_tx['hex'].htb)
56
+ end
57
+
58
+ # Validates a transaction and broadcasts it to the peer-to-peer network.
59
+ # @param [String] tx The serialized format transaction.
60
+ # @return [String] The TXID or error message.
61
+ def send_transaction(tx)
62
+ sendrawtransaction(tx)
63
+ end
64
+
65
+ # Adds an address or pubkey script to the wallet without the associated private key, allowing you to watch for transactions affecting that address or pubkey script without being able to spend any of its outputs.
66
+ # @param [String] address Either a P2PKH or P2SH address encoded in base58check, or a pubkey script encoded as hex.
67
+ def import_address(address)
68
+ importaddress(address)
69
+ end
70
+
71
+ def request(command, *params)
72
+ data = {
73
+ :method => command,
74
+ :params => params,
75
+ :id => 'jsonrpc'
76
+ }
77
+ post(server_url, @config[:timeout], @config[:open_timeout], data.to_json, content_type: :json) do |respdata, request, result|
78
+ raise ApiError, result.message if !result.kind_of?(Net::HTTPSuccess) && respdata.empty?
79
+ response = JSON.parse(respdata.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*').encode('ISO-8859-1').force_encoding('UTF-8') })
80
+ raise ApiError, response['error'] if response['error']
81
+ response['result']
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def server_url
88
+ url = "#{@config[:schema]}://"
89
+ url.concat "#{@config[:user]}:#{@config[:password]}@"
90
+ url.concat "#{@config[:host]}:#{@config[:port]}"
91
+ if !@config[:wallet].nil? && !@config[:wallet].empty?
92
+ url.concat "/wallet/#{@config[:wallet]}"
93
+ end
94
+ url
95
+ end
96
+
97
+ def post(url, timeout, open_timeout, payload, headers={}, &block)
98
+ RestClient::Request.execute(:method => :post, :url => url, :timeout => timeout, :open_timeout => open_timeout, :payload => payload, :headers => headers, &block)
99
+ end
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,22 @@
1
+ module OpenAssets
2
+ module Provider
3
+
4
+ # The base class providing access to the Blockchain.
5
+ class BlockChainProviderBase
6
+
7
+ def list_unspent(addresses = [], min = 1 , max = 9999999)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def get_transaction(transaction_hash, verbose = 0)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def sign_transaction(tx)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module OpenAssets
2
+
3
+ class SendAssetParam
4
+ attr_accessor :asset_id
5
+ attr_accessor :amount
6
+ attr_accessor :to
7
+ attr_accessor :from
8
+
9
+ def initialize(asset_id, amount, to, from = nil)
10
+ @asset_id = asset_id
11
+ @amount = amount
12
+ @to = to
13
+ @from = from
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,13 @@
1
+ module OpenAssets
2
+
3
+ class SendBitcoinParam
4
+ attr_accessor :amount
5
+ attr_accessor :to
6
+
7
+ def initialize(amount, to)
8
+ @amount = amount
9
+ @to = to
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,11 @@
1
+ module OpenAssets
2
+ module Transaction
3
+ autoload :TransactionBuilder, 'openassets/transaction/transaction_builder'
4
+ autoload :TransferParameters, 'openassets/transaction/transfer_parameters'
5
+ autoload :SpendableOutput, 'openassets/transaction/spendable_output'
6
+ autoload :TransactionBuildError, 'openassets/transaction/transaction_build_error'
7
+ autoload :InsufficientFundsError, 'openassets/transaction/insufficient_funds_error'
8
+ autoload :InsufficientAssetQuantityError, 'openassets/transaction/insufficient_asset_quantity_error'
9
+ autoload :DustOutputError, 'openassets/transaction/dust_output_error'
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module OpenAssets
2
+ module Transaction
3
+
4
+ # The value of an output would be too small, and the output would be considered as dust.
5
+ class DustOutputError < TransactionBuildError
6
+
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module OpenAssets
2
+ module Transaction
3
+ # An insufficient amount of assets is available.
4
+ class InsufficientAssetQuantityError < TransactionBuildError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module OpenAssets
2
+ module Transaction
3
+
4
+ # An insufficient amount of bitcoins is available.
5
+ class InsufficientFundsError < TransactionBuildError
6
+
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,39 @@
1
+ module OpenAssets
2
+ module Transaction
3
+
4
+ # A transaction output with information about the asset ID and asset quantity associated to it.
5
+ class SpendableOutput
6
+
7
+ # An object that can be used to locate the output.
8
+ attr_accessor :out_point
9
+ # The actual output object.
10
+ attr_accessor :output
11
+
12
+ attr_accessor :confirmations
13
+ attr_accessor :spendable
14
+ attr_accessor :solvable
15
+
16
+ # initialize
17
+ # @param [Bitcoin::OutPoint] out_point
18
+ # @param [OpenAssets::Protocol::TransactionOutput] output
19
+ def initialize(out_point, output)
20
+ @out_point = out_point
21
+ @output = output
22
+ @confirmations = nil
23
+ @solvable = nil
24
+ @spendable = nil
25
+ end
26
+
27
+ # convert to hash.
28
+ def to_hash
29
+ return {} if @output.nil?
30
+ h = {'txid' => @out_point.txid, 'vout' => @out_point.index, 'confirmations' => @confirmations}.merge(@output.to_hash)
31
+ h['solvable'] = @solvable unless @solvable.nil?
32
+ h['spendable'] = @spendable unless @spendable.nil?
33
+ h
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ module OpenAssets
2
+ module Transaction
3
+
4
+ class TransactionBuildError < OpenAssets::Error
5
+
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,312 @@
1
+ module OpenAssets
2
+ module Transaction
3
+
4
+ class TransactionBuilder
5
+ include OpenAssets::Util
6
+
7
+ # The minimum allowed output value.
8
+ attr_accessor :amount
9
+
10
+ # The estimated transaction fee rate (satoshis/KB).
11
+ attr_accessor :efr
12
+
13
+ def initialize(amount = 600, efr = 10000)
14
+ @amount = amount
15
+ @efr = efr
16
+ end
17
+
18
+ # Creates a transaction for issuing an asset.
19
+ # @param [TransferParameters] issue_spec The parameters of the issuance.
20
+ # @param [bytes] metadata The metadata to be embedded in the transaction.
21
+ # @param [Integer] fees The fees to include in the transaction.
22
+ # @return[Bitcoin:Protocol:Tx] An unsigned transaction for issuing asset.
23
+ def issue_asset(issue_spec, metadata, fees)
24
+
25
+ if fees == :auto
26
+ # Calculate fees (assume that one vin and four vouts are wrote)
27
+ fees = calc_fee(1, 4)
28
+ end
29
+
30
+ inputs, total_amount =
31
+ TransactionBuilder.collect_uncolored_outputs(issue_spec.unspent_outputs, 2 * @amount + fees)
32
+ tx = Bitcoin::Tx.new
33
+ inputs.each { |spendable|
34
+ tx.in << Bitcoin::TxIn.new(out_point: spendable.out_point, script_sig: spendable.output.script)
35
+ }
36
+ issue_address = oa_address_to_address(issue_spec.to_script)
37
+ from_address = oa_address_to_address(issue_spec.change_script)
38
+ validate_address([issue_address, from_address])
39
+ asset_quantities =[]
40
+ issue_spec.split_output_amount.each{|amount|
41
+ asset_quantities << amount
42
+ tx.out << create_colored_output(issue_address)
43
+ }
44
+ tx.out << create_marker_output(asset_quantities, metadata)
45
+ tx.out << create_uncolored_output(from_address, total_amount - @amount - fees)
46
+ tx
47
+ end
48
+
49
+ # Creates a transaction for sending an asset.
50
+ # @param[String] asset_id The ID of the asset being sent.
51
+ # @param[OpenAssets::Transaction::TransferParameters] asset_transfer_spec The parameters of the asset being transferred.
52
+ # @param[String] btc_change_script The script where to send bitcoin change, if any.
53
+ # @param[Integer] fees The fees to include in the transaction.
54
+ # @return[Bitcoin::Tx] The resulting unsigned transaction.
55
+ def transfer_asset(asset_id, asset_transfer_spec, btc_change_script, fees)
56
+ btc_transfer_spec = OpenAssets::Transaction::TransferParameters.new(
57
+ asset_transfer_spec.unspent_outputs, nil, oa_address_to_address(btc_change_script), 0)
58
+ transfer([[asset_id, asset_transfer_spec]], [btc_transfer_spec], fees)
59
+ end
60
+
61
+ # Creates a transaction for sending assets to many.
62
+ # @param[Array[OpenAssets::Transaction::TransferParameters]] asset_transfer_spec The parameters of the asset being transferred.
63
+ # @param[OpenAssets::Transaction::TransferParameters] btc_transfer_spec The script where to send bitcoin change, if any.
64
+ # @param[Integer] fees The fees to include in the transaction.
65
+ # @return[Bitcoin::Tx] The resulting unsigned transaction.
66
+ def transfer_assets(transfer_specs, btc_transfer_spec, fees)
67
+ transfer(transfer_specs, [btc_transfer_spec], fees)
68
+ end
69
+
70
+ # Creates a transaction for sending bitcoins.
71
+ # @param[OpenAssets::Transaction::TransferParameters] btc_transfer_spec The parameters of the bitcoins being transferred.
72
+ # @param[Integer] fees The fees to include in the transaction.
73
+ # @return[Bitcoin::Tx] The resulting unsigned transaction.
74
+ def transfer_btc(btc_transfer_spec, fees)
75
+ transfer_btcs([btc_transfer_spec], fees)
76
+ end
77
+
78
+ # Creates a transaction for sending bitcoins to many.
79
+ # @param[Array[OpenAssets::Transaction::TransferParameters]] btc_transfer_specs The parameters of the bitcoins being transferred.
80
+ # @param[Integer] fees The fees to include in the transaction.
81
+ # @return[Bitcoin::Tx] The resulting unsigned transaction.
82
+ def transfer_btcs(btc_transfer_specs, fees)
83
+ transfer([], btc_transfer_specs, fees)
84
+ end
85
+
86
+ # Create a transaction for burn asset
87
+ def burn_asset(unspents, asset_id, fee)
88
+
89
+ tx = Bitcoin::Tx.new
90
+ targets = unspents.select{|o|o.output.asset_id == asset_id}
91
+
92
+ if fee == :auto
93
+ # Calculate fees and otsuri (assume that one vout exists)
94
+ fee = calc_fee(targets.size, 1)
95
+ end
96
+
97
+ raise TransactionBuildError.new('There is no asset.') if targets.length == 0
98
+ total_amount = targets.inject(0){|sum, o|o.output.value + sum}
99
+ otsuri = total_amount - fee
100
+ if otsuri < @amount
101
+ uncolored_outputs, uncolored_amount =
102
+ TransactionBuilder.collect_uncolored_outputs(unspents, @amount - otsuri)
103
+ targets = targets + uncolored_outputs
104
+ otsuri += uncolored_amount
105
+ end
106
+ targets.each{|o|
107
+ tx_in = Bitcoin::TxIn.new(out_point: o.out_point, script_sig: o.output.script)
108
+ tx.in << tx_in
109
+ }
110
+ tx.out << create_uncolored_output(targets[0].output.address, otsuri)
111
+ tx
112
+ end
113
+
114
+ # collect uncolored outputs in unspent outputs(contains colored output).
115
+ # @param [Array[OpenAssets::Transaction::SpendableOutput]] unspent_outputs The Array of available outputs.
116
+ # @param [Integer] amount The amount to collect.
117
+ # @return [Array] inputs, total_amount
118
+ def self.collect_uncolored_outputs(unspent_outputs, amount)
119
+ total_amount = 0
120
+ results = []
121
+ unspent_outputs.each do |output|
122
+ if output.output.asset_id.nil?
123
+ results << output
124
+ total_amount += output.output.value
125
+ end
126
+ return results, total_amount if total_amount >= amount
127
+ end
128
+ raise InsufficientFundsError
129
+ end
130
+
131
+ # Returns a list of colored outputs for the specified quantity.
132
+ # @param[Array[OpenAssets::Transaction::SpendableOutput]] unspent_outputs
133
+ # @param[String] asset_id The ID of the asset to collect.
134
+ # @param[Integer] asset_quantity The asset quantity to collect.
135
+ # @return[Array[OpenAssets::Transaction::SpendableOutput], int] A list of outputs, and the total asset quantity collected.
136
+ def self.collect_colored_outputs(unspent_outputs, asset_id, asset_quantity)
137
+ total_amount = 0
138
+ result = []
139
+ unspent_outputs.each do |output|
140
+ if output.output.asset_id == asset_id
141
+ result << output
142
+ total_amount += output.output.asset_quantity
143
+ end
144
+ return result, total_amount if total_amount >= asset_quantity
145
+ end
146
+ raise InsufficientAssetQuantityError
147
+ end
148
+
149
+ private
150
+ # create colored output.
151
+ # @param [String] address The Bitcoin address.
152
+ # @return [Bitcoin::TxOut] colored output
153
+ def create_colored_output(address)
154
+ Bitcoin::TxOut.new(value: @amount, script_pubkey: Bitcoin::Script.parse_from_addr(address))
155
+ end
156
+
157
+ # create marker output.
158
+ # @param [Array] asset_quantities asset_quantity array.
159
+ # @param [String] metadata
160
+ # @return [Bitcoin::TxOut] the marker output.
161
+ def create_marker_output(asset_quantities, metadata = '')
162
+ script = OpenAssets::Protocol::MarkerOutput.new(asset_quantities, metadata).build_script
163
+ Bitcoin::TxOut.new(value: 0, script_pubkey: script)
164
+ end
165
+
166
+ # create an uncolored output.
167
+ # @param [String] address: The Bitcoin address.
168
+ # @param [Integer] value: The satoshi value of the output.
169
+ # @return [Bitcoin::TxOut] an uncolored output.
170
+ def create_uncolored_output(address, value)
171
+ raise DustOutputError if value < @amount
172
+ Bitcoin::TxOut.new(value: value, script_pubkey: Bitcoin::Script.parse_from_addr(address))
173
+ end
174
+
175
+
176
+ # create a transaction
177
+ # @param[Array[OpenAssets::Transaction::TransferParameters]] asset_transfer_specs The parameters of the assets being transferred.
178
+ # @param[Array[OpenAssets::Transaction::TransferParameters]] btc_transfer_specs The parameters of the bitcoins being transferred.
179
+ # @param[Integer] fees The fees to include in the transaction.
180
+ # @return[Bitcoin::Tx] The resulting unsigned transaction.
181
+ def transfer(asset_transfer_specs, btc_transfer_specs, fees)
182
+ inputs = [] # vin field
183
+ outputs = [] # vout field
184
+ asset_quantities = []
185
+
186
+ # Only when assets are transferred
187
+ asset_based_specs = {}
188
+ asset_transfer_specs.each{|asset_id, transfer_spec|
189
+ asset_based_specs[asset_id] = {} unless asset_based_specs.has_key?(asset_id)
190
+ asset_based_specs[asset_id][transfer_spec.change_script] = [] unless asset_based_specs[asset_id].has_key?(transfer_spec.change_script)
191
+ asset_based_specs[asset_id][transfer_spec.change_script] << transfer_spec
192
+ }
193
+
194
+ asset_based_specs.each{|asset_id, address_based_specs|
195
+ address_based_specs.values.each{|transfer_specs|
196
+ transfer_amount = transfer_specs.inject(0){|sum, s| sum + s.amount}
197
+ colored_outputs, total_amount = TransactionBuilder.collect_colored_outputs(transfer_specs[0].unspent_outputs, asset_id, transfer_amount)
198
+ inputs = inputs + colored_outputs
199
+ transfer_specs.each{|spec|
200
+ # add asset transfer output
201
+ spec.split_output_amount.each {|amount|
202
+ outputs << create_colored_output(oa_address_to_address(spec.to_script))
203
+ asset_quantities << amount
204
+ }
205
+ }
206
+ # add the rest of the asset to the origin address
207
+ if total_amount > transfer_amount
208
+ outputs << create_colored_output(oa_address_to_address(transfer_specs[0].change_script))
209
+ asset_quantities << (total_amount - transfer_amount)
210
+ end
211
+ }
212
+ }
213
+ # End of asset settings
214
+
215
+ ## For bitcoins transfer
216
+ # Assume that there is one address from
217
+ utxo = btc_transfer_specs[0].unspent_outputs.dup
218
+
219
+ # Calculate rest of bitcoins in asset settings
220
+ # btc_excess = inputs(colored) total satoshi - outputs(transfer) total satoshi
221
+ btc_excess = inputs.inject(0) { |sum, i| sum + i.output.value } - outputs.inject(0){|sum, o| sum + o.value}
222
+
223
+ # Calculate total amount of bitcoins to send
224
+ btc_transfer_total_amount = btc_transfer_specs.inject(0) {|sum, b| sum + b.amount}
225
+
226
+ if fees == :auto
227
+ fixed_fees = 0
228
+ else
229
+ fixed_fees = fees
230
+ end
231
+
232
+ if btc_excess < btc_transfer_total_amount + fixed_fees
233
+ # When there does not exist enough bitcoins to send in the inputs
234
+ # assign new address (utxo) to the inputs (does not include output coins)
235
+ # CREATING INPUT (if needed)
236
+ uncolored_outputs, uncolored_amount =
237
+ TransactionBuilder.collect_uncolored_outputs(utxo, btc_transfer_total_amount + fixed_fees - btc_excess)
238
+ utxo = utxo - uncolored_outputs
239
+ inputs << uncolored_outputs
240
+ btc_excess += uncolored_amount
241
+ end
242
+
243
+ if fees == :auto
244
+ # Calculate fees and otsuri (the numbers of vins and vouts are known)
245
+ # +1 in the second term means "otsuri" vout,
246
+ # and outputs size means the number of vout witn asset_id
247
+ fees = calc_fee(inputs.size, outputs.size + btc_transfer_specs.size + 1)
248
+ end
249
+
250
+ otsuri = btc_excess - btc_transfer_total_amount - fees
251
+
252
+ if otsuri > 0 && otsuri < @amount
253
+ # When there exists otsuri, but it is smaller than @amount (default is 600 satoshis)
254
+ # assign new address (utxo) to the input (does not include @amount - otsuri)
255
+ # CREATING INPUT (if needed)
256
+ uncolored_outputs, uncolored_amount =
257
+ TransactionBuilder.collect_uncolored_outputs(utxo, @amount - otsuri)
258
+ inputs << uncolored_outputs
259
+ otsuri += uncolored_amount
260
+ end
261
+
262
+ if otsuri > 0
263
+ # When there exists otsuri, write it to outputs
264
+ # CREATING OUTPUT
265
+ outputs << create_uncolored_output(btc_transfer_specs[0].change_script, otsuri)
266
+ end
267
+
268
+ btc_transfer_specs.each{|btc_transfer_spec|
269
+ if btc_transfer_spec.amount > 0
270
+ # Write output for bitcoin transfer by specifics of the argument
271
+ # CREATING OUTPUT
272
+ btc_transfer_spec.split_output_amount.each {|amount|
273
+ outputs << create_uncolored_output(btc_transfer_spec.to_script, amount)
274
+ }
275
+ end
276
+ }
277
+
278
+ # add marker output to outputs first.
279
+ unless asset_quantities.empty?
280
+ outputs.unshift(create_marker_output(asset_quantities))
281
+ end
282
+
283
+ # create a bitcoin transaction (assembling inputs and output into one transaction)
284
+ tx = Bitcoin::Tx.new
285
+ # for all inputs (vin fields), add sigs to the same transaction
286
+ inputs.flatten.each{|i|
287
+ tx_in = Bitcoin::TxIn.new(out_point: i.out_point, script_sig: i.output.script)
288
+ tx.in << tx_in
289
+ }
290
+
291
+ outputs.each{|o|tx.out << o}
292
+
293
+ tx
294
+ end
295
+
296
+ # Calculate a transaction fee
297
+ # @param [Integer] inputs_num: The number of vin fields.
298
+ # @param [Integer] outputs_num: The number of vout fields.
299
+ # @return [Integer] A transaction fee.
300
+ def calc_fee(inputs_num, outputs_num)
301
+ # See http://bitcoinfees.com/
302
+ tx_size = 148 * inputs_num + 34 * outputs_num + 10
303
+ # See Bitcoin::tx.calculate_minimum_fee
304
+ fee = (1 + tx_size / 1_000) * @efr
305
+
306
+ fee
307
+ end
308
+
309
+ end
310
+
311
+ end
312
+ end