Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 324 lines (276 sloc) 8.79 KB
#!/usr/bin/env ruby
$:.unshift( File.expand_path("../../lib", __FILE__) )
require 'bitcoin'
require 'eventmachine'
require 'optparse'
require 'yaml'
defaults = {
:network => "testnet",
:storage => "dummy",
:keystore => "simple::file=#{ENV['HOME']}/.bitcoin-ruby/keys.json",
:command => "127.0.0.1:9999"
}
options = Bitcoin::Config.load(defaults, :wallet)
optparse = OptionParser.new do |opts|
opts.banner =
"Usage: bitcoin_wallet [options] <command> [<command options>]\n"
opts.separator("\nAvailable options:\n")
opts.on("-c", "--config FILE",
"Config file (default: #{Bitcoin::Config::CONFIG_PATHS})") do |file|
options = Bitcoin::Config.load_file(options, file, :wallet)
end
opts.on("-n", "--network NETWORK",
"User Network (default: #{options[:network]})") do |network|
options[:network] = network
end
opts.on("-s", "--storage BACKEND::CONFIG",
"Use storage backend (default: #{options[:storage]})") do |storage|
options[:storage] = storage
end
opts.on("--command [HOST:PORT]",
"Node command socket (default: #{options[:command]})") do |command|
options[:command] = command
end
opts.on("-k", "--keystore [backend::<config>]",
"Key store (default: #{options[:store]})") do |store|
options[:keystore] = store.gsub("~", ENV['HOME'])
end
opts.on("-h", "--help", "Display this help") do
puts opts; exit
end
opts.separator "\nAvailable commands:\n" +
" balance [<addr>] - display balance for given addr or whole wallet\n" +
" list <addr> - list transaction history for address\n" +
" send <addr>:<amount>[,<addr>:<amount>...] [<fee>] - send transaction\n" +
" new - generate new key and add to keystore\n" +
" import <base58> - import key in base58 format\n" +
" export <addr> - export key to base58 format\n"
end
optparse.parse!
cmd = ARGV.shift; cmdopts = ARGV
unless cmd
exit puts optparse
end
Bitcoin.network = options[:network]
backend, config = options[:keystore].split("::")
config = Hash[config.split(",").map{|c| c.split("=")}]
keystore = Bitcoin::Wallet.const_get("#{backend.capitalize}KeyStore").new(config)
if backend == "deterministic" && !config["nonce"]
puts "nonce: #{keystore.generator.nonce}"
end
#puts *keystore.get_keys.map(&:addr)
backend, config = options[:storage].split("::")
storage = Bitcoin::Storage.send(backend, :db => config)
wallet = Bitcoin::Wallet::Wallet.new(storage, keystore, Bitcoin::Wallet::SimpleCoinSelector)
def str_val(val, pre='')
("#{pre}%.8f" % (val / 1e8)).rjust(15)
end
def val_str(str)
(str.to_f * 1e8).to_i
end
case cmd
when "balance"
if cmdopts && cmdopts.size == 1
addr = cmdopts[0]
balance = storage.get_balance(Bitcoin.hash160_from_address(addr))
puts "#{addr} balance: #{str_val balance}"
else
puts "Total balance: #{str_val wallet.get_balance}"
end
when "new"
puts "Generated new key with address: #{wallet.get_new_addr}"
when "add"
key = {:label => ARGV[2]}
case ARGV[0]
when "pub"
k = Bitcoin::Key.new(nil, ARGV[1])
key[:key] = k
key[:addr] = k.addr
when "priv"
k = Bitcoin::Key.new(ARGV[1], nil)
k.regenerate_pubkey
key[:key] = k
key[:addr] = k.addr
when "addr"
key[:addr] = ARGV[1]
else
raise "unknown type #{ARGV[0]}"
end
wallet.add_key key
when "label"
wallet.label(ARGV[0], ARGV[1])
when "flag"
wallet.flag(ARGV[0], *ARGV[1].split("="))
when "key"
key = wallet.keystore.key(ARGV[0])
puts "Label: #{key[:label]}"
puts "Address: #{key[:addr]}"
puts "Pubkey: #{key[:key].pub}"
puts "Privkey: #{key[:key].priv}" if ARGV[1] == '-p'
puts "Mine: #{key[:mine]}"
when "import"
if wallet.keystore.respond_to?(:import)
addr = wallet.keystore.import(cmdopts[0])
puts "Key for #{addr} imported."
else
puts "Keystore doesn't support importing."
end
when "export"
base58 = wallet.keystore.export(cmdopts[0])
puts "Base58 encoded private key for #{cmdopts[0]}:"
puts base58
when "list"
if cmdopts && cmdopts.size == 1
depth = storage.get_depth
total = 0
key = wallet.keystore.key(cmdopts[0])
storage.get_txouts_for_address(key[:addr]).each do |txout|
total += txout.value
tx = txout.get_tx
blocks = depth - tx.get_block.depth rescue 0
puts "#{tx.hash} | #{str_val txout.value, '+ '} | " +
"#{str_val total} | #{blocks}"
tx.in.map(&:get_prev_out).each do |prev_out|
if prev_out
puts " <- #{prev_out.get_address}"
else
puts " <- generation"
end
end
puts
if txin = txout.get_next_in
tx = txin.get_tx
total -= txout.value
blocks = depth - tx.get_block.depth rescue 0
puts "#{tx.hash} | #{str_val txout.value, '- '} | " +
"#{str_val total} | #{blocks}"
txin.get_tx.out.each do |out|
puts " -> #{out.get_addresses.join(', ')}"
end
puts
end
end
puts "Total balance: #{str_val total}"
else
puts "Wallet addresses:"
total = 0
wallet.list.each do |key, balance|
total += balance
icon = key[:key] && key[:key].priv ? "P" : (key[:mine] ? "M" : " ")
puts " #{icon} #{key[:label].to_s.ljust(10)} (#{key[:addr].to_s.ljust(34)}) - #{("%.8f" % (balance / 1e8)).rjust(15)}"
end
puts "Total balance: #{str_val wallet.get_balance}"
end
when "send"
to = cmdopts[0].split(',').map do |pair|
type, *addrs, value = pair.split(":")
value = val_str(value)
[type.to_sym, *addrs, value]
end
fee = val_str(cmdopts[1]) || 0
value = val_str value
unless wallet.get_balance >= (to.map{|t|t[-1]}.inject{|a,b|a+=b;a} + fee)
puts "Insufficient funds."; exit
end
tx = wallet.new_tx(to, fee)
if tx.is_a?(Bitcoin::Wallet::TxDP)
puts "Transaction needs to be signed by additional keys."
print "Filename to save TxDP: [./#{tx.id}.txdp] "
$stdout.flush
filename = $stdin.gets.strip
filename = "./#{tx.id}.txdp" if filename == ""
File.open(filename, "w") {|f| f.write(tx.serialize) }
exit
end
unless tx
puts "Error creating tx."; exit
end
total = 0
puts "Hash: #{tx.hash}"
puts "inputs:"
tx.in.each do |txin|
prev_out = storage.get_txout_for_txin(txin)
total += prev_out.value
puts " #{prev_out.get_address} - #{str_val prev_out.value}"
end
puts "outputs:"
tx.out.each do |txout|
total -= txout.value
script = Bitcoin::Script.new(txout.pk_script)
if script.is_pubkey?
puts "#{str_val txout.value} #{script.get_pubkey} (pubkey)"
elsif script.is_hash160?
puts "#{str_val txout.value} #{script.get_address} (address)"
elsif script.is_multisig?
puts "#{str_val txout.value} #{script.get_addresses.join(' ')} (multisig)"
end
end
puts "Fee: #{str_val total}"
$stdout.sync = true
print "Really send transaction? (y/N) " and $stdout.flush
unless $stdin.gets.chomp.downcase == 'y'
puts "Aborted."; exit
end
EM.run do
Bitcoin::Network::CommandClient.connect(*options[:command].split(":")) do
on_connected do
request(:relay_tx, tx)
end
on_relay_tx do
puts "Transaction #{tx.hash} relayed"
EM.stop
end
end
end
when "sign"
txt = File.read(ARGV[0])
txdp = Bitcoin::Wallet::TxDP.parse(txt)
puts txdp.tx[0].to_json
print "Really sign transaction? (y/N) " and $stdout.flush
unless $stdin.gets.chomp.downcase == 'y'
puts "Aborted."; exit
end
txdp.sign_inputs do |tx, prev_tx, i, addr|
key = keystore.key(addr)[:key] rescue nil
next nil unless key && !key.priv.nil?
sig_hash = tx.signature_hash_for_input(i, prev_tx)
sig = key.sign(sig_hash)
script_sig = Bitcoin::Script.to_pubkey_script_sig(sig, [key.pub].pack("H*"))
script_sig.unpack("H*")[0]
end
File.open(ARGV[0], "w") {|f| f.write txdp.serialize }
when "relay"
txt = File.read(ARGV[0])
txdp = Bitcoin::Wallet::TxDP.parse(txt)
tx = txdp.tx[0]
puts tx.to_json
txdp.inputs.each_with_index do |s, i|
value, sigs = *s
tx.in[i].script_sig = [sigs[0][1]].pack("H*")
end
tx.in.each_with_index do |txin, i|
p txdp.tx.map(&:hash)
prev_tx = storage.get_tx(txin.prev_out.reverse_hth)
raise "prev tx #{txin.prev_out.reverse_hth} not found" unless prev_tx
raise "signature error" unless tx.verify_input_signature(i, prev_tx)
end
$stdout.sync = true
print "Really send transaction? (y/N) " and $stdout.flush
unless $stdin.gets.chomp.downcase == 'y'
puts "Aborted."; exit
end
EM.run do
EM.connect(*options[:command].split(":")) do |conn|
conn.send_data(["relay_tx", tx.to_payload.unpack("H*")[0]].to_json)
def conn.receive_data(data)
(@buf ||= BufferedTokenizer.new("\x00")).extract(data).each do |packet|
res = JSON.load(packet)
puts "Transaction relayed: #{res[1]["hash"]}"
EM.stop
end
end
end
end
else
puts "Unknown command."
end