coin-op 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 15c0933fbbf6c12f7a0991e4a889f1ca232a006d
4
- data.tar.gz: acf109f9766f69d87e14160cb8df32b06e475696
3
+ metadata.gz: 0549717040829929865680910e3d10be98a3a605
4
+ data.tar.gz: bab2d7e727c99896a8689b9819662c7f7a9c4b89
5
5
  SHA512:
6
- metadata.gz: 3d79df943d4f273f8b067bc6e632cd4691090202a191738d87c844f7a8cc2dc71d3a918e1c96d84a5d36b3399a28789a997c8fe8d9e3f90478b261fc254539a2
7
- data.tar.gz: e426b4496eb66d122b8fb82e6d08d71e6b35c16fdb649f57ee5ada6fe05d251cae64230693ebf666125c1337691d9f653452b3346cbc41b2c3e14a7f59ec2d26
6
+ metadata.gz: 20025659e8398d0ae10ed292335dcc21b835641339c9c9b864b768640a8b553b00945dd017058826078a2790a70ba5481d228532bc037abd829fac4d789705da
7
+ data.tar.gz: 33fbc0697dd2d619c9d9dfdfe259338de649fae5497563295afb7ed9ca2d15f2c19f1e9677a79b542cb02c2787fdb9ee0c0c91b3fab82f171b0abe1023d21e3b
data/README.md CHANGED
@@ -5,13 +5,53 @@ Install:
5
5
  gem install coin-op
6
6
 
7
7
 
8
- Use:
8
+ Basic usage:
9
9
 
10
10
  ```ruby
11
11
  require "coin-op"
12
12
 
13
13
  include CoinOp::Bit
14
14
 
15
+ transaction = Transaction.from_data(
16
+ # Override the minimum suggested fee
17
+ :fee => 20_000,
18
+ :inputs => [
19
+ {
20
+ :output => {
21
+ :transaction_hash => "2f47a8d7537fd981670b6142f86e1961991577506a825cdfb4c6ab3666db4fc1",
22
+ :index => 0,
23
+ :value => 2_000_000
24
+ }
25
+ },
26
+ {
27
+ :output => {
28
+ :transaction_hash => "fe4d26f6536c17c451e7d9fd7bca3e981a1c9f4542ee49f3bdcb71050c8ef243",
29
+ :index => 0,
30
+ :value => 2_600_000
31
+ }
32
+ }
33
+ ],
34
+ :outputs => [
35
+ {
36
+ :value => 3_000_000,
37
+ :address => "2N9c7acEJNHkDaQvRShMxJcBu5Lw535AvwR"
38
+ }
39
+ ]
40
+ )
41
+
42
+ transaction.add_change(change_address)
43
+
44
+ # Set the script_sigs manually
45
+ transaction.inputs[0].script_sig = "foo"
46
+ transaction.inputs[1].script_sig = "bar"
47
+
48
+ # Or use an iterating helper method. First argument is an array of
49
+ # items corresponding to the inputs. The block yields to you each
50
+ # input, along with the corresponding element from your array.
51
+ transaction.set_script_sigs *keypairs do |input, keypair|
52
+ sig_for(keypair, input)
53
+ end
54
+
15
55
  ```
16
56
 
17
57
  # Developers
data/lib/coin-op.rb CHANGED
@@ -7,6 +7,5 @@ end
7
7
  require_relative "coin-op/encodings"
8
8
  require_relative "coin-op/crypto"
9
9
  require_relative "coin-op/bit"
10
- require_relative "coin-op/blockchain/mockchain"
11
10
  require_relative "coin-op/blockchain/blockr"
12
11
 
data/lib/coin-op/bit.rb CHANGED
@@ -1,4 +1,14 @@
1
1
  require "bitcoin"
2
+
3
+ # bitcoin-ruby is not multi-network friendly. It's also a hassle
4
+ # to tell what network you're using if you don't already know.
5
+ # This makes it a bit easier.
6
+ Bitcoin::NETWORKS.each do |name, definition|
7
+ definition[:name] = name
8
+ end
9
+
10
+
11
+ # BIP 32 Hierarchical Deterministic Wallets
2
12
  require "money-tree"
3
13
 
4
14
  # establish the namespace
@@ -15,6 +25,7 @@ require_relative "bit/output"
15
25
  require_relative "bit/input"
16
26
  require_relative "bit/transaction"
17
27
  require_relative "bit/spendable"
28
+ require_relative "bit/fee"
18
29
 
19
30
  # Augmented functionality
20
31
  require_relative "bit/multi_wallet"
