itsy-btc 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +21 -0
- data/LICENSE +7 -0
- data/README.md +108 -0
- data/bin/itsy +41 -0
- data/itsy-btc.gemspec +21 -0
- data/lib/itsy-btc.rb +38 -0
- data/lib/itsy-btc/commands.rb +37 -0
- data/lib/itsy-btc/commands/balance_command.rb +24 -0
- data/lib/itsy-btc/commands/command.rb +27 -0
- data/lib/itsy-btc/commands/comment_command.rb +36 -0
- data/lib/itsy-btc/commands/decrypt_command.rb +23 -0
- data/lib/itsy-btc/commands/delete_command.rb +29 -0
- data/lib/itsy-btc/commands/gen_command.rb +27 -0
- data/lib/itsy-btc/commands/help_command.rb +22 -0
- data/lib/itsy-btc/commands/import_command.rb +63 -0
- data/lib/itsy-btc/commands/init_command.rb +25 -0
- data/lib/itsy-btc/commands/list_command.rb +26 -0
- data/lib/itsy-btc/commands/passwd_command.rb +29 -0
- data/lib/itsy-btc/commands/push_command.rb +30 -0
- data/lib/itsy-btc/commands/sign_command.rb +66 -0
- data/lib/itsy-btc/commands/tx_command.rb +152 -0
- data/lib/itsy-btc/commands/verify_command.rb +40 -0
- data/lib/itsy-btc/lookup.rb +76 -0
- data/lib/itsy-btc/wallet.rb +156 -0
- data/lib/itsy-btc/wallet_entry.rb +6 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6cc6b7da4cccd2c198f421508ea9f27b44b6015e
|
4
|
+
data.tar.gz: 5296961199659fb87256ce91015965ad16111668
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9b62525545c12765e7ecd10fbe1382d1a98c6b325d7588f385c7b917590bed6e614b8e449808de91753b5f52245cd8bafd0ec9d3097e88b69fc5f40cdc4a500e
|
7
|
+
data.tar.gz: b19b4673fdcaa634a68933265cf5f3579f0cba67979ab4c5bdfb3db64c14be2090b9d87929ce1219b928129a1472935e9b221c1a2ca30406db7884006f2c7a11
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
itsy-btc (0.0.1)
|
5
|
+
bitcoin-ruby
|
6
|
+
bundler
|
7
|
+
highline
|
8
|
+
open4
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: http://rubygems.org/
|
12
|
+
specs:
|
13
|
+
bitcoin-ruby (0.0.2)
|
14
|
+
highline (1.6.20)
|
15
|
+
open4 (1.3.0)
|
16
|
+
|
17
|
+
PLATFORMS
|
18
|
+
ruby
|
19
|
+
|
20
|
+
DEPENDENCIES
|
21
|
+
itsy-btc!
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2013 Jeremy Ruten
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# itsy-btc
|
2
|
+
|
3
|
+
`itsy` is a funny little bitcoin wallet for people who can't trust their computers to keep a secret, especially when their *money* exists entirely as a collection of secrets. It also feels a lot like using git, where transactions are like commits that you create, sign, and then push to the distributed repository, i.e. the blockchain. If you like git, then you will feel right at home with this tool.
|
4
|
+
|
5
|
+
## Rationale
|
6
|
+
|
7
|
+
Here's the main problem this tool tries to solve: You likely have a computer that is years old, and over those many years you have downloaded and run countless executables from the internet, many of which themselves run on your computer and accept all sorts of connections to your computer from anyone in the world. You have to assume there are vulnerabilities in some of the code you have ran during your computer's history, and so you really can't assume that there isn't some ~~bad~~ code running that is inspecting your computer's memory for secrets and broadcasting them periodically to ~~bad~~ people.
|
8
|
+
|
9
|
+
Now it does seem rather unlikely to me that my keys are being logged, or my screen is being shot, or my RAM is being scanned, for purposes of skimming bitcoin private keys. But I think it's good to be paranoid about the possibility of my secrets being stolen even if their unencrypted form only exists in RAM for short amounts of time. As bitcoin gets more popular, these kinds of attacks will probably become common. (Probably mostly by frustrated miners who can no longer afford the hardware to keep up, so they have to "mine" their precious bitcoins in more creative ways...) Another thing to worry about is all the intentional backdoors at the OS or hardware level that have been uncovered lately, through which who-knows-how-many employees could get into your computer and swipe some secrets.
|
10
|
+
|
11
|
+
So the only solution I see is to create and manage all your secrets on a computer that is permanently offline. A nice cheap, convenient solution is to use a Raspberry Pi for this.
|
12
|
+
|
13
|
+
## Install
|
14
|
+
|
15
|
+
Install the `itsy-btc` gem on your online, every-day computer:
|
16
|
+
|
17
|
+
$ gem install itsy-btc
|
18
|
+
|
19
|
+
Then you want to install it on your offline computer as well. Simply fetch the gem file and its dependencies and transfer them to your offline computer with a USB stick or similar, and install:
|
20
|
+
|
21
|
+
$ gem fetch itsy-btc bitcoin-ruby ffi highline
|
22
|
+
$ cp *.gem /mnt/usb/
|
23
|
+
... switch computers ...
|
24
|
+
$ cd /mnt/usb
|
25
|
+
$ gem install itsy-btc-0.0.1.gem
|
26
|
+
|
27
|
+
## Basic usage
|
28
|
+
|
29
|
+
Most commands operate on a file called WALLET.itsy that is in your current directory. To create this file, use the `init` command:
|
30
|
+
|
31
|
+
$ itsy init
|
32
|
+
|
33
|
+
It will ask what passphrase to use to encrypt the wallet with. Make it a good one!
|
34
|
+
|
35
|
+
Now generate an address:
|
36
|
+
|
37
|
+
$ itsy gen "optional comment"
|
38
|
+
|
39
|
+
You can generate as many as you want. List them like this:
|
40
|
+
|
41
|
+
$ itsy list
|
42
|
+
|
43
|
+
To make transactions, your online computer needs to know what all your addresses are, so copy your wallet file to your online machine. Your private keys are encrypted in this file, so as long as you never type your passphrase on your online computer, they should be safe.
|
44
|
+
|
45
|
+
$ cp WALLET.itsy /mnt/usb/
|
46
|
+
|
47
|
+
Then on your online machine, after you've received money into your address, you can create a transaction to send some money to someone:
|
48
|
+
|
49
|
+
$ cd /mnt/usb
|
50
|
+
$ ls
|
51
|
+
WALLET.itsy
|
52
|
+
$ itsy tx 0.25 18wh2CbAjeB7i1w3zjPX8iCasy1zMYYgnv
|
53
|
+
Will send 0.25 BTC to 18wh2CbAjeB7i1w3zjPX8iCasy1zMYYgnv with a fee of 0.0001 BTC.
|
54
|
+
Transaction saved to 8b1738aaadb37a6c51d9caeec31a0b4f6bb0e0aa6ef7346a36a2813437298964.json.
|
55
|
+
|
56
|
+
It figures out what inputs to use using information downloaded from blockexplorer.com.
|
57
|
+
|
58
|
+
Okay, you have the transaction as a json file on your USB device. Now switch to your offline computer to sign the transaction:
|
59
|
+
|
60
|
+
$ itsy sign 8b1738aaadb37a6c51d9caeec31a0b4f6bb0e0aa6ef7346a36a2813437298964.json
|
61
|
+
1 input signed, transaction saved to 8b1738aaadb37a6c51d9caeec31a0b4f6bb0e0aa6ef7346a36a2813437298964-signed.json.
|
62
|
+
|
63
|
+
Finally, transfer this new file to your online computer and push the signed transaction to the block chain:
|
64
|
+
|
65
|
+
$ itsy push 8b1738aaadb37a6c51d9caeec31a0b4f6bb0e0aa6ef7346a36a2813437298964-signed.json
|
66
|
+
Transaction pushed successfully.
|
67
|
+
|
68
|
+
## Advanced usage
|
69
|
+
|
70
|
+
Create custom transaction:
|
71
|
+
|
72
|
+
$ itsy tx
|
73
|
+
(1) Add input, (2) Add output, (3) Save, or (4) Cancel? 1
|
74
|
+
Prev tx? f480a09eb541c27fa4d3b7f3d3fceabde50840758248327fd0fb3cc6970c7486
|
75
|
+
Output index? 0
|
76
|
+
Input added. (Amount: 0.25 BTC)
|
77
|
+
(1) Add input, (2) Add output, (3) Save, or (4) Cancel? 2
|
78
|
+
Address? 1B2p7d1BPMqVrQMb97kTk5RbUvUH9CetBK
|
79
|
+
Amount? 0.1
|
80
|
+
Output added.
|
81
|
+
(1) Add input, (2) Add output, (3) Save, or (4) Cancel? 2
|
82
|
+
Address? 1C19FTsSj2ahsRjGfRFuiyDm9QJg9UZVSJ
|
83
|
+
Amount? 0.1499
|
84
|
+
Output added.
|
85
|
+
(1) Add input, (2) Add output, (3) Save, or (4) Cancel? 3
|
86
|
+
Will send 0.1 BTC from 1 input to 1B2p7d1BPMqVrQMb97kTk5RbUvUH9CetBK with a 0.0001 BTC fee.
|
87
|
+
Transaction saved to 9e62c79d41670168e11e98f68ed709ea22004a73978c37384e7d9613e5e2961e.json.
|
88
|
+
|
89
|
+
Authenticate to access encrypted wallet during a session (otherwise it will ask for your key every time you sign a transaction or modify your wallet, which might be okay):
|
90
|
+
|
91
|
+
$ itsy auth
|
92
|
+
Your passphrase? mysup3rs3cretpassw0rd
|
93
|
+
Wallet unlocked until poweroff.
|
94
|
+
|
95
|
+
## TODO
|
96
|
+
|
97
|
+
* Unit tests
|
98
|
+
* Tighten error checking, integrity checks all over, i.e. make it hard for users to do anything stupid
|
99
|
+
* Document commands thoroughly, so `itsy help <command>` gives lots of info for each command
|
100
|
+
* Experiment with interacting with the bitcoin network directly (with bitcoin-ruby) to remove some of the dependency on blockexplorer.com (especially for the `push` command)
|
101
|
+
|
102
|
+
* Idea: maybe wallet should be partitioned into hot and cold storage, with simple ways of transferring one to the other. Then you can have the best of both worlds.
|
103
|
+
|
104
|
+
## Warning
|
105
|
+
|
106
|
+
This project is just me trying to do bitcoin the way I like to do things. I'm actually very new and inexperienced in bitcoin, crypto, and security. So read all the code and understand how it works before using it in any serious way, please.
|
107
|
+
|
108
|
+
|
data/bin/itsy
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require "itsy-btc"
|
4
|
+
|
5
|
+
if ARGV.first == "test"
|
6
|
+
ItsyBtc.test_mode!
|
7
|
+
ARGV.shift
|
8
|
+
end
|
9
|
+
|
10
|
+
WALLET_PATH = ItsyBtc::DEFAULT_WALLET_FILENAME
|
11
|
+
|
12
|
+
wallet = nil
|
13
|
+
if File.exists?(WALLET_PATH)
|
14
|
+
wallet = ItsyBtc::Wallet.new(WALLET_PATH)
|
15
|
+
end
|
16
|
+
|
17
|
+
if ARGV.empty?
|
18
|
+
puts "Yes, that's my name. Now what do you want?"
|
19
|
+
puts
|
20
|
+
command_name = "help"
|
21
|
+
else
|
22
|
+
command_name = ARGV.shift.downcase
|
23
|
+
end
|
24
|
+
|
25
|
+
command_class = ItsyBtc::Commands::LIST.find { |cmd| cmd.name == command_name }
|
26
|
+
|
27
|
+
if command_class
|
28
|
+
begin
|
29
|
+
command = command_class.new(*ARGV)
|
30
|
+
command.wallet = wallet
|
31
|
+
command.run
|
32
|
+
rescue OpenSSL::Cipher::CipherError
|
33
|
+
puts "Wrong passphrase! Aborting..."
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
else
|
37
|
+
puts "Unrecognized command: #{command_name}"
|
38
|
+
puts "Type `itsy help` for help."
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
|
data/itsy-btc.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "itsy-btc"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.date = "2013-11-14"
|
5
|
+
s.summary = "A simple secure bitcoin wallet."
|
6
|
+
s.description = "itsy-btc is a simple secure bitcoin wallet that allows you to turn an offline computer into a hardware wallet."
|
7
|
+
s.author = "Jeremy Ruten"
|
8
|
+
s.email = "jeremy.ruten@gmail.com"
|
9
|
+
s.homepage = "http://github.com/yjerem/itsy-btc"
|
10
|
+
s.license = "MIT"
|
11
|
+
s.required_ruby_version = ">= 1.9.2"
|
12
|
+
|
13
|
+
s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "itsy-btc.gemspec", "README.md"]
|
14
|
+
s.files += Dir["lib/**/*.rb"]
|
15
|
+
s.files += Dir["bin/itsy"]
|
16
|
+
s.executables = ["itsy"]
|
17
|
+
|
18
|
+
%w(bundler bitcoin-ruby ffi highline).each do |gem_name|
|
19
|
+
s.add_runtime_dependency gem_name
|
20
|
+
end
|
21
|
+
end
|
data/lib/itsy-btc.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require "bitcoin"
|
2
|
+
require "open-uri"
|
3
|
+
require "highline/import"
|
4
|
+
require "bigdecimal"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
require "itsy-btc/wallet"
|
8
|
+
require "itsy-btc/wallet_entry"
|
9
|
+
require "itsy-btc/lookup"
|
10
|
+
require "itsy-btc/commands"
|
11
|
+
|
12
|
+
module ItsyBtc
|
13
|
+
DEFAULT_WALLET_FILENAME = "WALLET.itsy"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def format_value(satoshis)
|
17
|
+
(BigDecimal.new(satoshis) / 1e8).to_s("F")
|
18
|
+
end
|
19
|
+
|
20
|
+
def blockexplorer_url
|
21
|
+
if test_mode?
|
22
|
+
"http://blockexplorer.com/testnet"
|
23
|
+
else
|
24
|
+
"http://blockexplorer.com"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_mode?
|
29
|
+
@test_mode
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_mode!
|
33
|
+
@test_mode = true
|
34
|
+
Bitcoin.network = :testnet
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "itsy-btc/commands/command"
|
2
|
+
require "itsy-btc/commands/init_command"
|
3
|
+
require "itsy-btc/commands/gen_command"
|
4
|
+
require "itsy-btc/commands/import_command"
|
5
|
+
require "itsy-btc/commands/delete_command"
|
6
|
+
require "itsy-btc/commands/balance_command"
|
7
|
+
require "itsy-btc/commands/list_command"
|
8
|
+
require "itsy-btc/commands/comment_command"
|
9
|
+
require "itsy-btc/commands/tx_command"
|
10
|
+
require "itsy-btc/commands/sign_command"
|
11
|
+
require "itsy-btc/commands/verify_command"
|
12
|
+
require "itsy-btc/commands/push_command"
|
13
|
+
require "itsy-btc/commands/decrypt_command"
|
14
|
+
require "itsy-btc/commands/passwd_command"
|
15
|
+
require "itsy-btc/commands/help_command"
|
16
|
+
|
17
|
+
module ItsyBtc
|
18
|
+
module Commands
|
19
|
+
LIST = [
|
20
|
+
InitCommand,
|
21
|
+
GenCommand,
|
22
|
+
ImportCommand,
|
23
|
+
DeleteCommand,
|
24
|
+
BalanceCommand,
|
25
|
+
ListCommand,
|
26
|
+
CommentCommand,
|
27
|
+
TxCommand,
|
28
|
+
SignCommand,
|
29
|
+
VerifyCommand,
|
30
|
+
PushCommand,
|
31
|
+
DecryptCommand,
|
32
|
+
PasswdCommand,
|
33
|
+
HelpCommand
|
34
|
+
]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class BalanceCommand < Command
|
4
|
+
name "balance"
|
5
|
+
summary "show your total balance"
|
6
|
+
|
7
|
+
def run
|
8
|
+
if @wallet.nil?
|
9
|
+
puts "No wallet found! Use `itsy init` to create one."
|
10
|
+
exit
|
11
|
+
end
|
12
|
+
|
13
|
+
entries = @wallet.entries
|
14
|
+
if entries.empty?
|
15
|
+
puts "This wallet has no addresses."
|
16
|
+
else
|
17
|
+
total = entries.map { |e| Lookup.balance(e.address) || exit }.inject(:+)
|
18
|
+
puts ItsyBtc.format_value(total) + " BTC"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class Command
|
4
|
+
attr_accessor :wallet
|
5
|
+
|
6
|
+
def run
|
7
|
+
raise NotImplementedError, "command not implemented!"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.name(value=nil)
|
11
|
+
@name = value if value
|
12
|
+
@name
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.args(value=nil)
|
16
|
+
@args = value if value
|
17
|
+
@args
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.summary(value=nil)
|
21
|
+
@summary = value if value
|
22
|
+
@summary
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class CommentCommand < Command
|
4
|
+
name "comment"
|
5
|
+
args "<address> <new comment>"
|
6
|
+
summary "edit an address's comment field"
|
7
|
+
|
8
|
+
def initialize(address, comment)
|
9
|
+
@address = address.to_s
|
10
|
+
@comment = comment.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
if @wallet.nil?
|
15
|
+
puts "No wallet found. Run `itsy init` to create one."
|
16
|
+
exit
|
17
|
+
end
|
18
|
+
|
19
|
+
entries = @wallet.addresses_starting_with(@address)
|
20
|
+
if entries.length == 1
|
21
|
+
address = entries.first.address
|
22
|
+
@wallet.update_comment(address, @comment)
|
23
|
+
puts "Comment for #{address} updated."
|
24
|
+
elsif entries.length == 0
|
25
|
+
puts "No addresses in your wallet start with '#{@address}'."
|
26
|
+
else
|
27
|
+
puts "Multiple addresses in your wallet start with '#{@address}':"
|
28
|
+
entries.each do |entry|
|
29
|
+
puts " #{entry.address} #{entry.comment}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class DecryptCommand < Command
|
4
|
+
name "decrypt"
|
5
|
+
summary "show all of your private keys"
|
6
|
+
|
7
|
+
def run
|
8
|
+
unless @wallet
|
9
|
+
puts "No wallet found! Use `itsy init` to create one."
|
10
|
+
exit
|
11
|
+
end
|
12
|
+
|
13
|
+
@wallet.entries_with_keys.each do |entry|
|
14
|
+
puts "addr: #{entry.address}"
|
15
|
+
puts "key: #{entry.key}"
|
16
|
+
puts "comment: #{entry.comment}"
|
17
|
+
puts
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class DeleteCommand < Command
|
4
|
+
name "delete"
|
5
|
+
args "<address>"
|
6
|
+
summary "delete an address permanently"
|
7
|
+
|
8
|
+
def initialize(address)
|
9
|
+
@address = address.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
if @wallet.nil?
|
14
|
+
puts "No wallet found. Run `itsy init` to create one."
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
|
18
|
+
key = @wallet.key_for_address(@address)
|
19
|
+
if @wallet.remove(@address)
|
20
|
+
puts "Address #{@address} deleted."
|
21
|
+
puts "Here is the key in case you made a mistake: #{key}"
|
22
|
+
else
|
23
|
+
puts "There is no address #{@address} in your wallet."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class GenCommand < Command
|
4
|
+
name "gen"
|
5
|
+
args "[comment]"
|
6
|
+
summary "generate an address"
|
7
|
+
|
8
|
+
def initialize(comment = "")
|
9
|
+
@comment = comment.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
if @wallet.nil?
|
14
|
+
puts "No wallet found. Run `itsy init` to create one."
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
|
18
|
+
key_gen = Bitcoin::Wallet::KeyGenerator.new
|
19
|
+
key = key_gen.get_key
|
20
|
+
@wallet.add(key.to_base58, @comment)
|
21
|
+
|
22
|
+
puts "Address #{@wallet.entries.last.address} added to wallet."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class HelpCommand < Command
|
4
|
+
name "help"
|
5
|
+
summary "print this summary"
|
6
|
+
|
7
|
+
SPACING = 40
|
8
|
+
|
9
|
+
def run
|
10
|
+
puts "Usage: itsy <command> [args...]"
|
11
|
+
puts
|
12
|
+
puts "Available commands:"
|
13
|
+
ItsyBtc::Commands::LIST.each do |cmd_class|
|
14
|
+
spacing = " " * (SPACING - cmd_class.name.length - 1 - cmd_class.args.to_s.length)
|
15
|
+
puts " #{cmd_class.name} #{cmd_class.args}#{spacing}#{cmd_class.summary}"
|
16
|
+
end
|
17
|
+
puts
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class ImportCommand < Command
|
4
|
+
name "import"
|
5
|
+
args "[path]"
|
6
|
+
summary "import an address from base58"
|
7
|
+
|
8
|
+
def initialize(path = nil)
|
9
|
+
@path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
if @wallet.nil?
|
14
|
+
puts "No wallet found. Run `itsy init` to create one."
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
|
18
|
+
f = File.open(@path, "r") if @path
|
19
|
+
|
20
|
+
num_imports = 0
|
21
|
+
done = false
|
22
|
+
while not done
|
23
|
+
if @path
|
24
|
+
line = f.readline
|
25
|
+
base58, *comment = line.split
|
26
|
+
base58 = base58.to_s
|
27
|
+
comment = comment.join(" ")
|
28
|
+
else
|
29
|
+
base58 = ask("Base58 key? ").to_s
|
30
|
+
comment = ask("Comment? (optional) ").to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
key = begin
|
34
|
+
Bitcoin::Key.from_base58(base58)
|
35
|
+
rescue
|
36
|
+
puts "Key '#{base58}' is invalid!" unless base58.empty? && @path
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
if key
|
41
|
+
begin
|
42
|
+
@wallet.add(key.to_base58, comment)
|
43
|
+
num_imports += 1
|
44
|
+
rescue DuplicateAddressError
|
45
|
+
puts "Address '#{key.addr}' already exists in your wallet!"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if @path
|
50
|
+
done = f.eof?
|
51
|
+
else
|
52
|
+
done = ask("Import another key? [Y/n] ").to_s.downcase == "n"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
f.close if @path
|
57
|
+
|
58
|
+
puts "#{num_imports} addresses added to wallet."
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class InitCommand < Command
|
4
|
+
name "init"
|
5
|
+
summary "create new wallet"
|
6
|
+
|
7
|
+
def run
|
8
|
+
path = ItsyBtc::DEFAULT_WALLET_FILENAME
|
9
|
+
if File.exists?(path)
|
10
|
+
puts "A '#{path}' file already exists in this directory!"
|
11
|
+
else
|
12
|
+
passphrase1 = ask("Choose wallet passphrase: ") { |q| q.echo = false }
|
13
|
+
passphrase2 = ask("Verify your passphrase: ") { |q| q.echo = false }
|
14
|
+
if passphrase1 != passphrase2
|
15
|
+
puts "You typed different passphrases! Aborting..."
|
16
|
+
else
|
17
|
+
Wallet.new(path).create(passphrase1)
|
18
|
+
puts "'#{path}' created."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class ListCommand < Command
|
4
|
+
name "list"
|
5
|
+
summary "list your addresses"
|
6
|
+
|
7
|
+
def run
|
8
|
+
if @wallet.nil?
|
9
|
+
puts "No wallet found! Use `itsy init` to create one."
|
10
|
+
exit
|
11
|
+
end
|
12
|
+
|
13
|
+
entries = @wallet.entries
|
14
|
+
if entries.empty?
|
15
|
+
puts "This wallet is empty."
|
16
|
+
else
|
17
|
+
puts "Address Comment"
|
18
|
+
entries.each do |entry|
|
19
|
+
puts "#{entry.address} #{entry.comment}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class PasswdCommand < Command
|
4
|
+
name "passwd"
|
5
|
+
summary "change your wallet passphrase"
|
6
|
+
|
7
|
+
def run
|
8
|
+
if @wallet.nil?
|
9
|
+
puts "No wallet found. Run `itsy init` to create one."
|
10
|
+
exit
|
11
|
+
end
|
12
|
+
|
13
|
+
old_passphrase = ask("Old wallet passphrase? ") { |q| q.echo = false }
|
14
|
+
@wallet.verify_passphrase(old_passphrase)
|
15
|
+
|
16
|
+
new_passphrase1 = ask("New wallet passphrase? ") { |q| q.echo = false }
|
17
|
+
new_passphrase2 = ask("Verify passphrase? ") { |q| q.echo = false }
|
18
|
+
|
19
|
+
if new_passphrase1 != new_passphrase2
|
20
|
+
puts "You typed different passphrases! Aborting..."
|
21
|
+
else
|
22
|
+
@wallet.change_passphrase(new_passphrase1)
|
23
|
+
puts "Passphrase changed."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "net/http"
|
2
|
+
|
3
|
+
module ItsyBtc
|
4
|
+
module Commands
|
5
|
+
class PushCommand < Command
|
6
|
+
name "push"
|
7
|
+
args "<path to tx>"
|
8
|
+
summary "push a transaction to the network"
|
9
|
+
|
10
|
+
def initialize(tx_path)
|
11
|
+
@tx_path = tx_path
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
tx = Bitcoin::Protocol::Tx.from_json_file(@tx_path)
|
16
|
+
hex = tx.to_payload.unpack("H*")[0]
|
17
|
+
|
18
|
+
uri = URI("http://blockchain.info/pushtx")
|
19
|
+
response = Net::HTTP.post_form(uri, "tx" => hex)
|
20
|
+
if response.is_a? Net::HTTPSuccess
|
21
|
+
puts "Transaction pushed successfully."
|
22
|
+
else
|
23
|
+
puts "#{response.code} Error!"
|
24
|
+
puts "Blockchain.info says: #{response.body}" if response.response_body_permitted?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Commands
|
3
|
+
class SignCommand < Command
|
4
|
+
name "sign"
|
5
|
+
args "<path to tx>"
|
6
|
+
summary "sign a transaction"
|
7
|
+
|
8
|
+
def initialize(tx_path)
|
9
|
+
@tx_path = tx_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
if @wallet.nil?
|
14
|
+
puts "No wallet found! Use `itsy init` to create one."
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
|
18
|
+
tx = Bitcoin::Protocol::Tx.from_json_file(@tx_path)
|
19
|
+
|
20
|
+
inputs_signed = 0
|
21
|
+
tx.in.each.with_index do |txin, i|
|
22
|
+
script = Bitcoin::Script.new(txin.script_sig)
|
23
|
+
if script.type == :unknown
|
24
|
+
puts "Input ##{i} already signed!"
|
25
|
+
inputs_signed += 1
|
26
|
+
else
|
27
|
+
address = case script.type
|
28
|
+
when :hash160
|
29
|
+
script.get_hash160_address
|
30
|
+
when :pubkey
|
31
|
+
script.get_pubkey_address
|
32
|
+
else
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
if address
|
36
|
+
if base58_key = @wallet.key_for_address(address)
|
37
|
+
key = Bitcoin.open_key(Bitcoin::Key.from_base58(base58_key).priv)
|
38
|
+
sig = Bitcoin.sign_data(key, tx.signature_hash_for_input(i, nil, txin.script_sig))
|
39
|
+
txin.script_sig = Bitcoin::Script.to_signature_pubkey_script(sig, [key.public_key_hex].pack("H*"))
|
40
|
+
puts "Input ##{i} signed."
|
41
|
+
inputs_signed += 1
|
42
|
+
else
|
43
|
+
puts "Input ##{i} to address #{address} cannot be signed, that address is not in your wallet!"
|
44
|
+
end
|
45
|
+
else
|
46
|
+
puts "Input ##{i} cannot be signed, its script type is #{script.type} which is unsupported."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
tx = Bitcoin::Protocol::Tx.new(tx.to_payload)
|
52
|
+
|
53
|
+
if inputs_signed == 0
|
54
|
+
puts "No inputs signed."
|
55
|
+
elsif inputs_signed == tx.in.length
|
56
|
+
File.open("#{tx.hash}-signed.json", "w") { |f| f << tx.to_json }
|
57
|
+
puts "Transaction signed, saved to #{tx.hash}-signed.json."
|
58
|
+
else
|
59
|
+
File.open("#{tx.hash}-signed#{inputs_signed}.json", "w") { |f| f << tx.to_json }
|
60
|
+
puts "#{inputs_signed} of #{tx.in.length} inputs signed, saved to #{tx.hash}-signed#{inputs_signed}.json."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module ItsyBtc
|
4
|
+
module Commands
|
5
|
+
class TxCommand < Command
|
6
|
+
name "tx"
|
7
|
+
args "[<amount> <recipient>]"
|
8
|
+
summary "create a transaction"
|
9
|
+
|
10
|
+
def initialize(amount = nil, recipient = nil)
|
11
|
+
@amount = amount
|
12
|
+
@recipient = recipient
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
if @amount && @recipient
|
17
|
+
create_automatic_tx
|
18
|
+
else
|
19
|
+
create_custom_tx
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def create_automatic_tx
|
26
|
+
if @wallet.nil?
|
27
|
+
puts "No wallet found! Use `itsy init` to create one."
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
|
31
|
+
if not Bitcoin.valid_address?(@recipient)
|
32
|
+
puts "The recipient address you specified is not a valid Bitcoin address."
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
|
36
|
+
tx = Bitcoin::Protocol::Tx.new
|
37
|
+
|
38
|
+
amount = (BigDecimal.new(@amount) * 1e8).to_i
|
39
|
+
tx_fee = 10000
|
40
|
+
current_value = 0
|
41
|
+
@wallet.entries.shuffle.each do |e|
|
42
|
+
outputs = Lookup.unspent_outputs(e.address) || exit
|
43
|
+
outputs.shuffle.each do |output|
|
44
|
+
prev_tx_hash = [output[:tx_hash]].pack("H*").reverse.unpack("H*")[0]
|
45
|
+
prev_tx = Lookup.tx(prev_tx_hash) || exit
|
46
|
+
txin = Bitcoin::Protocol::TxIn.new(prev_tx.binary_hash, output[:tx_output_n], 0)
|
47
|
+
txin.script_sig = prev_tx.out[output[:tx_output_n]].pk_script
|
48
|
+
tx.add_in txin
|
49
|
+
|
50
|
+
current_value += output[:value]
|
51
|
+
break if current_value >= amount + tx_fee
|
52
|
+
end
|
53
|
+
break if current_value >= amount + tx_fee
|
54
|
+
end
|
55
|
+
|
56
|
+
if current_value < amount
|
57
|
+
puts "Insufficient funds! You only have #{Bitcoin.format_value(current_value)} BTC."
|
58
|
+
exit
|
59
|
+
elsif current_value < amount + tx_fee
|
60
|
+
tx_fee = current_value - amount
|
61
|
+
puts "Warning: only #{Bitcoin.format_value(tx_fee)} BTC left for transaction fees."
|
62
|
+
end
|
63
|
+
|
64
|
+
change = current_value - amount - tx_fee
|
65
|
+
change_address = @wallet.entries.sample.address
|
66
|
+
|
67
|
+
tx.add_out Bitcoin::Protocol::TxOut.value_to_address(amount, @recipient)
|
68
|
+
tx.add_out Bitcoin::Protocol::TxOut.value_to_address(change, change_address)
|
69
|
+
|
70
|
+
tx = Bitcoin::Protocol::Tx.new(tx.to_payload)
|
71
|
+
|
72
|
+
print "Will send #{ItsyBtc.format_value(amount)} BTC to #{@recipient}, "
|
73
|
+
if tx_fee == 0
|
74
|
+
print "with no transaction fees.\n"
|
75
|
+
else
|
76
|
+
print "with a fee of #{ItsyBtc.format_value(tx_fee)} BTC.\n"
|
77
|
+
end
|
78
|
+
if change > 0
|
79
|
+
puts "#{ItsyBtc.format_value(change)} BTC will be sent to #{change_address} as change."
|
80
|
+
end
|
81
|
+
|
82
|
+
File.open("#{tx.hash}.json", "w") { |f| f << tx.to_json }
|
83
|
+
puts "Transaction saved to #{tx.hash}.json."
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_custom_tx
|
87
|
+
tx = Bitcoin::Protocol::Tx.new
|
88
|
+
|
89
|
+
total_input = 0
|
90
|
+
total_output = 0
|
91
|
+
loop do
|
92
|
+
msg = "(1) Add input, (2) Add output, (3) Show summary, (4) Save, or (5) Cancel? "
|
93
|
+
choice = ask(msg, Integer) { |q| q.in = 1..5 }
|
94
|
+
case choice
|
95
|
+
when 1
|
96
|
+
prev_tx = Lookup.tx(ask("Prev tx? ").to_s.strip) || exit
|
97
|
+
if prev_tx
|
98
|
+
output_index = ask("Output index? ", Integer)
|
99
|
+
txin = Bitcoin::Protocol::TxIn.new(prev_tx.binary_hash, output_index, 0)
|
100
|
+
txin.script_sig = prev_tx.out[output_index].pk_script
|
101
|
+
tx.add_in txin
|
102
|
+
puts "Input added. (#{ItsyBtc.format_value(prev_tx.out[output_index].value)} BTC)"
|
103
|
+
total_input += prev_tx.out[output_index].value
|
104
|
+
end
|
105
|
+
when 2
|
106
|
+
address = ask("Address? ").to_s
|
107
|
+
if Bitcoin.valid_address? address
|
108
|
+
amount = ask("Amount? ").to_s
|
109
|
+
value = BigDecimal.new(amount) * 1e8
|
110
|
+
raise "precision error! (TODO)" if value != value.to_i
|
111
|
+
tx.add_out Bitcoin::Protocol::TxOut.value_to_address(value.to_i, address)
|
112
|
+
puts "Output added. (#{ItsyBtc.format_value(value.to_i)} BTC)"
|
113
|
+
total_output += value.to_i
|
114
|
+
else
|
115
|
+
puts "Invalid address!"
|
116
|
+
end
|
117
|
+
when 3
|
118
|
+
print_summary(total_input, total_output, tx.out.length)
|
119
|
+
when 4
|
120
|
+
tx = Bitcoin::Protocol::Tx.new(tx.to_payload)
|
121
|
+
print_summary(total_input, total_output, tx.out.length)
|
122
|
+
File.open("#{tx.hash}.json", "w") { |f| f << tx.to_json }
|
123
|
+
puts "Transaction saved to #{tx.hash}.json."
|
124
|
+
exit
|
125
|
+
when 5
|
126
|
+
puts "No transaction created."
|
127
|
+
exit
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def print_summary(total_input, total_output, num_addresses)
|
133
|
+
fees = total_input - total_output
|
134
|
+
print "Will send #{ItsyBtc.format_value(total_output)} BTC "
|
135
|
+
print "to #{num_addresses} address#{'es' if num_addresses != 1}, "
|
136
|
+
if fees == 0
|
137
|
+
print "with no transaction fees.\n"
|
138
|
+
else
|
139
|
+
print "with a fee of #{ItsyBtc.format_value(fees)} BTC.\n"
|
140
|
+
end
|
141
|
+
if fees < 0
|
142
|
+
puts "Warning! This transaction is spending more than it is receiving!"
|
143
|
+
elsif fees < 10000
|
144
|
+
puts "Warning! Transaction fees should be at least 0.0001 BTC!"
|
145
|
+
elsif fees > 1000000
|
146
|
+
puts "Warning! Transaction fees are very large."
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module ItsyBtc
|
4
|
+
module Commands
|
5
|
+
class VerifyCommand < Command
|
6
|
+
name "verify"
|
7
|
+
args "<path to tx>"
|
8
|
+
summary "verify that a transaction is signed"
|
9
|
+
|
10
|
+
def initialize(path)
|
11
|
+
@path = path
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
tx = Bitcoin::Protocol::Tx.from_json_file(@path)
|
16
|
+
|
17
|
+
signed = true
|
18
|
+
tx.in.each.with_index do |txin, idx|
|
19
|
+
if Bitcoin::Script.new(txin.script_sig).type == :unknown
|
20
|
+
prev_tx = Lookup.tx(txin.previous_output)
|
21
|
+
if tx.verify_input_signature(idx, prev_tx)
|
22
|
+
puts "Input ##{idx} has a valid signature."
|
23
|
+
else
|
24
|
+
puts "Input ##{idx} has an invalid signature!"
|
25
|
+
signed = false
|
26
|
+
end
|
27
|
+
else
|
28
|
+
puts "Input ##{idx} is not signed."
|
29
|
+
signed = false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if tx.in.empty?
|
34
|
+
puts "Transaction has no inputs."
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ItsyBtc
|
2
|
+
module Lookup
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def tx(hash)
|
6
|
+
json_tx = open(ItsyBtc.blockexplorer_url + "/rawtx/#{hash}").read
|
7
|
+
Bitcoin::Protocol::Tx.from_json(json_tx)
|
8
|
+
rescue OpenURI::HTTPError => e
|
9
|
+
puts "Error looking up transaction: #{e.message}"
|
10
|
+
case e.io.status[0].to_i
|
11
|
+
when 400
|
12
|
+
puts "The transaction hash is invalid."
|
13
|
+
when 404
|
14
|
+
puts "blockexplorer.com doesn't know about the transaction."
|
15
|
+
else
|
16
|
+
puts "Make sure you have an internet connection and can load blockexplorer.com."
|
17
|
+
end
|
18
|
+
false
|
19
|
+
rescue SocketError => e
|
20
|
+
puts "Error looking up transaction: #{e.message}"
|
21
|
+
puts "Make sure you have an internet connection and can load blockexplorer.com."
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def unspent_outputs(address)
|
26
|
+
json = open("http://blockchain.info/unspent?active=#{address}&format=json").read
|
27
|
+
data = JSON.parse(json, symbolize_names: true)
|
28
|
+
data[:unspent_outputs]
|
29
|
+
rescue OpenURI::HTTPError => e
|
30
|
+
if e.io.status[0].to_i == 500
|
31
|
+
[]
|
32
|
+
else
|
33
|
+
puts "Error looking up unspent outputs: #{e.message}"
|
34
|
+
puts "Make sure you have an internet connection and can load blockchain.info."
|
35
|
+
false
|
36
|
+
end
|
37
|
+
rescue SocketEror => e
|
38
|
+
puts "Error looking up unspent outputs: #{e.message}"
|
39
|
+
puts "Make sure you have an internet connection and can load blockchain.info."
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def balance(address)
|
44
|
+
html = open(ItsyBtc.blockexplorer_url + "/address/#{address}").read
|
45
|
+
received_btc = nil
|
46
|
+
sent_btc = nil
|
47
|
+
if html =~ /Received BTC: ([0-9.]+)/
|
48
|
+
received_btc = BigDecimal.new($1)
|
49
|
+
end
|
50
|
+
if html =~ /Sent BTC: ([0-9.]+)/
|
51
|
+
sent_btc = BigDecimal.new($1)
|
52
|
+
end
|
53
|
+
if received_btc && sent_btc
|
54
|
+
btc = received_btc - sent_btc
|
55
|
+
(btc * 1e8).to_i
|
56
|
+
else
|
57
|
+
puts "Can't parse the balance from blockexplorer.com's HTML. (They must have changed it.)"
|
58
|
+
false
|
59
|
+
end
|
60
|
+
rescue OpenURI::HTTPError => e
|
61
|
+
puts "Error looking up balance: #{e.message}"
|
62
|
+
case e.io.status[0].to_i
|
63
|
+
when 400
|
64
|
+
puts "The address is invalid."
|
65
|
+
else
|
66
|
+
puts "Make sure you have an internet connection and can load blockexplorer.com."
|
67
|
+
end
|
68
|
+
false
|
69
|
+
rescue SocketError => e
|
70
|
+
puts "Error looking up balance: #{e.message}"
|
71
|
+
puts "Make sure you have an internet connection and can load blockexplorer.com."
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require "openssl"
|
2
|
+
|
3
|
+
module ItsyBtc
|
4
|
+
class Wallet
|
5
|
+
ALGORITHM = "aes-256-cbc"
|
6
|
+
PBKDF2_ITERATIONS = 4096
|
7
|
+
SALT_LEN = 8
|
8
|
+
|
9
|
+
class WalletError < RuntimeError; end
|
10
|
+
class WalletReadError < WalletError; end
|
11
|
+
class DuplicateAddressError < WalletError; end
|
12
|
+
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
def initialize(path)
|
16
|
+
@path = path
|
17
|
+
@passphrase = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def create(passphrase)
|
21
|
+
@passphrase = passphrase
|
22
|
+
write({entries: [], keys: encrypt_keys([])})
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(base58, comment = "")
|
26
|
+
key = Bitcoin::Key.from_base58(base58)
|
27
|
+
data = read
|
28
|
+
raise DuplicateAddressError if data[:entries].any? { |e| e[:address] == key.addr }
|
29
|
+
data[:entries] << { address: key.addr, comment: comment }
|
30
|
+
keys = decrypt_keys(data[:keys])
|
31
|
+
keys << key.to_base58
|
32
|
+
data[:keys] = encrypt_keys(keys)
|
33
|
+
write(data)
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove(address)
|
37
|
+
data = read
|
38
|
+
keys = decrypt_keys(data[:keys])
|
39
|
+
|
40
|
+
entry_index = data[:entries].index { |e| e[:address] == address }
|
41
|
+
if entry_index
|
42
|
+
data[:entries].delete_at(entry_index)
|
43
|
+
keys.delete_at(entry_index)
|
44
|
+
|
45
|
+
data[:keys] = encrypt_keys(keys)
|
46
|
+
write(data)
|
47
|
+
true
|
48
|
+
else
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def verify_passphrase(passphrase)
|
54
|
+
@passphrase = passphrase
|
55
|
+
decrypt_keys(read[:keys])
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def change_passphrase(new_passphrase)
|
60
|
+
data = read
|
61
|
+
keys = decrypt_keys(data[:keys])
|
62
|
+
@passphrase = new_passphrase
|
63
|
+
data[:keys] = encrypt_keys(keys)
|
64
|
+
write(data)
|
65
|
+
end
|
66
|
+
|
67
|
+
def update_comment(address, comment)
|
68
|
+
data = read
|
69
|
+
entry_index = data[:entries].index { |e| e[:address] == address }
|
70
|
+
data[:entries][entry_index][:comment] = comment
|
71
|
+
write(data)
|
72
|
+
end
|
73
|
+
|
74
|
+
def addresses_starting_with(beginning)
|
75
|
+
entries.select { |e| e.address.start_with? beginning }
|
76
|
+
end
|
77
|
+
|
78
|
+
def key_for_address(address)
|
79
|
+
if entry = entries_with_keys.find { |e| e.address == address }
|
80
|
+
entry.key
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def entries
|
85
|
+
read[:entries].inject([]) do |list, entry|
|
86
|
+
e = WalletEntry.new
|
87
|
+
e.address = entry[:address]
|
88
|
+
e.comment = entry[:comment]
|
89
|
+
list << e
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def entries_with_keys
|
94
|
+
data = read
|
95
|
+
keys = decrypt_keys(data[:keys])
|
96
|
+
data[:entries].inject([]) do |list, entry|
|
97
|
+
e = WalletEntry.new
|
98
|
+
e.address = entry[:address]
|
99
|
+
e.comment = entry[:comment]
|
100
|
+
e.key = keys.shift
|
101
|
+
list << e
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def read
|
108
|
+
data = JSON.parse(File.read(@path), symbolize_names: true)
|
109
|
+
raise "wallet is invalid!" if data[:entries].nil?
|
110
|
+
raise "wallet is invalid!" if data[:keys].nil?
|
111
|
+
data
|
112
|
+
end
|
113
|
+
|
114
|
+
def write(data)
|
115
|
+
raise "trying to write invalid data to wallet!" if data[:entries].nil?
|
116
|
+
raise "trying to write invalid data to wallet!" if data[:keys].nil?
|
117
|
+
File.write(@path, JSON.pretty_generate(data))
|
118
|
+
end
|
119
|
+
|
120
|
+
def encrypt_keys(keys)
|
121
|
+
encrypt(JSON.generate(keys)).unpack("H*")[0]
|
122
|
+
end
|
123
|
+
|
124
|
+
def decrypt_keys(hex)
|
125
|
+
JSON.parse(decrypt([hex].pack("H*")))
|
126
|
+
end
|
127
|
+
|
128
|
+
def encrypt(txt)
|
129
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
130
|
+
cipher.encrypt
|
131
|
+
salt = OpenSSL::Random.random_bytes(SALT_LEN)
|
132
|
+
key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, PBKDF2_ITERATIONS, cipher.key_len + cipher.iv_len)
|
133
|
+
cipher.key = key_iv[0, cipher.key_len]
|
134
|
+
cipher.iv = key_iv[cipher.key_len, cipher.iv_len]
|
135
|
+
encrypted_txt = cipher.update(txt) + cipher.final
|
136
|
+
|
137
|
+
salt + encrypted_txt
|
138
|
+
end
|
139
|
+
|
140
|
+
def decrypt(data)
|
141
|
+
salt, encrypted_txt = data[0, SALT_LEN], data[SALT_LEN..-1]
|
142
|
+
|
143
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
144
|
+
cipher.decrypt
|
145
|
+
key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, PBKDF2_ITERATIONS, cipher.key_len + cipher.iv_len)
|
146
|
+
cipher.key = key_iv[0, cipher.key_len]
|
147
|
+
cipher.iv = key_iv[cipher.key_len, cipher.iv_len]
|
148
|
+
cipher.update(encrypted_txt) + cipher.final
|
149
|
+
end
|
150
|
+
|
151
|
+
def passphrase
|
152
|
+
@passphrase ||= ask("Wallet passphrase? ") { |q| q.echo = false }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: itsy-btc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Ruten
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bitcoin-ruby
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ffi
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: highline
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: itsy-btc is a simple secure bitcoin wallet that allows you to turn an
|
70
|
+
offline computer into a hardware wallet.
|
71
|
+
email: jeremy.ruten@gmail.com
|
72
|
+
executables:
|
73
|
+
- itsy
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- Gemfile
|
78
|
+
- Gemfile.lock
|
79
|
+
- LICENSE
|
80
|
+
- itsy-btc.gemspec
|
81
|
+
- README.md
|
82
|
+
- lib/itsy-btc/commands/balance_command.rb
|
83
|
+
- lib/itsy-btc/commands/command.rb
|
84
|
+
- lib/itsy-btc/commands/comment_command.rb
|
85
|
+
- lib/itsy-btc/commands/decrypt_command.rb
|
86
|
+
- lib/itsy-btc/commands/delete_command.rb
|
87
|
+
- lib/itsy-btc/commands/gen_command.rb
|
88
|
+
- lib/itsy-btc/commands/help_command.rb
|
89
|
+
- lib/itsy-btc/commands/import_command.rb
|
90
|
+
- lib/itsy-btc/commands/init_command.rb
|
91
|
+
- lib/itsy-btc/commands/list_command.rb
|
92
|
+
- lib/itsy-btc/commands/passwd_command.rb
|
93
|
+
- lib/itsy-btc/commands/push_command.rb
|
94
|
+
- lib/itsy-btc/commands/sign_command.rb
|
95
|
+
- lib/itsy-btc/commands/tx_command.rb
|
96
|
+
- lib/itsy-btc/commands/verify_command.rb
|
97
|
+
- lib/itsy-btc/commands.rb
|
98
|
+
- lib/itsy-btc/lookup.rb
|
99
|
+
- lib/itsy-btc/wallet.rb
|
100
|
+
- lib/itsy-btc/wallet_entry.rb
|
101
|
+
- lib/itsy-btc.rb
|
102
|
+
- bin/itsy
|
103
|
+
homepage: http://github.com/yjerem/itsy-btc
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: 1.9.2
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.0.3
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: A simple secure bitcoin wallet.
|
127
|
+
test_files: []
|