mastercoin-ruby 0.0.2

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.
@@ -0,0 +1,146 @@
1
+ module Mastercoin
2
+ class ExodusPayment
3
+
4
+ attr_accessor :coins_bought, :bonus_bought, :address, :tx, :time_included
5
+
6
+ def to_s
7
+ "Bought #{self.coins_bought} Mastercoins and got a #{self.bonus_bought} Mastercoins extra."
8
+ end
9
+
10
+ def to_json
11
+ {coins_bought: self.coins_bought, bonus_bought: self.bonus_bought}.to_json
12
+ end
13
+
14
+ def total_amount
15
+ self.coins_bought + self.bonus_bought
16
+ end
17
+
18
+ def self.from_transaction(hash)
19
+ buying = ExodusPayment.new
20
+ buying.coins_bought = 0
21
+ buying.bonus_bought = 0
22
+
23
+ store = Mastercoin.storage
24
+ tx = store.get_tx(hash)
25
+ raise TransactionNotFoundException.new("Could not find the given transaction with #{hash}. Perhaps your blockchain is not up-to-date?") unless tx
26
+ buying.tx = tx
27
+ block_time = store.get_block_by_tx(tx.hash).time
28
+ buying.time_included = block_time
29
+ highest = ExodusPayment.highest_output_for_tx(tx)
30
+ buying.address = highest
31
+
32
+ exodus_output = tx.outputs.find{|x| x.to_hash(with_address:true)["address"] == Mastercoin::EXODUS_ADDRESS}
33
+
34
+ if tx.get_block.depth <= Mastercoin::END_BLOCK
35
+ btc_amount = (exodus_output.value / 1e8)
36
+ bought = btc_amount * 100
37
+ buying.coins_bought += bought
38
+ date_difference = (Mastercoin::END_TIME.to_i - block_time.to_i) / 60.0 / 60 / 24 / 7
39
+ if date_difference > 0
40
+ bonus = (btc_amount * 100 * (date_difference * 0.1))
41
+
42
+ buying.bonus_bought += sprintf("%0.08f", bonus).to_f
43
+ end
44
+ end
45
+ return buying
46
+ end
47
+
48
+ def self.highest_output_for_tx(tx)
49
+ result = {}
50
+ output_hash = tx.in.collect{|x| x.get_prev_out.to_hash(with_address: true) }
51
+
52
+ output_hash.each do |output|
53
+ address = output['address']
54
+ result[address] ||= 0
55
+ result[address] += output['value'].to_f
56
+ end
57
+
58
+ highest_input = result.sort{|x,y| y[1] <=> x[1]}
59
+ highest_input = highest_input[0][0]
60
+ end
61
+
62
+ # This is a very slow and probably very inefficient way to calculate the coins bought
63
+ # TODO: Please rewrite
64
+ def self.from_address(address)
65
+ buying = ExodusPayment.new
66
+ @used = {}
67
+ @rejected_tx = []
68
+
69
+ buying.address = address
70
+
71
+ buying.coins_bought = 0
72
+ buying.bonus_bought = 0
73
+
74
+ store = Mastercoin.storage
75
+ txouts = store.get_txouts_for_address(address)
76
+
77
+ # 1. Get all outputs for an address
78
+ # 2. Check to see if this ouput has a next input for the Exodus address
79
+ # A. Get the tx for the next input if any exist
80
+ # B. Check if the tx has any outputs with the Exodus address
81
+ # 3. If so find which input did the total best payments to Exodus
82
+ # 4. Check the inputs for Exodus output and award the one with the highest total
83
+
84
+ txouts.each do |txout|
85
+ Mastercoin.log.debug("Checking txout: #{txout.to_hash(with_address: true)}")
86
+ input = txout.get_next_in
87
+
88
+ if input
89
+ tx = input.get_tx
90
+ next if @rejected_tx.include?(tx.hash)
91
+
92
+ block_time = store.get_block_by_tx(tx.hash).time
93
+
94
+ if tx.get_block.depth > Mastercoin::END_BLOCK
95
+ Mastercoin.log.debug("Transaction after end date: Rejecting")
96
+ @rejected_tx << tx.hash
97
+ next
98
+ end
99
+
100
+ addresses = tx.outputs.collect{|x| x.to_hash(with_address: true)["address"] }
101
+
102
+ unless addresses.include?(Mastercoin::EXODUS_ADDRESS)
103
+ Mastercoin.log.debug("TX #{tx.hash} does not include transaction to Exodus")
104
+ @rejected_tx << tx.hash
105
+ next
106
+ else
107
+ Mastercoin.log.debug("TX #{tx.hash} is a transaction to Exodus")
108
+ end
109
+
110
+ highest_input = ExodusPayment.highest_output_for_tx(tx)
111
+
112
+ Mastercoin.log.debug("Highest input for #{tx.hash} is #{highest_input}")
113
+
114
+ # Get all the inputs from this transaction and see which has the higest one. the Funds belong to the input with the highest value
115
+ tx.out.each do |output|
116
+ if output.get_addresses.flatten.include?(Mastercoin::EXODUS_ADDRESS) && !@used.keys.include?(tx.hash)
117
+ Mastercoin.log.debug("TX #{tx.hash} is not inside our used tx hash: #{@used.keys}")
118
+
119
+ unless txout.get_address == highest_input
120
+ Mastercoin.log.debug("This is not the highest input; can't give the coins. #{txout.get_address} we needed #{highest_input}")
121
+ next
122
+ else
123
+ end
124
+
125
+ @used[tx.hash] = highest_input
126
+
127
+ btc_amount = (output.value / 1e8)
128
+ bought = btc_amount * 100
129
+ buying.coins_bought += bought
130
+ date_difference = (Mastercoin::END_TIME.to_i - block_time.to_i) / 60.0 / 60 / 24 / 7
131
+ if date_difference > 0
132
+ bonus = (btc_amount * 100 * (date_difference * 0.1))
133
+
134
+ buying.bonus_bought += sprintf("%0.08f", bonus).to_f
135
+ end
136
+ else
137
+ Mastercoin.log.debug("This is not the Exodus output; probably change address")
138
+ end
139
+ end
140
+ end
141
+ end
142
+ return buying
143
+ end
144
+ end
145
+ end
146
+
@@ -0,0 +1,67 @@
1
+ module Mastercoin
2
+ class SimpleSend
3
+ attr_accessor :transaction_type, :currency_id, :amount, :receiving_address, :sequence
4
+
5
+ # Supply the amount in 'dacoinminster's
6
+ def initialize(options= {})
7
+ self.transaction_type = Mastercoin::TRANSACTION_SIMPLE_SEND
8
+ self.currency_id = options[:currency_id]
9
+ self.amount = options[:amount]
10
+ self.receiving_address = options[:receiving_address]
11
+ end
12
+
13
+ # hardcode the sequence for a public key simple send since it's always fits inside a public key
14
+ # Please note that we start at 01 - 00 will generate unvalid ECDSA points somehow
15
+ def public_key_sequence
16
+ 01
17
+ end
18
+
19
+ def self.decode_from_compressed_public_key(public_key)
20
+ simple_send = SimpleSend.new
21
+ simple_send.transaction_type = Mastercoin::TRANSACTION_SIMPLE_SEND
22
+ simple_send.currency_id = public_key[12..19].to_i(16)
23
+ simple_send.amount = public_key[20..35].to_i(16)
24
+ simple_send.sequence = public_key[2..3].to_i(16)
25
+ return simple_send
26
+ end
27
+
28
+ def encode_to_compressed_public_key
29
+ raw = "02" + (self.public_key_sequence.to_i.to_s(16).rjust(2, "0") + self.transaction_type.to_i.to_s(16).rjust(8,"0") + self.currency_id.to_i.to_s(16).rjust(8, "0") + self.amount.to_i.to_s(16).rjust(16, "0"))
30
+ raw = raw.ljust(66,"0")
31
+
32
+ return raw
33
+ end
34
+
35
+ def encode_to_address
36
+ raw = (self.get_sequence.to_i.to_s(16).rjust(2, "0") + self.transaction_type.to_i.to_s(16).rjust(8,"0") + self.currency_id.to_i.to_s(16).rjust(8, "0") + self.amount.to_i.to_s(16).rjust(16, "0") + "000000")
37
+ Bitcoin.hash160_to_address(raw)
38
+ end
39
+
40
+ def self.decode_from_address(raw_address)
41
+ simple_send = Mastercoin::SimpleSend.new
42
+ decoded = Bitcoin.decode_base58(raw_address)
43
+ simple_send.sequence = decoded[2..3].to_i(16)
44
+ simple_send.transaction_type = decoded[4..11].to_i(16)
45
+ simple_send.currency_id = decoded[12..19].to_i(16)
46
+ simple_send.amount = decoded[20..35].to_i(16)
47
+ return simple_send
48
+ end
49
+
50
+ def get_sequence(bitcoin_address = nil)
51
+ bitcoin_address ||= self.receiving_address
52
+ Mastercoin::Util.get_sequence(bitcoin_address)
53
+ end
54
+
55
+ def looks_like_mastercoin?
56
+ Mastercoin::TRANSACTION_TYPES.keys.include?(self.transaction_type.to_s) && Mastercoin::CURRENCY_IDS.keys.include?(self.currency_id.to_s)
57
+ end
58
+
59
+ def to_s
60
+ "SimpleSend transaction for %.8f #{self.currency_id_text}." % (self.amount / 1e8)
61
+ end
62
+
63
+ def currency_id_text
64
+ Mastercoin::CURRENCY_IDS[self.currency_id.to_s]
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,94 @@
1
+ module Mastercoin
2
+ class Transaction
3
+ class NoMastercoinTransactionException < StandardError;end;
4
+
5
+ attr_accessor :btc_tx
6
+ attr_accessor :transaction_type, :currency_id, :amount
7
+ attr_accessor :source_address
8
+ attr_accessor :data_addresses, :rejected_outputs, :target_address, :multisig
9
+
10
+ def initialize(tx_hash)
11
+ @store = Mastercoin.storage
12
+ self.data_addresses = []
13
+ self.rejected_outputs = []
14
+ self.btc_tx = @store.get_tx(tx_hash)
15
+
16
+ raise TransactionNotFoundException.new("Transaction #{tx_hash} could not be found. Is your blockchain up to date?") if self.btc_tx.nil?
17
+
18
+ unless self.has_genesis_as_output?
19
+ raise NoMastercoinTransaction.new("This transaction does not contain a txout to the genesis address, invalid.")
20
+ end
21
+
22
+ unless self.has_three_outputs?
23
+ raise NoMastercoinTransaction.new("This transaction does not contain three outputs, invalid.")
24
+ end
25
+
26
+ if self.btc_tx.outputs.collect{|x| x.script.is_multisig?}.include?(true)
27
+ self.multisig = true
28
+ else
29
+ self.multisig = false
30
+ end
31
+
32
+ self.source_address = Mastercoin::ExodusPayment.highest_output_for_tx(self.btc_tx)
33
+
34
+ if multisig
35
+ self.btc_tx.outputs.each do |output|
36
+ if output.get_address == Mastercoin::EXODUS_ADDRESS
37
+ # Do nothing yet; this is simply the exodus address
38
+ elsif output.script.is_multisig?
39
+ keys = output.script.get_multisig_pubkeys.collect{|x| x.unpack("H*")[0]}
40
+ keys.each do |key|
41
+ self.data_addresses << Mastercoin::SimpleSend.decode_from_compressed_public_key(key) if Mastercoin::SimpleSend.decode_from_compressed_public_key(key).looks_like_mastercoin?
42
+ end
43
+ else
44
+ #TODO Change this not really too trust worthy
45
+ self.target_address = output.get_address if output.value == 0.00006 * 1e8
46
+ end
47
+ end
48
+ else
49
+ self.btc_tx.outputs.each do |output|
50
+ if output.get_address == Mastercoin::EXODUS_ADDRESS
51
+ # Do nothing yet; this is simply the exodus address
52
+ elsif Mastercoin::SimpleSend.decode_from_address(output.get_address).looks_like_mastercoin? # This looks like a data packet
53
+ self.data_addresses << Mastercoin::SimpleSend.decode_from_address(output.get_address)
54
+ end
55
+ end
56
+
57
+ self.btc_tx.outputs.each do |output|
58
+ address = output.get_address
59
+ sequence = Mastercoin::Util.get_sequence(address)
60
+ if self.data_addresses[0].sequence.to_s == sequence.to_s
61
+ self.target_address = address
62
+ end
63
+ end
64
+ end
65
+
66
+ self.data_addresses.sort!{|x, y| x.sequence.to_i <=> y.sequence.to_i }
67
+
68
+ self.analyze_addresses!
69
+ end
70
+
71
+ def analyze_addresses!
72
+ address = self.data_addresses[0]
73
+ self.transaction_type = address.transaction_type
74
+ self.currency_id = address.currency_id
75
+ self.amount = address.amount
76
+ end
77
+
78
+ def has_three_outputs?
79
+ self.btc_tx.outputs.size >= 3
80
+ end
81
+
82
+ def has_genesis_as_output?
83
+ self.btc_tx.outputs.collect{|x| x.get_address == Mastercoin::EXODUS_ADDRESS}.any?
84
+ end
85
+
86
+ def to_s
87
+ if self.transaction_type.to_s == "0"
88
+ "Simple send:: Sent #{self.amount / 1e8} '#{Mastercoin::CURRENCY_IDS[self.currency_id.to_s]}' to #{self.target_address}"
89
+ else
90
+ "Unknown transaction: #{self.transaction_type}"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,24 @@
1
+ module Mastercoin
2
+ class Util
3
+ def self.valid_ecdsa_point?(pub_key)
4
+ begin
5
+ Bitcoin::Key.new(nil, pub_key).addr
6
+ rescue OpenSSL::PKey::EC::Point::Error
7
+ return false
8
+ end
9
+
10
+ return true
11
+ end
12
+
13
+ def self.get_sequence(bitcoin_address)
14
+ decoded = Bitcoin.decode_base58(bitcoin_address)
15
+
16
+ seq = decoded[2..3].to_i(16) - 1
17
+ if seq > 255
18
+ seq -= 255
19
+ end
20
+
21
+ return seq
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ require 'bitcoin'
2
+ require 'logger'
3
+
4
+ module Mastercoin
5
+ class TransactionNotFoundException < StandardError;end
6
+ autoload :SimpleSend, 'mastercoin-ruby/simple_send'
7
+ autoload :ExodusPayment, 'mastercoin-ruby/exodus_payment'
8
+ autoload :Transaction, 'mastercoin-ruby/transaction'
9
+ autoload :Util, 'mastercoin-ruby/util'
10
+ autoload :BitcoinWrapper, 'mastercoin-ruby/bitcoin_wrapper'
11
+
12
+ TRANSACTION_SIMPLE_SEND = "0"
13
+
14
+ TRANSACTION_TYPES = {
15
+ TRANSACTION_SIMPLE_SEND => "Simple transfer",
16
+ "10" => "Mark saving",
17
+ "11" => "Mark compromised",
18
+ "20" => "Currency trade offer bitcoins",
19
+ "21" => "Currency trade offer master-coin derived",
20
+ "22" => "Currency trade offer accept",
21
+ "30" => "Register data-stream",
22
+ "40" => "Bet offer",
23
+ "100" => "Create child currency"
24
+ }
25
+
26
+ CURRENCY_IDS = {
27
+ "1" => "Mastercoin",
28
+ "2" => "Test Mastercoin"
29
+ }
30
+
31
+ EXODUS_ADDRESS = "1EXoDusjGwvnjZUyKkxZ4UHEf77z6A5S4P"
32
+ END_TIME = Time.new(2013,9,01,00,00,00, "+00:00")
33
+ END_BLOCK = 255365
34
+
35
+ def self.set_storage(storage_string)
36
+ @storage_string = storage_string
37
+ end
38
+
39
+ def self.storage
40
+ Bitcoin.network ||= :bitcoin
41
+ @@storage ||= Bitcoin::Storage.sequel(:db => @storage_string)
42
+ return @@storage
43
+ end
44
+
45
+ def self.init_logger(level = Logger::INFO)
46
+ @@log ||= Logger.new(STDOUT)
47
+ @@log.level = level
48
+ @@log
49
+ end
50
+
51
+ def self.log
52
+ @@log ||= Mastercoin.init_logger
53
+ end
54
+ end
@@ -0,0 +1,77 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "mastercoin-ruby"
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Maran"]
12
+ s.date = "2013-09-25"
13
+ s.description = "Basic implementation of the Mastercoin protocol."
14
+ s.email = "maran.hidskes@gmail.com"
15
+ s.executables = ["exodus_payment", "mastercoin_transaction", "simple_send", "wallet.rb"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE.txt",
18
+ "README.md"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.md",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "bin/exodus_payment",
29
+ "bin/mastercoin_transaction",
30
+ "bin/simple_send",
31
+ "bin/wallet.rb",
32
+ "lib/mastercoin-ruby.rb",
33
+ "lib/mastercoin-ruby/bitcoin_wrapper.rb",
34
+ "lib/mastercoin-ruby/exodus_payment.rb",
35
+ "lib/mastercoin-ruby/simple_send.rb",
36
+ "lib/mastercoin-ruby/transaction.rb",
37
+ "lib/mastercoin-ruby/util.rb",
38
+ "mastercoin-ruby.gemspec",
39
+ "spec/simple_send.rb"
40
+ ]
41
+ s.homepage = "http://github.com/maran/mastercoin-ruby"
42
+ s.licenses = ["MIT"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = "1.8.24"
45
+ s.summary = "Ruby library for the Mastercoin protocol"
46
+
47
+ if s.respond_to? :specification_version then
48
+ s.specification_version = 3
49
+
50
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
51
+ s.add_runtime_dependency(%q<bitcoin-ruby>, ["~> 0.0.1"])
52
+ s.add_runtime_dependency(%q<sequel>, ["~> 4.1.1"])
53
+ s.add_runtime_dependency(%q<thor>, [">= 0"])
54
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
55
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
56
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.7"])
57
+ s.add_development_dependency(%q<rspec>, [">= 0"])
58
+ else
59
+ s.add_dependency(%q<bitcoin-ruby>, ["~> 0.0.1"])
60
+ s.add_dependency(%q<sequel>, ["~> 4.1.1"])
61
+ s.add_dependency(%q<thor>, [">= 0"])
62
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
63
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
64
+ s.add_dependency(%q<jeweler>, ["~> 1.8.7"])
65
+ s.add_dependency(%q<rspec>, [">= 0"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<bitcoin-ruby>, ["~> 0.0.1"])
69
+ s.add_dependency(%q<sequel>, ["~> 4.1.1"])
70
+ s.add_dependency(%q<thor>, [">= 0"])
71
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
72
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.8.7"])
74
+ s.add_dependency(%q<rspec>, [">= 0"])
75
+ end
76
+ end
77
+
@@ -0,0 +1,64 @@
1
+ require 'mastercoin-ruby'
2
+
3
+ describe Mastercoin::SimpleSend do
4
+ before do
5
+ @simple_send = Mastercoin::SimpleSend.new(currency_id: 2, amount: 50, receiving_address: "184mQaxRYwiU2jqUE852FZGQvbZyRhcDSu")
6
+ end
7
+
8
+ context "Encoding and decoding addresses" do
9
+ it "Should output a valid looking bitcoin address" do
10
+ address = @simple_send.encode_to_address
11
+ address.should eq("17vrMab8gQx72eCEaUxJzL4fg5VwEUumJQ")
12
+ end
13
+
14
+ it "Should decode a valid looking bitcoin address" do
15
+ simple_send = Mastercoin::SimpleSend.decode_from_address("17vrMab8gQx72eCEaUxJzL4fg5VwEUumJQ")
16
+ simple_send.currency_id.should eq(2)
17
+ simple_send.amount.should eq(50)
18
+ simple_send.transaction_type.to_s.should eq(Mastercoin::TRANSACTION_SIMPLE_SEND)
19
+ end
20
+
21
+ it "Should backwards compatible with existing transactions" do
22
+ simple_send = Mastercoin::SimpleSend.decode_from_address("1CVE9Au1XEm3MkYxeAhUDVqWvaHrP98iUt")
23
+ simple_send.amount.should eq(100 * 1e8)
24
+ simple_send.sequence.should eq(126)
25
+ simple_send.transaction_type.should eq(0)
26
+ end
27
+
28
+ it "Should be backwards compatible with sequences" do
29
+ Mastercoin::SimpleSend.new.get_sequence("1CcJFxoEW5PUwesMVxGrq6kAPJ1TJsSVqq").should eq(126)
30
+ end
31
+ end
32
+
33
+ context "Encoding and decoding public keys" do
34
+ it "Should accept all options for a SimpleSend transaction" do
35
+ @simple_send.currency_id.should eq(2)
36
+ @simple_send.amount.should eq(50)
37
+ @simple_send.receiving_address.should eq("184mQaxRYwiU2jqUE852FZGQvbZyRhcDSu")
38
+ @simple_send.transaction_type.should eq(Mastercoin::TRANSACTION_SIMPLE_SEND)
39
+ end
40
+
41
+ it "Should output a valid looking compressed public key" do
42
+ public_key = @simple_send.encode_to_compressed_public_key
43
+ public_key.should eq("020100000000000000020000000000000032000000000000000000000000000000")
44
+ end
45
+
46
+ it "Should be a valid ECDSA point" do
47
+ public_key = @simple_send.encode_to_compressed_public_key
48
+ Mastercoin::Util.valid_ecdsa_point?(public_key).should eq(true)
49
+ end
50
+
51
+ it "Should always start with 02 for compressed key" do
52
+ public_key = @simple_send.encode_to_compressed_public_key
53
+ public_key[0..1].should eq("02")
54
+ end
55
+
56
+ it "Should be able to parse a given public key" do
57
+ simple_send = Mastercoin::SimpleSend.decode_from_compressed_public_key("02000000000000000002000000000000003200000000000000000000000000000")
58
+ simple_send.currency_id.should eq(2)
59
+ simple_send.amount.should eq(50)
60
+ simple_send.transaction_type.should eq(Mastercoin::TRANSACTION_SIMPLE_SEND)
61
+ simple_send.public_key_sequence.should eq(1)
62
+ end
63
+ end
64
+ end