bitcoin 0.1.0 → 0.1.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.
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