monacoin-openassets-ruby 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) 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 +420 -0
  11. data/Rakefile +5 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +7 -0
  14. data/exe/monacoin-openassets +66 -0
  15. data/lib/monacoin-openassets.rb +23 -0
  16. data/lib/openassets/api.rb +423 -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 +43 -0
  27. data/lib/openassets/protocol/http_asset_definition_loader.rb +27 -0
  28. data/lib/openassets/protocol/marker_output.rb +128 -0
  29. data/lib/openassets/protocol/output_type.rb +27 -0
  30. data/lib/openassets/protocol/transaction_output.rb +157 -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 +123 -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 +12 -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/out_point.rb +22 -0
  42. data/lib/openassets/transaction/spendable_output.rb +38 -0
  43. data/lib/openassets/transaction/transaction_build_error.rb +9 -0
  44. data/lib/openassets/transaction/transaction_builder.rb +319 -0
  45. data/lib/openassets/transaction/transfer_parameters.rb +41 -0
  46. data/lib/openassets/util.rb +173 -0
  47. data/lib/openassets/version.rb +3 -0
  48. data/openassets-ruby.gemspec +38 -0
  49. metadata +246 -0
@@ -0,0 +1,8 @@
1
+ module OpenAssets
2
+ module Cache
3
+ autoload :SQLiteBase, 'openassets/cache/sqlite_base'
4
+ autoload :TransactionCache, 'openassets/cache/transaction_cache'
5
+ autoload :SSLCertificateCache, 'openassets/cache/ssl_certificate_cache'
6
+ autoload :OutputCache, 'openassets/cache/output_cache'
7
+ end
8
+ end
@@ -0,0 +1,43 @@
1
+ module OpenAssets
2
+ module Cache
3
+
4
+ # An object that can be used for caching coloring transaction output in a Sqlite database.
5
+ class OutputCache < SQLiteBase
6
+
7
+ def setup
8
+ db.execute <<-SQL
9
+ CREATE TABLE IF NOT EXISTS Output(
10
+ TransactionHash BLOB,
11
+ OutputIndex INT,
12
+ Value BigInt,
13
+ Script BLOB,
14
+ AssetId BLOB,
15
+ AssetQuantity INT,
16
+ OutputType INT,
17
+ Metadata BLOB,
18
+ PRIMARY KEY (TransactionHash, OutputIndex))
19
+ SQL
20
+ end
21
+
22
+ # Get a cached transaction output
23
+ # @param[String] txid The transaction id.
24
+ # @param[Integer] index The index of the output in the transaction.
25
+ # @return[OpenAssets::Protocol::TransactionOutput] The output for the txid and index provided if it is found in the cache, or nil otherwise.
26
+ def get(txid, index)
27
+ rows = db.execute('SELECT Value,Script,AssetId,AssetQuantity,OutputType,Metadata FROM Output WHERE TransactionHash = ? AND OutputIndex = ?', [txid, index])
28
+ return nil if rows.empty?
29
+ script = Bitcoin::Script.from_string(rows[0][1])
30
+ OpenAssets::Protocol::TransactionOutput.new(rows[0][0], script, rows[0][2], rows[0][3], rows[0][4], rows[0][5])
31
+ end
32
+
33
+ # Put a transaction output
34
+ # @param[String] txid The transaction id.
35
+ # @param[Integer] index The index of the output in the transaction.
36
+ # @param[OpenAssets::Protocol::TransactionOutput] output The output to save.
37
+ def put(txid, index, output)
38
+ db.execute('INSERT INTO Output (TransactionHash, OutputIndex, Value,Script,AssetId,AssetQuantity,OutputType,Metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
39
+ [txid, index, output.value, output.script.to_string, output.asset_id, output.asset_quantity, output.output_type, output.metadata])
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ require 'sqlite3'
2
+
3
+ module OpenAssets
4
+ module Cache
5
+
6
+ # The base class of SQLite3 cache implementation.
7
+ class SQLiteBase
8
+
9
+ attr_reader :db
10
+
11
+ # Initializes the connection to the database, and creates the table if needed.
12
+ # @param[String] path The path to the database file. Use ':memory:' for an in-memory database.
13
+ def initialize(path)
14
+ @db = SQLite3::Database.new path
15
+ setup
16
+ end
17
+
18
+ # Setup table ddl, implements by subclass.
19
+ def setup
20
+ raise StandardError.new('need setup method implementation.')
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ module OpenAssets
2
+ module Cache
3
+ class SSLCertificateCache < SQLiteBase
4
+
5
+ def initialize
6
+ path = OpenAssets.configuration ? OpenAssets.configuration[:cache] : ':memory:'
7
+ super(path)
8
+ end
9
+
10
+ def setup
11
+ db.execute <<-SQL
12
+ CREATE TABLE IF NOT EXISTS SslCertificate(
13
+ Url TEXT,
14
+ Subject TEXT,
15
+ ExpireDate TEXT,
16
+ PRIMARY KEY (Url))
17
+ SQL
18
+ end
19
+
20
+ # Return the subject value which defined by ssl certificate.
21
+ # @param[String] url The URL of asset definition file.
22
+ # @return[String] The subject value. If not found, return nil.
23
+ def get(url)
24
+ rows = db.execute('SELECT Subject,ExpireDate FROM SslCertificate WHERE Url = ?', [url])
25
+ return nil if rows.empty?
26
+ if rows[0][1].to_i < Time.now.to_i
27
+ db.execute('DELETE FROM SslCertificate where Url = ?', [url])
28
+ nil
29
+ else
30
+ rows[0][0]
31
+ end
32
+ end
33
+
34
+ # Saves a serialized transaction in cache.
35
+ # @param[String] url The URL of asset definition file.
36
+ # @param[String] subject The SSL Certificate subject value.
37
+ # @param[Time] expire_date The expire date of SSL Certificate.
38
+ def put(url, subject, expire_date)
39
+ db.execute('INSERT INTO SslCertificate (Url, Subject, ExpireDate) VALUES (?, ?, ?)', [url, subject, expire_date.to_i])
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ module OpenAssets
2
+ module Cache
3
+
4
+ # An object that can be used for caching serialized transaction in a Sqlite database.
5
+ class TransactionCache < SQLiteBase
6
+
7
+ def setup
8
+ db.execute <<-SQL
9
+ CREATE TABLE IF NOT EXISTS Tx(
10
+ TransactionHash BLOB,
11
+ SerializedTx BLOB,
12
+ PRIMARY KEY (TransactionHash))
13
+ SQL
14
+ end
15
+
16
+ # Return the serialized transaction.
17
+ # @param[String] txid The transaction id.
18
+ # @return[String] The serialized transaction. If not found transaction, return nil.
19
+ def get(txid)
20
+ rows = db.execute('SELECT SerializedTx FROM Tx WHERE TransactionHash = ?', [txid])
21
+ rows.empty? ? nil : rows[0][0]
22
+ end
23
+
24
+ # Saves a serialized transaction in cache.
25
+ # @param[String] txid A transaction id.
26
+ # @param[String] serialized_tx A a hex-encoded serialized transaction.
27
+ def put(txid, serialized_tx)
28
+ db.execute('INSERT INTO Tx (TransactionHash, SerializedTx) VALUES (?, ?)', [txid, serialized_tx])
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module OpenAssets
2
+ class Error < StandardError
3
+
4
+ end
5
+ end
@@ -0,0 +1,55 @@
1
+ module OpenAssets
2
+ module MethodFilter
3
+
4
+ module Filters
5
+
6
+ def before_filter(method, options = {})
7
+ @before_filters ||= {}
8
+ @before_filters[method] = options
9
+ end
10
+
11
+ def filtered_method?(method, options)
12
+ if options.has_key? :include
13
+ return true if options[:include].include? method.intern
14
+ false
15
+ elsif options.has_key? :exclude
16
+ return false if options[:exclude].include? method.intern
17
+ true
18
+ else
19
+ true
20
+ end
21
+ end
22
+
23
+ private
24
+ def method_added(name)
25
+ @before_filters ||= {}
26
+ @modified_methods ||= []
27
+ return if @modified_methods.include?(name)
28
+ return if @before_filters.include?(name)
29
+ return if /with_filters$/ =~ name.to_s
30
+ return if /without_filters$/ =~ name.to_s
31
+ @modified_methods << name
32
+
33
+ name = name.to_s
34
+ alias_method( "#{name}_without_filters", name)
35
+ before_filters = @before_filters
36
+
37
+ define_method("#{name}_with_filters") do |*args|
38
+ before_filters.each do |filter_name, options|
39
+ method(filter_name).call if self.class.filtered_method?(name, options)
40
+ end
41
+ result = method("#{name}_without_filters").call(*args)
42
+ result
43
+ end
44
+
45
+ alias_method(name, "#{name}_with_filters")
46
+ @modified_methods.delete_if { |x| name == x }
47
+ end
48
+ end
49
+
50
+ def self.included(receiver)
51
+ receiver.extend(Filters)
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,10 @@
1
+ module OpenAssets
2
+ module Protocol
3
+ autoload :MarkerOutput, 'openassets/protocol/marker_output'
4
+ autoload :TransactionOutput, 'openassets/protocol/transaction_output'
5
+ autoload :OutputType, 'openassets/protocol/output_type'
6
+ autoload :AssetDefinitionLoader, 'openassets/protocol/asset_definition_loader'
7
+ autoload :HttpAssetDefinitionLoader, 'openassets/protocol/http_asset_definition_loader'
8
+ autoload :AssetDefinition, 'openassets/protocol/asset_definition'
9
+ end
10
+ end
@@ -0,0 +1,118 @@
1
+ require 'rest-client'
2
+ require 'httpclient'
3
+ module OpenAssets
4
+ module Protocol
5
+
6
+ # The Definition of Open Asset
7
+ class AssetDefinition
8
+ include OpenAssets::MethodFilter
9
+
10
+ before_filter :clear_poa_cache, {:include => [:issuer=, :asset_definition_url=, :link_to_website=]}
11
+
12
+ attr_accessor :asset_definition_url
13
+
14
+ attr_accessor :asset_ids
15
+ attr_accessor :name_short
16
+ attr_accessor :name
17
+ attr_accessor :contract_url
18
+ attr_accessor :issuer
19
+ attr_accessor :description
20
+ attr_accessor :description_mime
21
+ attr_accessor :type
22
+ attr_accessor :divisibility
23
+ attr_accessor :link_to_website
24
+ attr_accessor :icon_url
25
+ attr_accessor :image_url
26
+ attr_accessor :version
27
+ attr_accessor :proof_of_authenticity
28
+
29
+ def initialize
30
+ @asset_ids = []
31
+ @version = '1.0'
32
+ @divisibility = 0
33
+ end
34
+
35
+ # Parse the JSON obtained from the json String, and create a AssetDefinition object.
36
+ # @param[String]
37
+ def self.parse_json(json)
38
+ parsed_json = JSON.parse(json)
39
+ definition = new
40
+ definition.asset_ids = parsed_json['asset_ids']
41
+ definition.name_short = parsed_json['name_short']
42
+ definition.name = parsed_json['name']
43
+ definition.contract_url = parsed_json['contract_url']
44
+ definition.issuer = parsed_json['issuer']
45
+ definition.description = parsed_json['description']
46
+ definition.description_mime = parsed_json['description_mime']
47
+ definition.type = parsed_json['type']
48
+ definition.divisibility = parsed_json['divisibility']
49
+ definition.link_to_website = parsed_json['link_to_website']
50
+ definition.icon_url = parsed_json['icon_url']
51
+ definition.image_url = parsed_json['image_url']
52
+ definition.version = parsed_json['version']
53
+ definition
54
+ end
55
+
56
+ def include_asset_id?(asset_id)
57
+ return false if asset_ids.nil? || asset_ids.empty?
58
+ asset_ids.include?(asset_id)
59
+ end
60
+
61
+ # Convert Asset Definition to json format.
62
+ def to_json
63
+ h = to_hash
64
+ h.delete('proof_of_authenticity')
65
+ h.to_json
66
+ end
67
+
68
+ def to_hash
69
+ instance_variables.inject({}) do |result, var|
70
+ key = var.to_s
71
+ key.slice!(0) if key.start_with?('@')
72
+ result.update(key => instance_variable_get(var))
73
+ end
74
+ end
75
+
76
+ # Check Proof of authenticity.
77
+ # SSL certificate subject matches issuer.
78
+ def proof_of_authenticity
79
+ @proof_of_authenticity ||= calc_proof_of_authenticity
80
+ end
81
+
82
+ private
83
+ def calc_proof_of_authenticity
84
+ result = false
85
+ if !asset_definition_url.nil? && link_to_website
86
+ subject = ssl_certificate_subject
87
+ return true if !subject.nil? && subject == issuer
88
+ end
89
+ result
90
+ end
91
+
92
+ def clear_poa_cache
93
+ @proof_of_authenticity = nil
94
+ end
95
+
96
+ def ssl_certificate_subject
97
+ cache = OpenAssets::Cache::SSLCertificateCache.new
98
+ subject = cache.get(asset_definition_url)
99
+ if subject.nil?
100
+ client = HTTPClient.new
101
+ client.redirect_uri_callback = ->(uri, res) {res.header['location'][0]}
102
+ response = client.get(asset_definition_url, :follow_redirect => true)
103
+ cert = response.peer_cert
104
+ unless cert.nil?
105
+ subjects = cert.subject.to_a
106
+ o = subjects.find{|x|x[0] == 'O'}
107
+ if !o.nil? && o.length > 2
108
+ subject = o[1]
109
+ cache.put(asset_definition_url, subject, cert.not_after)
110
+ end
111
+ end
112
+ end
113
+ subject
114
+ end
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,43 @@
1
+ module OpenAssets
2
+ module Protocol
3
+
4
+ class AssetDefinitionLoader
5
+ extend Bitcoin::Util
6
+ extend OpenAssets::Util
7
+
8
+ attr_reader :loader
9
+
10
+ def initialize(url)
11
+ if url.start_with?('http://') || url.start_with?('https://')
12
+ @loader = HttpAssetDefinitionLoader.new(url)
13
+ end
14
+ end
15
+
16
+ # load Asset Definition File
17
+ # @return[OpenAssets::Protocol::AssetDefinition] loaded asset definition object
18
+ def load_definition
19
+ @loader.load if @loader
20
+ end
21
+
22
+ # create redeem script of asset definition file using p2sh
23
+ # @param[String] url The asset definition url.
24
+ # @param[String] to The open asset address to send the asset to.
25
+ # @return[Bitcoin::Script] redeem script.
26
+ def self.create_pointer_redeem_script(url, to)
27
+ asset_def = "u=#{url}".bytes.map{|b|b.to_s(16)}.join
28
+ btc_addr = oa_address_to_address(to)
29
+ script = Bitcoin::Script.from_string("#{asset_def}")
30
+ Bitcoin::Script.new(script.append_opcode(Bitcoin::Script::OP_DROP).to_payload + Bitcoin::Script.to_address_script(btc_addr))
31
+ end
32
+
33
+ # create ps2s script which specify asset definition pointer
34
+ # @param[String] url The asset definition url.
35
+ # @param[String] to The open asset address to send the asset to.
36
+ # @return[Bitcoin::Script] p2sh script.
37
+ def self.create_pointer_p2sh(url, to)
38
+ redeem_script = create_pointer_redeem_script(url, to)
39
+ Bitcoin::Script.new(Bitcoin::Script.to_p2sh_script(hash160(redeem_script.to_payload.bth)))
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ module OpenAssets
2
+ module Protocol
3
+
4
+ # Asset Definition loader for http or https uri scheme
5
+ class HttpAssetDefinitionLoader
6
+
7
+ attr_reader :url
8
+
9
+ def initialize(url)
10
+ @url = url
11
+ end
12
+
13
+ # load asset definition
14
+ def load
15
+ begin
16
+ definition = AssetDefinition.parse_json(RestClient::Request.execute(:method => :get, :url => url, :timeout => 10, :open_timeout => 10))
17
+ definition.asset_definition_url = url
18
+ definition
19
+ rescue => e
20
+ puts e
21
+ nil
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,128 @@
1
+ require 'rest-client'
2
+ module OpenAssets
3
+ module Protocol
4
+
5
+ class MarkerOutput
6
+ include OpenAssets::Util
7
+ extend OpenAssets::Util
8
+
9
+ MAX_ASSET_QUANTITY = 2 ** 63 -1
10
+
11
+ # A tag indicating thath this transaction is an Open Assets transaction.
12
+ OAP_MARKER = "4f41"
13
+ # The major revision number of the Open Assets Protocol.(1=0x0100)
14
+ VERSION = "0100"
15
+
16
+ attr_accessor :asset_quantities
17
+ attr_accessor :metadata
18
+
19
+ # @param [Array] asset_quantities The asset quantity array
20
+ # @param [String] metadata The metadata in the marker output.
21
+ def initialize(asset_quantities, metadata = '')
22
+ @asset_quantities = asset_quantities
23
+ @metadata = metadata
24
+ end
25
+
26
+ # Serialize the marker output into a Open Assets Payload buffer.
27
+ # @return [String] The serialized payload.
28
+ def to_payload
29
+ payload = [OAP_MARKER, VERSION]
30
+ asset_quantity_count = Bitcoin::Protocol.pack_var_int(@asset_quantities.length).unpack("H*")
31
+ payload << sort_count(asset_quantity_count[0])
32
+ @asset_quantities.map{|q|payload << encode_leb128(q)}
33
+ @metadata ||= ''
34
+ metadata_length = Bitcoin::Protocol.pack_var_int(@metadata.length).unpack("H*")
35
+ payload << sort_count(metadata_length[0])
36
+ payload << @metadata.bytes.map{|b| sprintf("%02x", b)}.join
37
+ payload.join
38
+ end
39
+
40
+ # Deserialize the marker output payload.
41
+ # @param [String] payload The Open Assets Payload.
42
+ # @return [OpenAssets::Protocol::MarkerOutput] The marker output object.
43
+ def self.deserialize_payload(payload)
44
+ return nil unless valid?(payload)
45
+ payload = payload[8..-1] # exclude OAP_MARKER,VERSION
46
+ asset_quantity, payload = parse_asset_qty(payload)
47
+ list = to_bytes(payload).map{|x|(x.to_i(16)>=128 ? x : x+"|")}.join.split('|')[0..(asset_quantity - 1)].join
48
+ asset_quantities = decode_leb128(list)
49
+ meta = to_bytes(payload[list.size..-1])
50
+ metadata = meta.empty? ? '' : meta[1..-1].map{|x|x.to_i(16).chr}.join
51
+ new(asset_quantities, metadata)
52
+ end
53
+
54
+ # Parses an output and returns the payload if the output matches the right pattern for a marker output,
55
+ # @param [Bitcoin::Script] output_script: The output script to be parsed.
56
+ # @return [String] The byte string of the marker output payload if the output fits the pattern, nil otherwise.
57
+ def self.parse_script(output_script)
58
+ data = Bitcoin::Script.new(output_script).get_op_return_data
59
+ return data if valid?(data)
60
+ end
61
+
62
+ # Creates an output script containing an OP_RETURN and a PUSHDATA from payload.
63
+ # @return [Bitcoin::Script] the output script.
64
+ def build_script
65
+ Bitcoin::Script.from_string("OP_RETURN #{to_payload}")
66
+ end
67
+
68
+ private
69
+ def self.parse_asset_qty(payload)
70
+ bytes = to_bytes(payload)
71
+ case bytes[0]
72
+ when "fd" then
73
+ [(bytes[1]+bytes[2]).to_i(16), payload[6..-1]]
74
+ when "fe" then
75
+ [(bytes[1]+bytes[2]+bytes[3]+bytes[4]).to_i(16),payload[10..-1]]
76
+ else
77
+ [bytes[0].to_i(16),payload[2..-1]]
78
+ end
79
+ end
80
+
81
+ def sort_count(count)
82
+ bytes = to_bytes(count)
83
+ case bytes[0]
84
+ when "fd" then
85
+ tmp = count[2..3]
86
+ count[2..3] = count[4..5]
87
+ count[4..5] = tmp
88
+ count
89
+ when "fe" then
90
+ tmp_1 = count[2..3]
91
+ tmp_2 = count[4..5]
92
+ count[2..3] = count[8..9]
93
+ count[8..9] = tmp_1
94
+ count[4..5] = count[6..7]
95
+ count[6..7] = tmp_2
96
+ count
97
+ else
98
+ count
99
+ end
100
+ end
101
+
102
+ # validate marker output format
103
+ # @param[String] data marker output data with start with 4f41
104
+ def self.valid?(data)
105
+ return false if data.nil?
106
+ # check open assets marker
107
+ return false unless data.start_with?(OAP_MARKER + VERSION)
108
+ # check asset quantity
109
+ offset = [OAP_MARKER + VERSION].pack('H*').length
110
+ count, offset = read_var_integer(data, offset)
111
+ return false unless count
112
+ # check metadata
113
+ count.times do
114
+ quantity, length = read_leb128(data, offset)
115
+ return false if quantity.nil? || (length - offset) > 9
116
+ offset = length
117
+ end
118
+ # check metadata
119
+ length, offset = read_var_integer(data, offset)
120
+ return false unless length
121
+ return false if [data].pack('H*').bytes.length < length + offset
122
+ true
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+ end