#!/usr/bin/env ruby $:.unshift( File.expand_path("../../lib", __FILE__) ) require 'bitcoin' require 'eventmachine' require 'optparse' require 'yaml' defaults = { :network => "testnet", :storage => nil, :keystore => nil, :command => "127.0.0.1:9999" } options = Bitcoin::Config.load(defaults, :wallet) optparse = OptionParser.new do |opts| opts.banner = "Usage: bitcoin_wallet [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::]", "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 [] - display balance for given addr or whole wallet\n" + " list - list transaction history for address\n" + " send :[,:...] [] - send transaction\n" + " new - generate new key and add to keystore\n" + " import - import key in base58 format\n" + " export - export key to base58 format\n" + " name_list - list names in the wallet\n" + " name_show - display name information\n" + " name_history - display name history\n" + " name_new - reserve a name\n" + " name_firstupdate - register a name\n" + " name_update [] - update/transfer a name\n" end optparse.parse! cmd = ARGV.shift; cmdopts = ARGV unless cmd exit puts optparse end Bitcoin.network = options[:network] options[:keystore] ||= "simple::file=~/.bitcoin-ruby/#{Bitcoin.network_name}/keys.json" 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) options[:storage] ||= "sequel::sqlite://~/.bitcoin-ruby/#{Bitcoin.network_name}/blocks.db" 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 def send_transaction(storage, options, tx, ask = true) # puts tx.to_json if ask 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) print "#{str_val txout.value} " if script.is_pubkey? puts "#{script.get_pubkey} (pubkey)" elsif script.is_hash160? puts "#{script.get_address} (address)" elsif script.is_multisig? puts "#{script.get_addresses.join(' ')} (multisig)" elsif script.is_namecoin? puts "#{script.get_address} (#{script.type})" print " " * 16 if script.is_name_new? puts "Name Hash: #{script.get_namecoin_hash}" else puts "#{script.get_namecoin_name}: #{script.get_namecoin_value}" end else puts "#{str_val txout.value} (unknown type)" 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 end EM.run do Bitcoin::Network::CommandClient.connect(*options[:command].split(":")) do on_connected do request(:relay_tx, tx.to_payload.hth) end on_relay_tx do |res| if res["success"] puts "Transaction #{tx.hash} relayed to approx. #{"%.2f" % res['propagation']['percent']}% of the network." else puts "Error relaying tx: #{res['error']}" end EM.stop end end end 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.import_key(cmdopts[0]) puts "Key for #{addr} imported." else puts "Keystore doesn't support importing." end when "rescan" wallet.rescan 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| if Bitcoin.namecoin? && out.type.to_s =~ /^name_/ script = out.script puts " -> #{script.get_namecoin_name || script.get_namecoin_hash} (#{out.type})" else puts " -> #{out.get_addresses.join(', ') rescue 'unknown'}" end 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 "name_list" names = wallet.get_txouts.select {|o| [:name_firstupdate, :name_update].include?(o.type)} .map(&:get_namecoin_name).group_by(&:name).map {|n, l| l.sort_by(&:expires_in).last }.map {|name| { name: name.name, value: name.value, address: name.get_address, expires_in: name.expires_in } } puts JSON.pretty_generate(names) when "name_show" name = storage.name_show(cmdopts[0]) puts name.to_json when "name_history" names = storage.name_history(cmdopts[0]) puts JSON.pretty_generate(names) when "name_new" name = cmdopts[0] address = wallet.keystore.keys.sample[:key].addr @rand = nil def self.set_rand rand @rand = rand end tx = wallet.new_tx([[:name_new, self, name, address, 1000000]]) (puts "Error creating tx."; exit) unless tx send_transaction(storage, options, tx, true) puts JSON.pretty_generate([tx.hash, @rand]) when "name_firstupdate" name, rand, value = *cmdopts address = wallet.keystore.keys.sample[:key].addr tx = wallet.new_tx([[:name_firstupdate, name, rand, value, address, 1000000]]) (puts "Error creating tx."; exit) unless tx send_transaction(storage, options, tx, true) puts tx.hash when "name_update" name, value, address = *cmdopts address ||= wallet.keystore.keys.sample[:key].addr tx = wallet.new_tx([[:name_update, name, value, address, 1000000]]) (puts "Error creating tx."; exit) unless tx send_transaction(storage, options, tx, true) puts tx.hash 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 (puts "Error creating tx."; exit) unless tx send_transaction(storage, options, tx) 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