@@ -0,0 +1,84 @@
1
+
2
+ module CoinOp::Bit
3
+
4
+ module Fee
5
+
6
+ module_function
7
+
8
+ # Given an array of unspent Outputs and an array of Outputs for a
9
+ # Transaction, estimate the fee required for the transaction to be
10
+ # included in a block. Optionally takes an Integer specifying the
11
+ # transaction size in bytes, which is necessary when using unspents
12
+ # that deviate from the customary single signature.
13
+ #
14
+ # Returns the estimated fee in satoshis.
15
+ def estimate(unspents, payees, tx_size=nil)
16
+ # https://en.bitcoin.it/wiki/Transaction_fees
17
+
18
+ # dupe because we'll need to add a change output
19
+ payees = payees.dup
20
+
21
+ unspent_total = unspents.inject(0) {|sum, output| sum += output.value}
22
+ payee_total = payees.inject(0) {|sum, payee| sum += payee.value}
23
+ nominal_change = unspent_total - payee_total
24
+ payees << Output.new(:value => nominal_change)
25
+
26
+ tx_size ||= estimate_tx_size(unspents.size, payees.size)
27
+ min = payees.min_by {|payee| payee.value }
28
+
29
+ small = tx_size < 1000
30
+ big_outputs = min.value > 1_000_000
31
+
32
+ p = priority :size => tx_size, :unspents => (unspents.map do |output|
33
+ {:value => output.value, :age => output.confirmations}
34
+ end)
35
+ high_priority = p > PRIORITY_THRESHOLD
36
+
37
+ if small && big_outputs && high_priority
38
+ 0
39
+ else
40
+ fee_for_bytes(tx_size)
41
+ end
42
+
43
+ end
44
+
45
+ def fee_for_bytes(bytes)
46
+ # https://en.bitcoin.it/wiki/Transaction_fees
47
+ # > the reference implementation will round up the transaction size to the
48
+ # > next thousand bytes and add a fee of 0.1 mBTC (0.0001 BTC) per thousand bytes
49
+ size = (bytes / 1000) + 1
50
+ Bitcoin.network[:min_tx_fee] * size
51
+ end
52
+
53
+ # From http://bitcoinfees.com. This estimation is only valid for
54
+ # transactions with all inputs using the common "public key hash" method
55
+ # for authorization.
56
+ def estimate_tx_size(num_inputs, num_outputs)
57
+ # From http://bitcoinfees.com.
58
+ (148 * num_inputs) + (34 * num_outputs) + 10
59
+ end
60
+
61
+
62
+ # https://en.bitcoin.it/wiki/Transaction_fees#Including_in_Blocks
63
+ #
64
+ # https://en.bitcoin.it/wiki/Transaction_fees#Technical_info
65
+ # > Transactions need to have a priority above 57,600,000 to avoid the
66
+ # > enforced limit.... This threshold is written in the code as
67
+ # > COIN * 144 / 250, suggesting that the threshold represents a one day
68
+ # > old, 1 btc coin (144 is the expected number of blocks per day) and a
69
+ # > transaction size of 250 bytes.
70
+ PRIORITY_THRESHOLD = 57_600_000
71
+
72
+ def priority(params)
73
+ tx_size, unspents = params.values_at :size, :unspents
74
+ sum = unspents.inject(0) do |sum, output|
75
+ age = output[:age] || 0
76
+ sum += (output[:value] * age)
77
+ sum
78
+ end
79
+ sum / tx_size
80
+ end
81
+
82
+
83
+ end
84
+ end
@@ -1,55 +1,60 @@
1
1
 
2
2
  module CoinOp::Bit
3
3
 
4
- class SparseInput
5
- include CoinOp::Encodings
6
-
7
- def initialize(binary_hash, index)
8
- @output = {
9
- # the binary hash is the result of
10
- # [tx.hash].pack("H*").reverse
11
- :transaction_hash => hex(binary_hash.reverse),
12
- :index => index,
13
- }
14
- end
15
-
16
- def to_json(*a)
17
- {
18
- :output => @output,
19
- }.to_json(*a)
20
- end
21
-
22
- end
23
-
24
-
25
4
  class Input
5
+
26
6
  include CoinOp::Encodings
27
7
 
28
8
  attr_reader :native, :output, :binary_sig_hash,
29
9
  :signatures, :sig_hash, :script_sig, :index
30
10
 
11
+ # Takes a Hash containing these fields:
12
+ #
13
+ # * :transaction - a Transaction instance
14
+ # * :index - the index of the input within the transaction
15
+ # * :output - a value representing this input's unspent output
16
+ #
17
+ # Optionally:
18
+ #
19
+ # * script_sig_asm - the string form of the scriptSig for this input
20
+ #
31
21
  def initialize(options={})
32
22
  @transaction, @index, @output =
33
23
  options.values_at :transaction, :index, :output
34
24
 
25
+ script_sig_asm = options[:script_sig_asm]
26
+
35
27
  unless @output.is_a? Output
36
28
  @output = Output.new(@output)
37
29
  end
38
30
 
39
31
  @native = Bitcoin::Protocol::TxIn.new
40
32
 
33
+ # TODO: the reverse is cargo-culted from a function in bitcoin-ruby
34
+ # that doesn't document the reason. Find the explanation in the bitcoin
35
+ # wiki or in the reference client source and document here.
41
36
  @native.prev_out = decode_hex(@output.transaction_hash).reverse
42
37
  @native.prev_out_index = @output.index
43
38
 
39
+ if script_sig_asm
40
+ self.script_sig = Bitcoin::Script.binary_from_string(script_sig_asm)
41
+ end
44
42
  @signatures = []
45
43
  end
46
44
 
45
+ # Set the sig_hash (the digest used in signing) for this input using a
46
+ # string of bytes.
47
47
  def binary_sig_hash=(blob)
48
+ # This is only a setter because of the initial choice to do things
49
+ # eagerly. Can become an attr_accessor when we move to lazy eval.
48
50
  @binary_sig_hash = blob
49
51
  @sig_hash = hex(blob)
50
52
  end
51
53
 
