coin-op 0.2.1 → 0.2.2

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