openassets 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +11 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE +23 -0
- data/README.md +423 -0
- data/Rakefile +5 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/openassets +66 -0
- data/lib/openassets.rb +23 -0
- data/lib/openassets/api.rb +430 -0
- data/lib/openassets/cache.rb +8 -0
- data/lib/openassets/cache/output_cache.rb +43 -0
- data/lib/openassets/cache/sqlite_base.rb +26 -0
- data/lib/openassets/cache/ssl_certificate_cache.rb +44 -0
- data/lib/openassets/cache/transaction_cache.rb +33 -0
- data/lib/openassets/error.rb +5 -0
- data/lib/openassets/medhod_filter.rb +55 -0
- data/lib/openassets/protocol.rb +10 -0
- data/lib/openassets/protocol/asset_definition.rb +118 -0
- data/lib/openassets/protocol/asset_definition_loader.rb +45 -0
- data/lib/openassets/protocol/http_asset_definition_loader.rb +27 -0
- data/lib/openassets/protocol/marker_output.rb +137 -0
- data/lib/openassets/protocol/output_type.rb +27 -0
- data/lib/openassets/protocol/transaction_output.rb +136 -0
- data/lib/openassets/provider.rb +7 -0
- data/lib/openassets/provider/api_error.rb +9 -0
- data/lib/openassets/provider/bitcoin_core_provider.rb +103 -0
- data/lib/openassets/provider/block_chain_provider_base.rb +22 -0
- data/lib/openassets/send_asset_param.rb +17 -0
- data/lib/openassets/send_bitcoin_param.rb +13 -0
- data/lib/openassets/transaction.rb +11 -0
- data/lib/openassets/transaction/dust_output_error.rb +10 -0
- data/lib/openassets/transaction/insufficient_asset_quantity_error.rb +7 -0
- data/lib/openassets/transaction/insufficient_funds_error.rb +10 -0
- data/lib/openassets/transaction/spendable_output.rb +39 -0
- data/lib/openassets/transaction/transaction_build_error.rb +9 -0
- data/lib/openassets/transaction/transaction_builder.rb +312 -0
- data/lib/openassets/transaction/transfer_parameters.rb +41 -0
- data/lib/openassets/util.rb +165 -0
- data/lib/openassets/version.rb +3 -0
- data/openassets.gemspec +33 -0
- 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,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,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,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,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
|