coin-op 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +28 -0
- data/lib/coin-op.rb +12 -0
- data/lib/coin-op/bit.rb +21 -0
- data/lib/coin-op/bit/input.rb +73 -0
- data/lib/coin-op/bit/multi_wallet.rb +261 -0
- data/lib/coin-op/bit/output.rb +79 -0
- data/lib/coin-op/bit/script.rb +94 -0
- data/lib/coin-op/bit/spendable.rb +85 -0
- data/lib/coin-op/bit/transaction.rb +238 -0
- data/lib/coin-op/blockchain/blockr.rb +162 -0
- data/lib/coin-op/blockchain/mockchain.rb +124 -0
- data/lib/coin-op/crypto.rb +70 -0
- data/lib/coin-op/encodings.rb +26 -0
- metadata +144 -0
@@ -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
|
+
|