coin-op 0.1.0

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,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
+