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.
- data/.rvmrc +1 -1
- data/Gemfile.lock +56 -4
- data/Guardfile +38 -0
- data/README.markdown +21 -2
- data/bin/rbcoin +8 -1
- data/bitcoin.gemspec +17 -5
- data/config/cucumber.yml +3 -3
- data/config/darcs.boring +121 -0
- data/doc/DEFINITION_OF_DONE.markdown +12 -0
- data/doc/HISTORY.markdown +19 -0
- data/{LICENCE.markdown → doc/LICENCE.markdown} +1 -1
- data/doc/TODO.markdown +31 -0
- data/doc/UBIQUITOUS_LANGUAGE.markdown +15 -0
- data/features/descriptions/command_help.feature +31 -0
- data/features/descriptions/satoshi_wallet/add_address.feature +49 -0
- data/features/descriptions/satoshi_wallet/show_addresses.feature +18 -0
- data/features/descriptions/satoshi_wallet/show_version.feature +17 -0
- data/features/descriptions/satoshi_wallet/subcommand_help.feature +20 -0
- data/features/fixtures/ABOUT_FIXTURES.markdown +6 -0
- data/features/fixtures/addressbook_wallet.dat +0 -0
- data/features/fixtures/new_wallet.dat +0 -0
- data/features/step_definitions/command_steps.rb +3 -0
- data/features/step_definitions/wallet_steps.rb +11 -0
- data/features/support/env.rb +8 -1
- data/lib/bitcoin/cli.rb +35 -0
- data/lib/bitcoin/commands.rb +3 -0
- data/lib/bitcoin/commands/help_command.rb +32 -0
- data/lib/bitcoin/commands/satoshi_wallet.rb +11 -0
- data/lib/bitcoin/commands/satoshi_wallet/add_address_command.rb +61 -0
- data/lib/bitcoin/commands/satoshi_wallet/show_addresses_command.rb +16 -0
- data/lib/bitcoin/commands/satoshi_wallet/show_version_command.rb +15 -0
- data/lib/bitcoin/commands/satoshi_wallet_command.rb +37 -0
- data/lib/bitcoin/commands/satoshi_wallet_command_environment.rb +28 -0
- data/lib/bitcoin/console/capturing_stream_bundle.rb +42 -0
- data/lib/bitcoin/console/stream_bundle.rb +21 -0
- data/lib/bitcoin/data_access/satoshi/bdb_satoshi_wallet_repository.rb +155 -0
- data/lib/bitcoin/data_access/satoshi/satoshi_version.rb +58 -0
- data/lib/bitcoin/data_access/satoshi/satoshi_wallet.rb +39 -0
- data/lib/bitcoin/domain/address_book.rb +19 -0
- data/lib/bitcoin/domain/bitcoin_address.rb +33 -0
- data/lib/bitcoin/filesystem/empty_temp_dir.rb +74 -0
- data/lib/bitcoin/rspec/argument_matchers.rb +1 -0
- data/lib/bitcoin/rspec/argument_matchers/block_evaluating_to_matcher.rb +23 -0
- data/lib/bitcoin/rspec/directory_helpers.rb +22 -0
- data/lib/bitcoin/version.rb +1 -1
- data/spec/bitcoin/cli_spec.rb +128 -0
- data/spec/bitcoin/commands/help_command_spec.rb +53 -0
- data/spec/bitcoin/commands/satoshi_wallet/add_address_command_spec.rb +149 -0
- data/spec/bitcoin/commands/satoshi_wallet/show_addresses_command_spec.rb +26 -0
- data/spec/bitcoin/commands/satoshi_wallet/show_version_command_spec.rb +26 -0
- data/spec/bitcoin/commands/satoshi_wallet_command_environment_spec.rb +76 -0
- data/spec/bitcoin/commands/satoshi_wallet_command_spec.rb +73 -0
- data/spec/bitcoin/console/_contracts/stream_bundle_contract.rb +29 -0
- data/spec/bitcoin/console/capturing_stream_bundle_spec.rb +74 -0
- data/spec/bitcoin/console/stream_bundle_spec.rb +13 -0
- data/spec/bitcoin/data_access/satoshi/bdb_satoshi_wallet_repository_spec.rb +78 -0
- data/spec/bitcoin/data_access/satoshi/satoshi_version_spec.rb +112 -0
- data/spec/bitcoin/data_access/satoshi/satoshi_wallet_spec.rb +102 -0
- data/spec/bitcoin/domain/address_book_spec.rb +63 -0
- data/spec/bitcoin/domain/bitcoin_address_spec.rb +52 -0
- data/spec/bitcoin/filesystem/empty_temp_dir_spec.rb +170 -0
- data/spec/bitcoin/rspec/argument_matchers/block_evaluating_to_matcher_spec.rb +36 -0
- data/spec/spec_helper.rb +29 -1
- 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
|