bitcoin 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/.rvmrc +1 -1
  2. data/Gemfile.lock +56 -4
  3. data/Guardfile +38 -0
  4. data/README.markdown +21 -2
  5. data/bin/rbcoin +8 -1
  6. data/bitcoin.gemspec +17 -5
  7. data/config/cucumber.yml +3 -3
  8. data/config/darcs.boring +121 -0
  9. data/doc/DEFINITION_OF_DONE.markdown +12 -0
  10. data/doc/HISTORY.markdown +19 -0
  11. data/{LICENCE.markdown → doc/LICENCE.markdown} +1 -1
  12. data/doc/TODO.markdown +31 -0
  13. data/doc/UBIQUITOUS_LANGUAGE.markdown +15 -0
  14. data/features/descriptions/command_help.feature +31 -0
  15. data/features/descriptions/satoshi_wallet/add_address.feature +49 -0
  16. data/features/descriptions/satoshi_wallet/show_addresses.feature +18 -0
  17. data/features/descriptions/satoshi_wallet/show_version.feature +17 -0
  18. data/features/descriptions/satoshi_wallet/subcommand_help.feature +20 -0
  19. data/features/fixtures/ABOUT_FIXTURES.markdown +6 -0
  20. data/features/fixtures/addressbook_wallet.dat +0 -0
  21. data/features/fixtures/new_wallet.dat +0 -0
  22. data/features/step_definitions/command_steps.rb +3 -0
  23. data/features/step_definitions/wallet_steps.rb +11 -0
  24. data/features/support/env.rb +8 -1
  25. data/lib/bitcoin/cli.rb +35 -0
  26. data/lib/bitcoin/commands.rb +3 -0
  27. data/lib/bitcoin/commands/help_command.rb +32 -0
  28. data/lib/bitcoin/commands/satoshi_wallet.rb +11 -0
  29. data/lib/bitcoin/commands/satoshi_wallet/add_address_command.rb +61 -0
  30. data/lib/bitcoin/commands/satoshi_wallet/show_addresses_command.rb +16 -0
  31. data/lib/bitcoin/commands/satoshi_wallet/show_version_command.rb +15 -0
  32. data/lib/bitcoin/commands/satoshi_wallet_command.rb +37 -0
  33. data/lib/bitcoin/commands/satoshi_wallet_command_environment.rb +28 -0
  34. data/lib/bitcoin/console/capturing_stream_bundle.rb +42 -0
  35. data/lib/bitcoin/console/stream_bundle.rb +21 -0
  36. data/lib/bitcoin/data_access/satoshi/bdb_satoshi_wallet_repository.rb +155 -0
  37. data/lib/bitcoin/data_access/satoshi/satoshi_version.rb +58 -0
  38. data/lib/bitcoin/data_access/satoshi/satoshi_wallet.rb +39 -0
  39. data/lib/bitcoin/domain/address_book.rb +19 -0
  40. data/lib/bitcoin/domain/bitcoin_address.rb +33 -0
  41. data/lib/bitcoin/filesystem/empty_temp_dir.rb +74 -0
  42. data/lib/bitcoin/rspec/argument_matchers.rb +1 -0
  43. data/lib/bitcoin/rspec/argument_matchers/block_evaluating_to_matcher.rb +23 -0
  44. data/lib/bitcoin/rspec/directory_helpers.rb +22 -0
  45. data/lib/bitcoin/version.rb +1 -1
  46. data/spec/bitcoin/cli_spec.rb +128 -0
  47. data/spec/bitcoin/commands/help_command_spec.rb +53 -0
  48. data/spec/bitcoin/commands/satoshi_wallet/add_address_command_spec.rb +149 -0
  49. data/spec/bitcoin/commands/satoshi_wallet/show_addresses_command_spec.rb +26 -0
  50. data/spec/bitcoin/commands/satoshi_wallet/show_version_command_spec.rb +26 -0
  51. data/spec/bitcoin/commands/satoshi_wallet_command_environment_spec.rb +76 -0
  52. data/spec/bitcoin/commands/satoshi_wallet_command_spec.rb +73 -0
  53. data/spec/bitcoin/console/_contracts/stream_bundle_contract.rb +29 -0
  54. data/spec/bitcoin/console/capturing_stream_bundle_spec.rb +74 -0
  55. data/spec/bitcoin/console/stream_bundle_spec.rb +13 -0
  56. data/spec/bitcoin/data_access/satoshi/bdb_satoshi_wallet_repository_spec.rb +78 -0
  57. data/spec/bitcoin/data_access/satoshi/satoshi_version_spec.rb +112 -0
  58. data/spec/bitcoin/data_access/satoshi/satoshi_wallet_spec.rb +102 -0
  59. data/spec/bitcoin/domain/address_book_spec.rb +63 -0
  60. data/spec/bitcoin/domain/bitcoin_address_spec.rb +52 -0
  61. data/spec/bitcoin/filesystem/empty_temp_dir_spec.rb +170 -0
  62. data/spec/bitcoin/rspec/argument_matchers/block_evaluating_to_matcher_spec.rb +36 -0
  63. data/spec/spec_helper.rb +29 -1
  64. metadata +221 -18