54
+ # Set the scriptSig for this input using a string of bytes.
52
55
  def script_sig=(blob)
56
+ # This is only a setter because of the initial choice to do things
57
+ # eagerly. Can become an attr_accessor when we move to lazy eval.
53
58
  script = Script.new(:blob => blob)
54
59
  @script_sig = script.to_s
55
60
  @native.script_sig = blob
@@ -67,6 +72,30 @@ module CoinOp::Bit
67
72
 
68
73
  end
69
74
 
75
+ # Used in Transaction.from_native or other situations where we do
76
+ # not have full information about the unspent output being used
77
+ # for an input.
78
+ class SparseInput
79
+ include CoinOp::Encodings
80
+
81
+ def initialize(binary_hash, index)
82
+ @output = {
83
+ # the binary hash is the result of
84
+ # [tx.hash].pack("H*").reverse
85
+ :transaction_hash => hex(binary_hash.reverse),
86
+ :index => index,
87
+ }
88
+ end
89
+
90
+ def to_json(*a)
91
+ {
92
+ :output => @output,
93
+ }.to_json(*a)
94
+ end
95
+
96
+ end
97
+
98
+
70
99
 
71
100
  end
72
101
 
@@ -6,32 +6,42 @@ module CoinOp::Bit
6
6
  class MultiWallet
7
7
  include CoinOp::Encodings
8
8
 
9
- def self.generate(names, network=:bitcoin_testnet)
9
+ NetworkMap = {
10
+ :testnet3 => :bitcoin_testnet,
11
+ :bitcoin_testnet => :bitcoin_testnet,
12
+ :bitcoin => :bitcoin
13
+ }
14
+
15
+ def self.generate(names, network_name=:testnet3)
16
+ unless network = NetworkMap[network_name]
17
+ raise ArgumentError, "Unknown network #{network_name}"
18
+ end
10
19
  masters = {}
11
20
  names.each do |name|
12
21
  name = name.to_sym
13
22
  masters[name] = MoneyTree::Master.new(:network => network)
14
23
  end
15
- self.new(:private => masters)
24
+ self.new(:private => masters, :network => network)
16
25
  end
17
26
 
18
27
  attr_reader :trees
19
28
 
20
29
  def initialize(options)
21
- # FIXME: must accept option for which network to use.
22
30
  @private_trees = {}
23
31
  @public_trees = {}
24
32
  @trees = {}
25
- private_trees = options[:private]
33
+ @network = NetworkMap[options.include? :network ? options[:network] : :testnet3]
26
34
 
27
35
  # FIXME: we should allow this.
28
- if !private_trees
29
- raise "Must supply :private"
30
- end
36
+ # if !private_trees
37
+ # raise "Must supply :private"
38
+ # end
31
39
 
32
- private_trees.each do |name, arg|
33
- name = name.to_sym
34
- @private_trees[name] = @trees[name] = self.get_node(arg)
40
+ if private_trees = options[:private]
41
+ private_trees.each do |name, arg|
42
+ name = name.to_sym
43
+ @private_trees[name] = @trees[name] = self.get_node(arg)
44
+ end
35
45
  end
36
46
 
37
47
  if public_trees = options[:public]
@@ -130,7 +140,8 @@ module CoinOp::Bit
130
140
  options = {
131
141
  :path => path,
132
142
  :private => {},
133
- :public => {}
143
+ :public => {},
144
+ :network => @network
134
145
  }
135
146
  @private_trees.each do |name, node|
136
147
  options[:private][name] = node.node_for_path(path)
@@ -142,6 +153,10 @@ module CoinOp::Bit
142
153
  MultiNode.new(options)
143
154
  end
144
155
 
156
+ def address(path)
157
+ path(path).address
158
+ end
159
+
145
160
  def valid_output?(output)
146
161
  if path = output.metadata.wallet_path
147
162
  node = self.path(path)
@@ -219,6 +234,7 @@ module CoinOp::Bit
219
234
  @public_keys = {}
220
235
  @private = options[:private]
221
236
  @public = options[:public]
237
+ @network = options[:network]
222
238
 
223
239
  @private.each do |name, node|
224
240
  key = Bitcoin::Key.new(node.private_key.to_hex, node.public_key.to_hex)
@@ -231,9 +247,9 @@ module CoinOp::Bit
231
247
  end
232
248
 
233
249
  def script(m=2)
234
- # m of n
250
+ # m of n
235
251
  keys = @public_keys.sort_by {|name, key| name }.map {|name, key| key.pub }
236
- Script.new(:public_keys => keys, :needed => m)
252
+ Script.new(:public_keys => keys, :needed => m, :network => @network)
237
253
  end
238
254
 
239
255
  def address
@@ -243,7 +259,7 @@ module CoinOp::Bit
243
259
  alias_method :p2sh_address, :address
244
260
 
245
261
  def p2sh_script
246
- Script.new(:address => self.script.p2sh_address)
262
+ Script.new(:address => self.script.p2sh_address, :network => @network)
247
263
  end
248
264
 
249
265
  def sign(name, value)
@@ -269,4 +285,3 @@ module CoinOp::Bit
269
285
 
270
286
 
271
287
  end
272
-
@@ -5,20 +5,21 @@ module CoinOp::Bit
5
5
  include CoinOp::Encodings
6
6
 
7
7
  attr_accessor :metadata
