coin-op 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,94 @@
1
+
2
+ module CoinOp::Bit
3
+
4
+ class Script
5
+ include CoinOp::Encodings
6
+
7
+ attr_reader :native
8
+
9
+ def initialize(options)
10
+ # literals
11
+ if options.is_a? String
12
+ @blob = Bitcoin::Script.binary_from_string options
13
+ elsif string = options[:string]
14
+ @blob = Bitcoin::Script.binary_from_string string
15
+ elsif options[:blob]
16
+ @blob = options[:blob]
17
+ elsif options[:hex]
18
+ @blob = decode_hex(options[:hex])
19
+ # arguments for constructing
20
+ else
21
+ if address = options[:address]
22
+ @blob = Bitcoin::Script.to_address_script(address)
23
+ elsif public_key = options[:public_key]
24
+ @blob = Bitcoin::Script.to_pubkey_script(public_key)
25
+ elsif (keys = options[:public_keys]) && (needed = options[:needed])
26
+ @blob = Bitcoin::Script.to_multisig_script(needed, *keys)
27
+ elsif signatures = options[:signatures]
28
+ @blob = Bitcoin::Script.to_multisig_script_sig(*signatures)
29
+ else
30
+ raise ArgumentError
31
+ end
32
+ end
33
+
34
+ @hex = hex(@blob)
35
+ @native = Bitcoin::Script.new @blob
36
+ @string = @native.to_string
37
+ end
38
+
39
+ def to_s
40
+ @string
41
+ end
42
+
43
+ def to_hex
44
+ @hex
45
+ end
46
+
47
+ def to_blob
48
+ @blob
49
+ end
50
+
51
+ alias_method :to_binary, :to_blob
52
+
53
+ def type
54
+ case self.native.type
55
+ when :hash160
56
+ :pubkey_hash
57
+ when :p2sh
58
+ :script_hash
59
+ else
60
+ self.native.type
61
+ end
62
+ end
63
+
64
+ def to_hash
65
+ {
66
+ :type => self.type,
67
+ :string => self.to_s
68
+ }
69
+ end
70
+
71
+ def to_json(*a)
72
+ self.to_hash.to_json(*a)
73
+ end
74
+
75
+ def hash160
76
+ Bitcoin.hash160(@hex)
77
+ end
78
+
79
+ def p2sh_script
80
+ self.class.new Bitcoin::Script.to_p2sh_script(self.hash160)
81
+ end
82
+
83
+ def p2sh_address
84
+ Bitcoin.hash160_to_p2sh_address(self.hash160)
85
+ end
86
+
87
+ def p2sh_sig(options)
88
+ string = Script.new(options).to_s
89
+ Bitcoin::Script.binary_from_string("#{string} #{self.to_hex}")
90
+ end
91
+
92
+ end
93
+
94
+ end
@@ -0,0 +1,85 @@
1
+ module CoinOp::Bit
2
+ module Spendable
3
+
4
+ def network
5
+ raise "implement #network in your class"
6
+ end
7
+
8
+ def balance
9
+ raise "implement #balance in your class"
10
+ end
11
+
12
+ def unspent
13
+ raise "implement #unspent in your class"
14
+ end
15
+
16
+ def select_unspent
17
+ raise "implement #select_unspent in your class"
18
+ end
19
+
20
+ def authorize
21
+ raise "implement #authorize in your class"
22
+ end
23
+
24
+ def blockchain
25
+ # FIXME: use the return value of #network as the arg, once this ticket
26
+ # is resolved: # https://github.com/BitVault/bitvault/issues/251
27
+ @blockchain ||= BitVaultAPI::Blockchain::Blockr.new(:test)
28
+ end
29
+
30
+ def lock(outputs)
31
+ # no op
32
+ # Mixing classes may wish to lock down these selected outputs
33
+ # so that concurrent payments or transfers cannot use them.
34
+ #
35
+ # When do we release unspents (if a user abandons a transaction)?
36
+ end
37
+
38
+ def unlock(outputs)
39
+ end
40
+
41
+ def create_transaction(outputs, change_address)
42
+
43
+ transaction = CoinOp::Bit::Transaction.build do |t|
44
+ outputs.each do |output|
45
+ t.add_output(output)
46
+ end
47
+ end
48
+
49
+ if self.balance < transaction.output_value
50
+ raise InsufficientFunds
51
+ end
52
+
53
+ unspent = self.select_unspent(transaction.output_value)
54
+
55
+ unspent.each do |output|
56
+ transaction.add_input output
57
+ end
58
+
59
+ input_amount = unspent.inject(0) {|sum, output| sum += output.value }
60
+ fee = transaction.suggested_fee
61
+
62
+ # FIXME: there's likely another unspent output we can add, but the present
63
+ # implementation of all this can't easily help us. Possibly stop
64
+ # using select_unspent(value) and start using a while loop that shifts
65
+ # outputs off the array. Then we can start the process over.
66
+ if input_amount < (transaction.output_value + transaction.suggested_fee)
67
+ raise InsufficientFunds
68
+ end
69
+
70
+ change = input_amount - (transaction.output_value + fee)
71
+
72
+ transaction.add_output(
73
+ :value => change,
74
+ :script => {
75
+ :address => change_address
76
+ },
77
+ :address => change_address,
78
+ )
79
+
80
+ self.authorize(transaction)
81
+ transaction
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,238 @@
1
+
2
+ module CoinOp::Bit
3
+
4
+ class Transaction
5
+ include CoinOp::Encodings
6
+
7
+ def self.build(&block)
8
+ transaction = self.new
9
+ yield transaction
10
+ transaction
11
+ end
12
+
13
+ def self.native(tx)
14
+ transaction = self.new()
15
+ # TODO: reconsider use of instance_eval
16
+ transaction.instance_eval do
17
+ @native = tx
18
+ tx.inputs.each_with_index do |input, i|
19
+ # We use SparseInput because it does not require the retrieval
20
+ # of the previous output. Its functionality should probably be
21
+ # folded into the Input class.
22
+ @inputs << SparseInput.new(input.prev_out, input.prev_out_index)
23
+ end
24
+ tx.outputs.each_with_index do |output, i|
25
+ @outputs << Output.new(
26
+ :transaction => transaction,
27
+ :index => i,
28
+ :value => output.value,
29
+ :script => {:blob => output.pk_script}
30
+ )
31
+ end
32
+ end
33
+
34
+ report = transaction.validate_syntax
35
+ unless report[:valid] == true
36
+ raise "Invalid syntax: #{report[:errors].to_json}"
37
+ end
38
+ transaction
39
+ end
40
+
41
+ def self.raw(raw_tx)
42
+ self.native ::Bitcoin::Protocol::Tx.new(raw_tx)
43
+ end
44
+
45
+ def self.hex(hex)
46
+ self.raw CoinOp::Encodings.decode_hex(hex)
47
+ end
48
+
49
+ def self.data(hash)
50
+ version, lock_time, tx_hash, inputs, outputs =
51
+ hash.values_at :version, :lock_time, :tx_hash, :inputs, :outputs
52
+
53
+ transaction = self.new
54
+
55
+ outputs.each do |data|
56
+ transaction.add_output Output.new(data)
57
+ end
58
+
59
+ #FIXME: we're not handling sig_scripts for already signed inputs.
60
+
61
+ inputs.each_with_index do |data, index|
62
+ transaction.add_input data[:output]
63
+
64
+ ## FIXME: verify that the supplied and computed sig_hashes match
65
+ #puts :sig_hashes_match => (data[:sig_hash] == input.sig_hash)
66
+ end
67
+
68
+ transaction
69
+ end
70
+
71
+ attr_reader :native, :inputs, :outputs
72
+
73
+ def initialize
74
+ @native = native || Bitcoin::Protocol::Tx.new
75
+ @inputs = []
76
+ @outputs = []
77
+ end
78
+
79
+ def update_native
80
+ yield @native if block_given?
81
+ @native = Bitcoin::Protocol::Tx.new(@native.to_payload)
82
+ @inputs.each_with_index do |input, i|
83
+ native = @native.inputs[i]
84
+ # Using instance_eval here because I really don't want to expose
85
+ # Input#native=. As we consume more and more of the native
86
+ # functionality, we can dispense with such ugliness.
87
+ input.instance_eval do
88
+ @native = native
89
+ end
90
+ if input.is_a? Input
91
+ input.binary_sig_hash = self.sig_hash(input)
92
+ end
93
+ # TODO: is this re-nativization necessary for outputs, too?
94
+ end
95
+ end
96
+
97
+ def validate_syntax
98
+ update_native
99
+ validator = Bitcoin::Validation::Tx.new(@native, nil)
100
+ valid = validator.validate :rules => [:syntax]
101
+ {:valid => valid, :error => validator.error}
102
+ end
103
+
104
+ def validate_script_sigs
105
+ bad_inputs = []
106
+ valid = true
107
+ @inputs.each_with_index do |input, index|
108
+ # TODO: confirm whether we need to mess with the block_timestamp arg
109
+
110
+ unless self.native.verify_input_signature(index, input.output.transaction.native)
111
+ valid = false
112
+ bad_inputs << index
113
+ end
114
+
115
+ end
116
+ {:valid => valid, :inputs => bad_inputs}
117
+ end
118
+
119
+ # Takes one of
120
+ #
121
+ # * an instance of Input
122
+ # * an instance of Output
123
+ # * a Hash describing an Output
124
+ #
125
+ def add_input(arg)
126
+ # TODO: allow specifying prev_tx and index with a Hash.
127
+ # Possibly stop using SparseInput.
128
+ if arg.is_a? Input
129
+ input = arg
130
+ else
131
+ input = Input.new(
132
+ :transaction => self,
133
+ :index => @inputs.size,
134
+ :output => arg
135
+ )
136
+ end
137
+
138
+ @inputs << input
139
+ self.update_native do |native|
140
+ native.add_in input.native
141
+ end
142
+ input
143
+ end
144
+
145
+ def add_output(output)
146
+ unless output.is_a? Output
147
+ output = Output.new(output)
148
+ end
149
+
150
+ index = @outputs.size
151
+ output.set_transaction self, index
152
+ @outputs << output
153
+ self.update_native do |native|
154
+ native.add_out(output.native)
155
+ end
156
+ end
157
+
158
+ def binary_hash
159
+ update_native
160
+ @native.binary_hash
161
+ end
162
+
163
+ def hex_hash
164
+ update_native
165
+ @native.hash
166
+ end
167
+
168
+ def version
169
+ @native.ver
170
+ end
171
+
172
+ def lock_time
173
+ @native.lock_time
174
+ end
175
+
176
+ def to_hex
177
+ payload = self.native.to_payload
178
+ CoinOp::Encodings.hex(payload)
179
+ end
180
+
181
+ def to_json(*a)
182
+ self.to_hash.to_json(*a)
183
+ end
184
+
185
+ def to_hash
186
+ {
187
+ :version => self.version,
188
+ :lock_time => self.lock_time,
189
+ :hash => self.hex_hash,
190
+ :inputs => self.inputs,
191
+ :outputs => self.outputs,
192
+ }
193
+ end
194
+
195
+ def sig_hash(input, script=nil)
196
+ # NOTE: we only allow SIGHASH_ALL at this time
197
+ # https://en.bitcoin.it/wiki/OP_CHECKSIG#Hashtype_SIGHASH_ALL_.28default.29
198
+
199
+ prev_out = input.output
200
+ script ||= prev_out.script
201
+
202
+ @native.signature_hash_for_input(input.index, nil, script.to_blob)
203
+ end
204
+
205
+ def set_script_sigs(*input_args, &block)
206
+ # No sense trying to authorize when the transaction isn't usable.
207
+ report = validate_syntax
208
+ unless report[:valid] == true
209
+ raise "Invalid syntax: #{report[:errors].to_json}"
210
+ end
211
+
212
+ # Array#zip here allows us to iterate over the inputs in lockstep with any
213
+ # number of sets of signatures.
214
+ self.inputs.zip(*input_args) do |input, *input_arg|
215
+ input.script_sig = yield input, *input_arg
216
+ end
217
+ end
218
+
219
+
220
+ def suggested_fee
221
+ @native.minimum_block_fee
222
+ end
223
+
224
+
225
+ # Total value being spent
226
+ def output_value
227
+ total = 0
228
+ @outputs.each do |output|
229
+ total += output.value
230
+ end
231
+
232
+ total
233
+ end
234
+
235
+
236
+ end
237
+
238
+ end
@@ -0,0 +1,162 @@
1
+ require "http"
2
+ require "json"
3
+ require 'enumerator'
4
+
5
+ require_relative "../bit"
6
+
7
+ module CoinOp
8
+ module Blockchain
9
+
10
+ # Blockr.io API documentation: http://blockr.io/documentation/api
11
+ class Blockr
12
+ include CoinOp::Encodings
13
+
14
+ def initialize(env=:test)
15
+ subdomain = (env.to_sym == :test) ? "tbtc" : "btc"
16
+ @base_url = "http://#{subdomain}.blockr.io/api/v1"
17
+
18
+ # Testing says 20 is the absolute max
19
+ @max_per_request = 20
20
+
21
+ @http = HTTP.with_headers(
22
+ "User-Agent" => "bv-blockchain-worker v0.1.0",
23
+ "Accept" => "application/json"
24
+ )
25
+ end
26
+
27
+
28
+ attr_accessor :max_per_request
29
+
30
+
31
+ def unspent(addresses, confirmations=6)
32
+
33
+ result = request(
34
+ :address, :unspent, addresses,
35
+ :confirmations => confirmations
36
+ )
37
+
38
+ outputs = []
39
+ result.each do |record|
40
+ record[:unspent].each do |output|
41
+ address = record[:address]
42
+
43
+ transaction_hex, index, value, script_hex =
44
+ output.values_at :tx, :n, :amount, :script
45
+
46
+ outputs << CoinOp::Bit::Output.new(
47
+ :transaction_hex => transaction_hex,
48
+ :index => index,
49
+ :value => bitcoins_to_satoshis(value),
50
+ :script => {:hex => script_hex},
51
+ :address => address
52
+ )
53
+ end
54
+ end
55
+
56
+ outputs.sort_by {|output| -output.value }
57
+ end
58
+
59
+
60
+ def balance(addresses)
61
+ result = request(:address, :balance, addresses)
62
+ balances = {}
63
+ result.each do |record|
64
+ balances[record[:address]] = float_to_satoshis(record[:balance])
65
+ end
66
+
67
+ balances
68
+ end
69
+
70
+
71
+ def transactions(tx_ids)
72
+ results = request(:tx, :raw, tx_ids)
73
+ results.map do |record|
74
+ hex = record[:tx][:hex]
75
+
76
+ transaction = CoinOp::Bit::Transaction.hex(hex)
77
+ end
78
+ end
79
+
80
+
81
+ def address_info(addresses, confirmations=6)
82
+ # Useful for testing transactions()
83
+ request(
84
+ :address, :info, addresses,
85
+ :confirmations => confirmations
86
+ )
87
+ end
88
+
89
+
90
+ def block_info(block_list)
91
+ request(:block, :info, block_list)
92
+ end
93
+
94
+
95
+ def block_txs(block_list)
96
+ request(:block, :txs, block_list)
97
+ end
98
+
99
+
100
+ # Helper methods
101
+
102
+ def bitcoins_to_satoshis(string)
103
+ string.gsub(".", "").to_i
104
+ end
105
+
106
+ def float_to_satoshis(float)
107
+ (float * 100_000_000).to_i
108
+ end
109
+
110
+
111
+ # Queries the Blockr Bitcoin from_type => to_type API with
112
+ # list, returning the results or throwing an exception on
113
+ # failure.
114
+ def request(from_type, to_type, args, query=nil)
115
+
116
+ unless args.is_a? Array
117
+ args = [args]
118
+ end
119
+
120
+ data = []
121
+ args.each_slice(@max_per_request) do |arg_slice|
122
+ # Permit calling with either an array or a scalar
123
+ slice_string = arg_slice.join(",")
124
+ url = "#{@base_url}/#{from_type}/#{to_type}/#{slice_string}"
125
+
126
+ # Construct query string if any params were passed.
127
+ if query
128
+ # TODO: validation. The value of the "confirmations" parameter
129
+ # must be an integer.
130
+ params = query.map { |name, value| "#{name}=#{value}" }.join("&")
131
+ url = "#{url}?#{params}"
132
+ end
133
+
134
+ response = @http.request "GET", url, :response => :object
135
+ # FIXME: rescue any JSON parsing exception and raise an
136
+ # exception explaining that it's blockr's fault.
137
+ begin
138
+ content = JSON.parse(response.body, :symbolize_names => true)
139
+ rescue JSON::ParserError => e
140
+ raise "Blockr returned invalid JSON: #{e}"
141
+ end
142
+
143
+ if content[:status] != "success"
144
+ raise "Blockr.io failure: #{content.to_json}"
145
+ end
146
+
147
+ slice_data = content[:data]
148
+ if content[:data].is_a? Array
149
+ data.concat slice_data
150
+ else
151
+ data << slice_data
152
+ end
153
+ end
154
+
155
+ data
156
+ end
157
+
158
+ end
159
+
160
+ end
161
+ end
162
+