@@ -0,0 +1,21 @@
1
+ module Bitcoin
2
+ module Console
3
+ # StreamBundles are objects that encapsulate the standard unix pipes
4
+ # (STDIN, STDOUT, STDERR)
5
+ class StreamBundle
6
+ def initialize(input, output, error)
7
+ @input = input
8
+ @output = output
9
+ @error = error
10
+ end
11
+
12
+ def puts_output(stringlike)
13
+ @output.puts(stringlike)
14
+ end
15
+
16
+ def puts_error(stringlike)
17
+ @error.puts(stringlike)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,155 @@
1
+ require 'sbdb'
2
+ require 'bdb/base'
3
+
4
+ module Bitcoin
5
+ module DataAccess
6
+ module Satoshi
7
+ # Raw Berkeley DB access (thin wrapper around SBDB) for wallet data
8
+ # WARNING! This code is in really bad shape. It works, but is verbose
9
+ # and full of duplication. It needs massive refactoring.
10
+ class BDBSatoshiWalletRepository
11
+ def initialize(db_filename, options)
12
+ @db_filename = db_filename
13
+ @db_dirname = options[:db_dirname]
14
+ end
15
+
16
+ def bdb_bytes_to_utf8_string(bytes)
17
+ bytes.pack("C*").force_encoding("UTF-8")
18
+ end
19
+
20
+ def get_value_immediate_uint32le(key_name)
21
+ with_bdb_db do |db|
22
+ db[make_key(key_name)].unpack("V").first
23
+ end
24
+ end
25
+
26
+ def set_value_immediate_uint32le(key_name, value_data)
27
+ with_bdb_db do |db|
28
+ db[make_key(key_name)] = [ value_data ].pack("V")
29
+ end
30
+ end
31
+
32
+ def get_key_and_value_data_string_utf8(key_name)
33
+ with_bdb_db do |db|
34
+ db_records(db).select { |record|
35
+ record.first == key_name
36
+ }.map { |name, data_hash|
37
+ data_hash
38
+ }.map { |data_hash|
39
+ [ data_hash[:key_data], data_hash[:value_data] ].map { |value|
40
+ bdb_bytes_to_utf8_string(value)
41
+ }
42
+ }
43
+ end
44
+ end
45
+
46
+ def get_value_with_key_data_string_utf8(key_name, key_data)
47
+ with_bdb_db do |db|
48
+ value_data = db[make_key_with_data(key_name, key_data)]
49
+ return unless value_data
50
+ value_bytes = value_data.unpack("C*")
51
+ value_bytes.shift(read_compact_size(value_bytes)).pack("C*")
52
+ end
53
+ end
54
+
55
+ def set_value_with_key_data_string_utf8(key_name, key_data, value_data)
56
+ with_bdb_db do |db|
57
+ db[make_key_with_data(key_name, key_data)] = make_key(value_data)
58
+ end
59
+ end
60
+
61
+ def make_key(key_name)
62
+ key = [ ]
63
+ key << write_compact_size(key_name)
64
+ key.concat(key_name.unpack("C*"))
65
+ key.pack("C*")
66
+ end
67
+
68
+ def make_key_with_data(key_name, key_data)
69
+ make_key(key_name) + make_key(key_data)
70
+ end
71
+
72
+ # Maybe allow changing read/write & read-only when calling this?
73
+ def with_bdb_env(&block)
74
+ # This doesn't yet exactly mirror the DB setup in CWalletDB::LoadWallet
75
+ SBDB::Env.new(
76
+ dir: File.expand_path(@db_dirname),
77
+ lg_max: 10_000_000,
78
+ flags: Bdb::DB_CREATE | # Create the environment if it does not already exist.
79
+ Bdb::DB_INIT_LOCK | # Initialize locking.
80
+ Bdb::DB_INIT_LOG | # Initialize logging
81
+ Bdb::DB_INIT_MPOOL | # Initialize the in-memory cache.
82
+ Bdb::DB_INIT_TXN | # Initialize transactions
83
+ Bdb::DB_THREAD | # TODO: What?
84
+ Bdb::DB_RECOVER # TODO: What?
85
+ ) do |env|
86
+ yield env
87
+ end
88
+ end
89
+
90
+ def with_bdb_db(&block)
91
+ with_bdb_env do |env|
92
+ bitcoin_logical_database_name = "main"
93
+ # Leaving this here for reference: we could offer a read-only mode if we wanted (see Env flags though)
94
+ read_write_flags = Bdb::DB_CREATE | Bdb::DB_AUTO_COMMIT
95
+ read_only_flags = Bdb::DB_RDONLY
96
+ env.open(SBDB::Btree, File.expand_path(@db_filename), name: bitcoin_logical_database_name, flags: read_write_flags) do |db|
97
+ yield db
98
+ end
99
+ end
100
+ end
101
+
102
+ def db_records(db)
103
+ db.to_hash.map { |key, value|
104
+ # Key name
105
+ key_bytes = key.bytes.to_a
106
+ key_name_length = read_compact_size(key_bytes)
107
+ key_name = key_bytes.shift(key_name_length).pack("C*").force_encoding("UTF-8")
108
+
109
+ # Key data
110
+ key_data_length = read_compact_size(key_bytes)
111
+ key_data_raw = key_bytes.shift(key_data_length)
112
+
113
+ # Value data
114
+ value_bytes = value.bytes.to_a
115
+ value_data_raw =
116
+ case key_name
117
+ when "version" # values that don't start with a length
118
+ value_bytes
119
+ else # variable-length values
120
+ value_length = read_compact_size(value_bytes)
121
+ value_bytes.shift(value_length)
122
+ end
123
+
124
+ [ key_name, { key_data: key_data_raw, value_data: value_data_raw } ]
125
+ }
126
+ end
127
+
128
+ def write_compact_size(string)
129
+ string.bytesize
130
+ end
131
+
132
+ def read_compact_size(byte_array)
133
+ # The Satoshi client throws an error here if the determined length is greater
134
+ # than (uint64)MAX_SIZE - I don't know if we need to worry about it
135
+
136
+ byte_1 = byte_array.shift
137
+ if byte_1.nil?
138
+ 0
139
+ elsif byte_1 < 253
140
+ byte_1
141
+ elsif byte_1 == 253
142
+ # Little-endian 16-bit
143
+ byte_array.shift(2).pack("C*").unpack("v").first
144
+ elsif byte_1 == 254
145
+ # Little-endian 32-bit int
146
+ byte_array.shift(4).pack("C*").unpack("V").first
147
+ else
148
+ # 64-bit int (is this little-endian or native?)
149
+ byte_array.shift(8).pack("C*").unpack("Q").first
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,58 @@
1
+ module Bitcoin
2
+ module DataAccess
3
+ module Satoshi
4
+ class SatoshiVersion
5
+ include Comparable
6
+
7
+ class << self
8
+ # Takes an array of unsigned bytes as extracted from a BDB wallet
9
+ def from_wallet_bytes(bytes)
10
+ from_wallet_int(bytes.pack("C*").unpack("V").first)
11
+ end
12
+
13
+ # Takes an 4-byte unsigned little-endian integer as extracted from a BDB wallet
14
+ def from_wallet_int(int_version)
15
+ new(*[int_version / 1_000_000, (int_version / 10_000) % 100, (int_version / 100) % 100, int_version % 100])
16
+ end
17
+ end
18
+
19
+ def initialize(*version_components)
20
+ @version_components = pad_4(version_components)
21
+ end
22
+
23
+ def <=>(other)
24
+ other.compare_version_components(@version_components)
25
+ end
26
+
27
+ def to_s
28
+ components = @version_components.dup
29
+ components.delete_at(3) if components[3] == 0
30
+ components.join(".")
31
+ end
32
+
33
+ alias_method :to_str, :to_s
34
+
35
+ def to_wallet_bytes
36
+ [
37
+ @version_components.zip([1_000_000, 10_000, 100, 1]).map { |component, multiplier|
38
+ component * multiplier
39
+ }.inject(:+)
40
+ ].pack("V").unpack("C*")
41
+ end
42
+
43
+ protected
44
+
45
+ def compare_version_components(other_version_components)
46
+ other_version_components <=> @version_components
47
+ end
48
+
49
+ # Pads 3-digit arrays with a 4th 0 value if necessary
50
+ def pad_4(components)
51
+ ([0] * 4).tap do |padded|
52
+ padded[0...components.length] = components
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,39 @@
1
+ require 'bitcoin/data_access/satoshi/bdb_satoshi_wallet_repository'
2
+ require 'bitcoin/data_access/satoshi/satoshi_version'
3
+ require 'bitcoin/domain/address_book'
4
+ require 'bitcoin/domain/bitcoin_address'
5
+
6
+ module Bitcoin
7
+ module DataAccess
8
+ module Satoshi
9
+ class SatoshiWallet
10
+ class << self
11
+ def open(options, &block)
12
+ raise ArgumentError.new("You must provide a block to SatoshiWallet.new") unless block_given?
13
+ yield(new(BDBSatoshiWalletRepository.new(options[:wallet_filename], db_dirname: options[:db_dirname])))
14
+ end
15
+ end
16
+
17
+ def initialize(wallet_repository)
18
+ @wallet_repository = wallet_repository
19
+ end
20
+
21
+ def version
22
+ SatoshiVersion.from_wallet_int(@wallet_repository.get_value_immediate_uint32le("version"))
23
+ end
24
+
25
+ def addresses
26
+ addresses =
27
+ @wallet_repository.get_key_and_value_data_string_utf8("name").map { |key_data, value_data|
28
+ { address: Domain::BitcoinAddress.new(key_data), name: value_data }
29
+ }
30
+ Domain::AddressBook.new(addresses)
31
+ end
32
+
33
+ def add_address(bitcoin_address, options)
34
+ @wallet_repository.set_value_with_key_data_string_utf8("name", bitcoin_address.to_s, options[:name])
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ module Bitcoin
2
+ module Domain
3
+ class AddressBook
4
+ def initialize(address_list = [])
5
+ @address_list = Marshal.load(Marshal.dump(address_list))
6
+ end
7
+
8
+ def to_a
9
+ @address_list
10
+ end
11
+
12
+ def to_s
13
+ @address_list.map { |address|
14
+ "#{address[:address]} #{address[:name]}"
15
+ }.join("\n")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ module Bitcoin
2
+ module Domain
3
+ # A Value Object representing a Bitcoin address
4
+ class BitcoinAddress
5
+ # An error indicating that we tried to create a BitcoinAddress with an invalid string
6
+ class InvalidBitcoinAddressError < ArgumentError
7
+ def initialize(address)
8
+ super(%'The address "#{address}" is not a valid Bitcoin address')
9
+ end
10
+ end
11
+
12
+ def initialize(string_address)
13
+ raise InvalidBitcoinAddressError.new(string_address) unless string_address.length == 34
14
+ @string_address = string_address
15
+ end
16
+
17
+ def to_s
18
+ @string_address
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(BitcoinAddress) &&
23
+ other.has_string_address_representation?(@string_address)
24
+ end
25
+
26
+ protected
27
+
28
+ def has_string_address_representation?(string_representation)
29
+ @string_address == string_representation
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,74 @@
1
+ require 'fileutils'
2
+
3
+ module Bitcoin
4
+ module FileSystem
5
+ # A class for managing temporary directories
6
+ #
7
+ # Example:
8
+ #
9
+ # EmptyTempDir.new(with_name: "foo", parallel_to: "/tmp/myfile") do |temp_dir|
10
+ # # ...
11
+ # end
12
+ #
13
+ # creates the directory "/tmp/foo". It will be deleted at the end of the block,
14
+ # regardless of whether it existed before or not.
15
+ #
16
+ # If it existed before, it will be emptied before the block starts.
17
+ #
18
+ # If the requested directory name exists but is a file, the behaviour is undefined
19
+ class EmptyTempDir
20
+ class << self
21
+ def open(options, &block)
22
+ raise ArgumentError.new("You must provide a block to EmptyTempDir.open") if block.nil?
23
+ BlockRunner.new(dirname(options[:with_name], options[:parallel_to])).run(block)
24
+ end
25
+
26
+ private
27
+
28
+ def dirname(name, parallel_to)
29
+ File.expand_path(File.join(File.dirname(parallel_to), name))
30
+ end
31
+ end
32
+
33
+ class BlockRunner
34
+ def initialize(path)
35
+ @path = path
36
+ raise_if_path_is_unsafe
37
+ end
38
+
39
+ def run(proc)
40
+ ensure_empty_directory(absolute_path)
41
+ result = proc.(self)
42
+ ensure
43
+ FileUtils.rm_rf(absolute_path)
44
+ result
45
+ end
46
+
47
+ def absolute_path
48
+ @path
49
+ end
50
+
51
+ private
52
+
53
+ def ensure_empty_directory(absolute_path)
54
+ if File.directory?(@path)
55
+ File.join(@path, "*")
56
+ FileUtils.rm_rf(Dir[path_entries_wildcard])
57
+ else
58
+ FileUtils.mkdir(@path)
59
+ end
60
+ end
61
+
62
+ def raise_if_path_is_unsafe
63
+ if path_entries_wildcard =~ %r"^/+\*"
64
+ raise ArgumentError.new("Unacceptable directory name for EmptyTempDir")
65
+ end
66
+ end
67
+
68
+ def path_entries_wildcard
69
+ File.join(@path, "*")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1 @@
1
+ require_relative 'argument_matchers/block_evaluating_to_matcher'
@@ -0,0 +1,23 @@
1
+ module Bitcoin
2
+ module RSpec
3
+ module ArgumentMatchers
4
+ class BlockEvaluatingToMatcher
5
+ def initialize(expected_value)
6
+ @expected_value = expected_value
7
+ end
8
+
9
+ def description
10
+ "block_evaluating_to(#{@expected_value.inspect})"
11
+ end
12
+
13
+ def ==(other)
14
+ @expected_value == other.()
15
+ end
16
+ end
17
+
18
+ def block_evaluating_to(expected_value)
19
+ BlockEvaluatingToMatcher.new(expected_value)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ module Bitcoin
2
+ module RSpec
3
+ module DirectoryHelpers
4
+ module ClassMethods
5
+ def ensure_empty_test_dir(dirname)
6
+ before(:each) do
7
+ ensure_empty_directory(dirname)
8
+ end
9
+ end
10
+ end
11
+
12
+ def self.included(host)
13
+ host.extend(ClassMethods)
14
+ end
15
+
16
+ def ensure_empty_directory(dirname)
17
+ FileUtils.rm_rf(dirname)
18
+ FileUtils.mkdir_p(dirname)
19
+ end
20
+ end
21
+ end
22
+ end