8
- attr_reader :native, :transaction, :index, :value, :script, :address
8
+ attr_reader :native, :transaction, :index, :value, :script
9
9
 
10
10
  # Takes a Hash with required keys:
11
11
  #
12
- # * either :transaction (an instance of Transaction)
13
- # or :transaction_hash (the hex-encoded hash of a Bitcoin transaction)
12
+ # * either :transaction (instance of Transaction)
13
+ # or :transaction_hash (hex-encoded hash of a Bitcoin transaction)
14
14
  # * :index
15
- # * :script
15
+ # * :value
16
16
  #
17
17
  # optional keys:
18
18
  #
19
- # * :value
20
- # * :metadata
21
- #
19
+ # * either :script (a value usable in Script.new)
20
+ # or :address (a valid Bitcoin address)
21
+ # * :metadata (a Hash with arbitrary contents)
22
+ #
22
23
  def initialize(options)
23
24
  if options[:transaction]
24
25
  @transaction = options[:transaction]
@@ -28,26 +29,43 @@ module CoinOp::Bit
28
29
 
29
30
  # FIXME: be aware of string bitcoin values versus
30
31
  # integer satoshi values
31
- @index, @value, @address = options.values_at :index, :value, :address
32
+ @index, @value, @address, confirmations =
33
+ options.values_at :index, :value, :address, :confirmations
34
+
32
35
  @metadata = options[:metadata] || {}
36
+ @metadata[:confirmations] ||= confirmations
33
37
 
34
38
  if options[:script]
35
39
  @script = Script.new(options[:script])
36
- else
37
- raise ArgumentError, "No script supplied"
40
+ elsif @address
41
+ @script = Script.new(:address => @address)
38
42
  end
39
43
 
44
+
40
45
  @native = Bitcoin::Protocol::TxOut.from_hash(
41
46
  "value" => @value.to_s,
42
47
  "scriptPubKey" => @script.to_s
43
48
  )
44
49
  end
45
50
 
51
+ # The bitcoin address generated from the associated Script.
52
+ def address
53
+ if @script
54
+ @script.address
55
+ end
56
+ end
57
+
58
+ def confirmations
59
+ @metadata[:confirmations]
60
+ end
61
+
62
+ # DEPRECATED
46
63
  def set_transaction(transaction, index)
47
64
  @transaction_hash = nil
48
65
  @transaction, @index = transaction, index
49
66
  end
50
67
 
68
+ # Returns the transaction hash for this output.
51
69
  def transaction_hash
52
70
  if @transaction
53
71
  @transaction.hex_hash
@@ -1,12 +1,37 @@
1
1
 
2
2
  module CoinOp::Bit
3
3
 
4
+ # A wrapper class to make it easier to read and write Bitcoin scripts.
5
+ # Provides a sane #to_json method.
4
6
  class Script
5
7
  include CoinOp::Encodings
6
8
 
9
+ # Accessor for the "native" ::Bitcoin::Script script instance
7
10
  attr_reader :native
8
11
 
12
+ # Takes either a String or a Hash as its argument.
13
+ #
14
+ # A String argument will be parsed as a human readable script.
15
+ #
16
+ # A Hash argument specifies a script using one of several possible
17
+ # keys:
18
+ #
19
+ # * :string
20
+ # * :blob
21
+ # * :hex
22
+ # * :address
23
+ # * :public_key
24
+ # * :public_keys, :needed
25
+ # * :signatures
26
+ #
27
+ # The name of the crypto-currency network may also be specified. It
28
+ # defaults to :testnet3. Names supplied in this manner must correspond
29
+ # to the names in the ::Bitcoin::NETWORKS Hash.
9
30
  def initialize(options)
31
+ # Doing the rescue in case the input argument is a String.
32
+ network_name = (options[:network] || :testnet3) rescue :testnet3
33
+ @network = Bitcoin::NETWORKS[network_name]
34
+
10
35
  # literals
11
36
  if options.is_a? String
12
37
  @blob = Bitcoin::Script.binary_from_string options
@@ -19,6 +44,9 @@ module CoinOp::Bit
19
44
  # arguments for constructing
20
45
  else
21
46
  if address = options[:address]
47
+ unless Bitcoin::valid_address?(address)
48
+ raise ArgumentError, "Invalid address: #{address}"
49
+ end
22
50
  @blob = Bitcoin::Script.to_address_script(address)
23
51
  elsif public_key = options[:public_key]
24
52
  @blob = Bitcoin::Script.to_pubkey_script(public_key)
@@ -36,6 +64,10 @@ module CoinOp::Bit
36
64
  @string = @native.to_string
37
65
  end
38
66
 
67
+ def address
68
+ @native.get_p2sh_address
69
+ end
70
+
39
71
  def to_s
40
72
  @string
41
73
  end
@@ -53,8 +85,11 @@ module CoinOp::Bit
53
85
  def type
54
86
  case self.native.type
55
87
  when :hash160
88
+ # Pay to address, because an "address" is really just the hash
89
+ # of a public key.
56
90
  :pubkey_hash
57
91
  when :p2sh
92
+ # Pay to Script Hash
58
93
  :script_hash
59
94
  else
60
95
  self.native.type
@@ -76,6 +111,10 @@ module CoinOp::Bit
76
111
  Bitcoin.hash160(@hex)
77
112
  end
78
113
 
