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