114
+ # Generate the script that uses a P2SH address.
115
+ # Used for an Output's scriptPubKey value. Not much used, and
116
+ # can probably be removed, as I think it is equivalent to
117
+ # Script.new :address => some_p2sh_address
79
118
  def p2sh_script
80
119
  self.class.new Bitcoin::Script.to_p2sh_script(self.hash160)
81
120
  end
@@ -84,6 +123,9 @@ module CoinOp::Bit
84
123
  Bitcoin.hash160_to_p2sh_address(self.hash160)
85
124
  end
86
125
 
126
+ # Generate a P2SH script_sig for the current script, using the
127
+ # supplied options, which will, in the case of a multisig input,
128
+ # be {:signatures => array_of_signatures}.
87
129
  def p2sh_sig(options)
88
130
  string = Script.new(options).to_s
89
131
  Bitcoin::Script.binary_from_string("#{string} #{self.to_hex}")
@@ -1,9 +1,21 @@
1
1
  module CoinOp::Bit
2
+
3
+ # A mixin to provide simple transaction preparation. Not currently
4
+ # used in any production code, so needs vetting.
5
+ #
6
+ # Requires the including class to define these methods:
7
+ #
8
+ # * network
9
+ # * balance
10
+ # * select_unspent
11
+ # * authorize
12
+ #
2
13
  module Spendable
3
14
 
4
15
  class InsufficientFunds < RuntimeError
5
16
  end
6
17
 
18
+ # Return the network name (must be one of the keys from Bitcoin.network)
7
19
  def network
8
20
  raise "implement #network in your class"
9
21
  end
@@ -12,42 +24,24 @@ module CoinOp::Bit
12
24
  raise "implement #balance in your class"
13
25
  end
14
26
 
15
- def unspent
16
- raise "implement #unspent in your class"
17
- end
18
-
19
- def select_unspent
27
+ # Takes a value in satoshis.
28
+ # Returns an array of spendable Outputs
29
+ def select_unspent(value)
20
30
  raise "implement #select_unspent in your class"
21
31
  end
22
32
 
23
- def authorize
33
+ # Authorize the supplied transaction by setting its inputs' script_sigs
34
+ # to whatever values are appropriate.
35
+ def authorize(transaction)
24
36
  raise "implement #authorize in your class"
25
37
  end
26
38
 
27
- def blockchain
28
- # FIXME: use the return value of #network as the arg, once this ticket
29
- # is resolved: # https://github.com/BitVault/bitvault/issues/251
30
- @blockchain ||= BitVaultAPI::Blockchain::Blockr.new(:test)
31
- end
32
-
33
- def lock(outputs)
34
- # no op
35
- # Mixing classes may wish to lock down these selected outputs
36
- # so that concurrent payments or transfers cannot use them.
37
- #
38
- # When do we release unspents (if a user abandons a transaction)?
39
- end
40
-
41
- def unlock(outputs)
42
- end
43
-
44
- def create_transaction(outputs, change_address)
39
+ def create_transaction(outputs, change_address, fee_amount=nil)
45
40
 
46
- transaction = CoinOp::Bit::Transaction.build do |t|
47
- outputs.each do |output|
48
- t.add_output(output)
49
- end
50
- end
41
+ transaction = CoinOp::Bit::Transaction.from_data(
42
+ :fee => fee_amount,
43
+ :outputs => outputs
44
+ )
51
45
 
52
46
  if self.balance < transaction.output_value
53
47
  raise InsufficientFunds
@@ -56,29 +50,20 @@ module CoinOp::Bit
56
50
  unspent = self.select_unspent(transaction.output_value)
57
51
 
58
52
  unspent.each do |output|
59
- transaction.add_input output
53
+ transaction.add_input :output => output
60
54
  end
61
55
 
62
56
  input_amount = unspent.inject(0) {|sum, output| sum += output.value }
63
- fee = transaction.suggested_fee
64
57
 
65
58
  # FIXME: there's likely another unspent output we can add, but the present
66
59
  # implementation of all this can't easily help us. Possibly stop
67
60
  # using select_unspent(value) and start using a while loop that shifts
68
61
  # outputs off the array. Then we can start the process over.
69
- if input_amount < (transaction.output_value + transaction.suggested_fee)
62
+ unless transaction.funded?
70
63
  raise InsufficientFunds
71
64
  end
72
65
 
73
- change = input_amount - (transaction.output_value + fee)
74
-
75
- transaction.add_output(
76
- :value => change,
77
- :script => {
78
- :address => change_address
79
- },
80
- :address => change_address,
81
- )
66
+ transaction.add_change change_address
82
67
 
83
68
  self.authorize(transaction)
84
69
  transaction
@@ -86,3 +71,4 @@ module CoinOp::Bit
86
71
 
87
72
  end
88
73
  end
74
+
@@ -1,15 +1,57 @@
1
-
2
1
  module CoinOp::Bit
3
2
 
4
3
  class Transaction
5
4
  include CoinOp::Encodings
6
5
 
6
+ # Deprecated. Easier to use Transaction.from_data
7
7
  def self.build(&block)
8
8
  transaction = self.new
9
9
  yield transaction
10
10
  transaction
11
11
  end
12
12
 
13
+ # Construct a Transaction from a data structure of nested Hashes
14
+ # and Arrays.
15
+ def self.data(data)
16
+ version, lock_time, fee, inputs, outputs, confirmations =
17
+ data.values_at :version, :lock_time, :fee, :inputs, :outputs, :confirmations
18
+
19
+ transaction = self.new(
20
+ :fee => fee,
21
+ :version => version, :lock_time => lock_time,
22
+ :confirmations => confirmations
23
+ )
24
+
25
+ outputs.each do |data|
26
+ transaction.add_output Output.new(data)
27
+ end
28
+
29
+ #FIXME: we're not handling sig_scripts for already signed inputs.
30
+
31
+ if inputs
32
+ # TODO: use #each instead of #each_with_index
33
+ inputs.each_with_index do |data, index|
34
+ transaction.add_input(data)
35
+
36
+ ## FIXME: verify that the supplied and computed sig_hashes match
37
+ #puts :sig_hashes_match => (data[:sig_hash] == input.sig_hash)
38
+ end
39
+ end
40
+
41
+ transaction
42
+ end
43
+
44
+ # Construct a Transaction from raw bytes.
45
+ def self.raw(raw_tx)
46
+ self.native ::Bitcoin::Protocol::Tx.new(raw_tx)
47
+ end
48
+
49
+ # Construct a Transaction from a hex representation of the raw bytes.
50
+ def self.hex(hex)
51
+ self.from_bytes CoinOp::Encodings.decode_hex(hex)
52
+ end
53
+
54
+ # Construct a transaction from an instance of ::Bitcoin::Protocol::Tx
13
55
  def self.native(tx)
14
56
  transaction = self.new()
15
57
  # TODO: reconsider use of instance_eval
@@ -33,49 +75,39 @@ module CoinOp::Bit
33
75
 
34
76
  report = transaction.validate_syntax
35
77
  unless report[:valid] == true
36
- raise "Invalid syntax: #{report[:errors].to_json}"
78
+ raise "Invalid syntax: #{report[:error].to_json}"
37
79
  end
38
80
  transaction
39
81
  end
40
82
 
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)
83
+ # Preparation for interface change, where the from_foo methods become
84
+ # preferred, and the terser method names are deprecated.
85
+ #
86
+ # This nasty little construct allows us to work on the class's metaclass.
87
+ class << self
88
+ alias_method :from_data, :data
89
+ alias_method :from_hex, :hex
90
+ alias_method :from_bytes, :raw
91
+ alias_method :from_native, :native
47
92
  end
48
93
 
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
94
 
71
- attr_reader :native, :inputs, :outputs
95
+ attr_reader :native, :inputs, :outputs, :confirmations
72
96
 
73
- def initialize
74
- @native = native || Bitcoin::Protocol::Tx.new
97
+ # A new Transaction contains no inputs or outputs; these can be added with
98
+ # #add_input and #add_output.
99
+ # FIXME: version and locktime options are ignored here.
100
+ def initialize(options={})
101
+ @native = Bitcoin::Protocol::Tx.new
75
102
  @inputs = []
76
103
  @outputs = []
104
+ @fee_override = options[:fee]
105
+ @confirmations = options[:confirmations]
77
106
  end
78
107
 
108
+ # Update the "native" bitcoin-ruby instances for the transaction and
109
+ # all its inputs. Will be removed when we rework the wrapper classes
110
+ # to be lazy, rather than eager.
79
111
  def update_native
80
112
  yield @native if block_given?
81
113
  @native = Bitcoin::Protocol::Tx.new(@native.to_payload)
@@ -91,6 +123,11 @@ module CoinOp::Bit
91
123
  end
92
124
  end
93
125
 
126
+ # Monkeypatch to remove a test that fails because bitcoin-ruby thinks a
127
+ # transaction doesn't have valid syntax when it contains a coinbase input.
128
+ Bitcoin::Validation::Tx::RULES[:syntax].delete(:inputs)
129
+
130
+ # Validate that the transaction is plausibly signable.
94
131
  def validate_syntax
95
132
  update_native
96
133
  validator = Bitcoin::Validation::Tx.new(@native, nil)
@@ -98,6 +135,7 @@ module CoinOp::Bit
98
135
  {:valid => valid, :error => validator.error}
99
136
  end
100
137
 
138
+ # Verify that the script_sigs for all inputs are valid.
101
139
  def validate_script_sigs
102
140
  bad_inputs = []
103
141
  valid = true
@@ -119,16 +157,14 @@ module CoinOp::Bit
119
157
  # * an instance of Output
120
158
  # * a Hash describing an Output
121
159
  #
122
- def add_input(arg)
160
+ def add_input(input)
123
161
  # TODO: allow specifying prev_tx and index with a Hash.
124
162
  # Possibly stop using SparseInput.
125
- if arg.is_a? Input
126
- input = arg
127
- else
128
- input = Input.new(
163
+
164
+ unless input.is_a?(Input)
165
+ input = Input.new input.merge(
129
166
  :transaction => self,
130
167
  :index => @inputs.size,
131
- :output => arg
132
168
  )
133
169
  end
134
170
 
@@ -139,12 +175,15 @@ module CoinOp::Bit
139
175
  input
140
176
  end
141
177
 
178
+ # Takes either an Output or a Hash describing an output.
142
179
  def add_output(output)
143
180
  unless output.is_a? Output
144
181
  output = Output.new(output)
145
182
  end
146
183
 
147
184
  index = @outputs.size
185
+ # TODO: stop using set_transaction and just pass self to Output.new
186
+ # Then remove output.set_transaction
148
187
  output.set_transaction self, index
149
188
  @outputs << output
150
189
  self.update_native do |native|
@@ -152,11 +191,13 @@ module CoinOp::Bit
152
191
  end
153
192
  end
154
193
 
194
+ # Returns the transaction hash as a string of bytes.
155
195
  def binary_hash
156
196
  update_native
157
197
  @native.binary_hash
158
198
  end
159
199
 
200
+ # Returns the transaction hash encoded as hex
160
201
  def hex_hash
161
202
  update_native
162
203
  @native.hash
@@ -170,27 +211,36 @@ module CoinOp::Bit
170
211
  @native.lock_time
171
212
  end
172
213
 
214
+ # Returns the transaction payload encoded as hex. This value can
215
+ # be used by other bitcoin tools for publishing to the network.
173
216
  def to_hex
174
217
  payload = self.native.to_payload
175
218
  CoinOp::Encodings.hex(payload)
176
219
  end
177
220
 
178
- def to_json(*a)
179
- self.to_hash.to_json(*a)
180
- end
181
-
221
+ # Returns a custom data structure representing the full transaction.
222
+ # Typically used only by #to_json.
182
223
  def to_hash
183
224
  {
184
225
  :version => self.version,
185
226
  :lock_time => self.lock_time,
186
227
  :hash => self.hex_hash,
228
+ :fee => self.fee,
187
229
  :inputs => self.inputs,
188
230
  :outputs => self.outputs,
189
231
  }
190
232
  end
191
233
 
234
+ def to_json(*a)
235
+ self.to_hash.to_json(*a)
236
+ end
237
+
238
+ # Compute the digest for a given input. In most cases, you need to provide
239
+ # the script. Which script to supply can be confusing, especially in the
240
+ # case of P2SH outputs.
241
+ # TODO: explain the above more clearly.
192
242
  def sig_hash(input, script=nil)
193
- # NOTE: we only allow SIGHASH_ALL at this time
243
+ # We only allow SIGHASH_ALL at this time
194
244
  # https://en.bitcoin.it/wiki/OP_CHECKSIG#Hashtype_SIGHASH_ALL_.28default.29
195
245
 
196
246
  prev_out = input.output
@@ -199,6 +249,28 @@ module CoinOp::Bit
199
249
  @native.signature_hash_for_input(input.index, nil, script.to_blob)
200
250
  end
201
251
 
252
+ # A convenience method for authorizing inputs in a generic manner.
253
+ # Rather than iterating over the inputs manually, the user can
254
+ # provide this method with an array of values and a block that
255
+ # knows what to do with the values.
256
+ #
257
+ # For example, if you happen to have the script sigs precomputed
258
+ # for some strange reason, you could do this:
259
+ #
260
+ # tx.set_script_sigs sig_array do |input, sig|
261
+ # sig
262
+ # end
263
+ #
264
+ # More realistically, if you have an array of the keypairs corresponding
265
+ # to the inputs:
266
+ #
267
+ # tx.set_script_sigs keys do |input, key|
268
+ # sig_hash = tx.sig_hash(input)
269
+ # key.sign(sig_hash)
270
+ # end
271
+ #
272
+ # Each element of the array may be an array, which allows for easy handling
273
+ # of multisig situations.
202
274
  def set_script_sigs(*input_args, &block)
203
275
  # No sense trying to authorize when the transaction isn't usable.
204
276
  report = validate_syntax
@@ -207,19 +279,32 @@ module CoinOp::Bit
207
279
  end
208
280
 
209
281
  # Array#zip here allows us to iterate over the inputs in lockstep with any
210
- # number of sets of signatures.
282
+ # number of sets of values.
211
283
  self.inputs.zip(*input_args) do |input, *input_arg|
212
284
  input.script_sig = yield input, *input_arg
213
285
  end
214
286
  end
215
287
 
288
+ def fee_override
289
+ @fee_override || self.estimate_fee
290
+ end
216
291
 
217
- def suggested_fee
218
- @native.minimum_block_fee
292
+ # Estimate the fee in satoshis for this transaction. Takes an optional
293
+ # tx_size argument because it is impossible to determine programmatically
294
+ # the size of the scripts used to create P2SH outputs.
295
+ # Rough testing of the size of a 2of3 multisig p2sh input: 297
296
+ def estimate_fee(tx_size=nil)
297
+ unspents = inputs.map(&:output)
298
+ Fee.estimate(unspents, outputs, tx_size)
219
299
  end
220
300
 
301
+ # Returns the transaction fee computed from the actual input and output
302
+ # values, as opposed to the requested override fee or the estimated fee.
303
+ def fee
304
+ input_value - output_value rescue nil
305
+ end
221
306
 
222
- # Total value being spent
307
+ # Total value of all outputs.
223
308
  def output_value
224
309
  total = 0
225
310
  @outputs.each do |output|
@@ -229,6 +314,51 @@ module CoinOp::Bit
229
314
  total
230
315
  end
231
316
 
317
+ # Are the currently selected inputs sufficient to cover the current
318
+ # outputs and the desired fee?
319
+ def funded?
320
+ input_value >= (output_value + fee_override)
321
+ end
322
+
323
+ # Total value of all inputs.
324
+ def input_value
325
+ inputs.inject(0) { |sum, input| sum += input.output.value }
326
+ end
327
+
328
+ # Takes a set of Bitcoin addresses and returns the net change in value
329
+ # expressed in this transaction.
330
+ def value_for(addresses)
331
+ output_value_for(addresses) - input_value_for(addresses)
332
+ end
333
+
334
+ # Takes a set of Bitcoin addresses and returns the value expressed in
335
+ # the inputs for this transaction.
336
+ def input_value_for(addresses)
337
+ own = inputs.select { |input| addresses.include?(input.output.address) }
338
+ own.inject(0) { |sum, input| input.output.value }
339
+ end
340
+
341
+ # Takes a set of Bitcoin addresses and returns the value expressed in
342
+ # the outputs for this transaction.
343
+ def output_value_for(addresses)
344
+ own = outputs.select { |output| addresses.include?(output.address) }
345
+ own.inject(0) { |sum, output| output.value }
346
+ end
347
+
348
+ # Returns the value that should be assigned to a change output.
349
+ def change_value
350
+ input_value - (output_value + fee_override)
351
+ end
352
+
353
+ # Add an output to receive change for this transaction.
354
+ # Takes a bitcoin address and optional metadata Hash.
355
+ def add_change(address, metadata={})
356
+ add_output(
357
+ :value => change_value,
358
+ :address => address,
359
+ :metadata => {:memo => "change"}.merge(metadata)
360
+ )
361
+ end
232
362
 
233
363
  end
234
364
 
@@ -1,3 +1,5 @@
1
+ # DEPRECATED
2
+ #
1
3
  require "bitcoin"
2
4
 
3
5
  Bitcoin::NETWORKS[:mockchain] = {
@@ -1,19 +1,39 @@
1
+ # Ruby bindings for libsodium, a port of DJB's NaCl crypto library
1
2
  require "rbnacl"
2
3
  require "openssl"
3
4
 
4
5
  module CoinOp
5
6
  module Crypto
6
7
 
8
+ # A wrapper for NaCl's Secret Box, taking a user-supplied passphrase
9
+ # and deriving a secret key, rather than using a (far more secure)
10
+ # randomly generated secret key.
11
+ #
12
+ # NaCl Secret Box provides a high level interface for authenticated
13
+ # symmetric encryption. When creating the box, you must supply a key.
14
+ # When using the box to encrypt, you must supply a random nonce. Nonces
15
+ # must never be re-used.
16
+ #
17
+ # Secret Box decryption requires the ciphertext and the nonce used to
18
+ # create it.
19
+ #
20
+ # The PassphraseBox class takes a passphrase, rather than a randomly
21
+ # generated key. It uses PBKDF2 to generate a key that, while not random,
22
+ # is somewhat resistant to brute force attacks. Great care should still
23
+ # be taken to avoid passphrases that are subject to dictionary attacks.
7
24
  class PassphraseBox
8
- include CoinOp::Encodings
25
+
26
+ # Both class and instance methods need encoding help, so we supply
27
+ # them to both scopes using extend and include, respectively.
9
28
  extend CoinOp::Encodings
29
+ include CoinOp::Encodings
10
30
 
11
31
  # PBKDF2 work factor
12
32
  ITERATIONS = 100_000
13
33
 
14
34
  # Given passphrase and plaintext as strings, returns a Hash
15
35
  # containing the ciphertext and other values needed for later
16
- # decryption.
36
+ # decryption. Binary values are encoded as hexadecimal strings.
17
37
  def self.encrypt(passphrase, plaintext)
18
38
  box = self.new(passphrase)
19
39
  box.encrypt(plaintext)
@@ -53,8 +73,8 @@ module CoinOp
53
73
  nonce = RbNaCl::Random.random_bytes(RbNaCl::SecretBox.nonce_bytes)
54
74
  ciphertext = @box.encrypt(nonce, plaintext)
55
75
  {
56
- :salt => hex(@salt),
57
76
  :iterations => @iterations,
77
+ :salt => hex(@salt),
58
78
  :nonce => hex(nonce),
59
79
  :ciphertext => hex(ciphertext)
60
80
  }
@@ -2,6 +2,8 @@
2
2
  module CoinOp
3
3
 
4
4
  module Encodings
5
+ # Extending a module with itself allows it to be used as both a mixin
6
+ # and a bag of functions, e.g. CoinOp::Encodings.hex(string)
5
7
  extend self
6
8
 
7
9
  def hex(blob)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coin-op
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew King
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-10 00:00:00.000000000 Z
11
+ date: 2015-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bitcoin-ruby
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.0.5
19
+ version: 0.0.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.0.5
26
+ version: 0.0.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: money-tree
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +107,7 @@ files:
107
107
  - README.md
108
108
  - lib/coin-op.rb
109
109
  - lib/coin-op/bit.rb
110
+ - lib/coin-op/bit/fee.rb
110
111
  - lib/coin-op/bit/input.rb
111
112
  - lib/coin-op/bit/multi_wallet.rb
112
113
  - lib/coin-op/bit/output.rb
@@ -137,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
138
  version: '0'
138
139
  requirements: []
139
140
  rubyforge_project:
140
- rubygems_version: 2.2.0
141
+ rubygems_version: 2.2.2
141
142
  signing_key:
142
143
  specification_version: 4
143
144
  summary: Crypto currency classes in Ruby