Browse Source

Added transaction creation/signing scripts

	modified:   MANIFEST
	new file:   mmgen-txcreate
	new file:   mmgen-txsign
	modified:   mmgen/__init__.py
	modified:   mmgen/bitcoin.py
	modified:   mmgen/config.py
	modified:   mmgen/license.py
	new file:   mmgen/tx.py
	modified:   mmgen/utils.py
	new file:   scripts/bitcoind-walletunlock.py
	modified:   setup.py
philemon 11 years ago
parent
commit
35ddbc029c
11 changed files with 708 additions and 74 deletions
  1. 3 0
      MANIFEST
  2. 106 0
      mmgen-txcreate
  3. 118 0
      mmgen-txsign
  4. 1 0
      mmgen/__init__.py
  5. 24 0
      mmgen/bitcoin.py
  6. 1 0
      mmgen/config.py
  7. 25 33
      mmgen/license.py
  8. 285 0
      mmgen/tx.py
  9. 79 39
      mmgen/utils.py
  10. 61 0
      scripts/bitcoind-walletunlock.py
  11. 5 2
      setup.py

+ 3 - 0
MANIFEST

@@ -2,6 +2,8 @@
 mmgen-addrgen
 mmgen-addrgen
 mmgen-keygen
 mmgen-keygen
 mmgen-passchg
 mmgen-passchg
+mmgen-txcreate
+mmgen-txsign
 mmgen-walletchk
 mmgen-walletchk
 mmgen-walletgen
 mmgen-walletgen
 setup.py
 setup.py
@@ -14,5 +16,6 @@ mmgen/license.py
 mmgen/mn_electrum.py
 mmgen/mn_electrum.py
 mmgen/mn_tirosh.py
 mmgen/mn_tirosh.py
 mmgen/mnemonic.py
 mmgen/mnemonic.py
+mmgen/tx.py
 mmgen/utils.py
 mmgen/utils.py
 mmgen/walletgen.py
 mmgen/walletgen.py

+ 106 - 0
mmgen-txcreate

@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C) 2013 by philemon <mmgen-py@yandex.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+mmgen-txcreate: Send BTC from specified outputs to specified addresses
+"""
+
+import sys
+#from hashlib import sha256
+
+from mmgen.Opts import *
+from mmgen.license import *
+from mmgen.config import *
+from mmgen.tx import *
+from mmgen.utils import check_opts, msg, user_confirm
+from decimal import Decimal
+
+prog_name = sys.argv[0].split("/")[-1]
+
+help_data = {
+	'prog_name': prog_name,
+	'desc':    "Send BTC from specified outputs to specified addresses",
+	'usage':   "[opts] <recipient address> <amount> <transaction fee> <change address>",
+	'options': """
+-h, --help                 Print this help message
+-d, --outdir            d  Specify an alternate directory 'd' for output
+-e, --echo-passphrase      Print passphrase to screen when typing it
+-q, --quiet                Suppress warnings; overwrite files without asking
+"""
+}
+
+short_opts = "hd:eq"
+long_opts  = "help","outdir=","echo_passphrase","quiet"
+
+opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
+
+# Exits on invalid input
+check_opts(opts, ('outdir',))
+
+if debug:
+	print "Processed options:     %s" % repr(opts)
+	print "Cmd args:              %s" % repr(cmd_args)
+
+if len(cmd_args) == 4:
+	rcpt_addr,send_amt,tx_fee,change_addr = cmd_args	
+	check_address(change_addr)
+elif len(cmd_args) == 3:
+	rcpt_addr,send_amt,tx_fee = cmd_args	
+	change_addr = ""
+else: usage(help_data)
+
+check_address(rcpt_addr)
+send_amt = check_btc_amt(send_amt)
+tx_fee   = check_btc_amt(tx_fee)
+
+# Begin execution
+c = connect_to_bitcoind()
+
+if not 'quiet' in opts: do_license_msg()
+
+unspent = sort_and_view(c.listunspent())
+
+total = remove_exponent(sum([i.amount for i in unspent]))
+
+msg("Total unspent:   %s BTC" % total)
+msg("Amount to spend: %s BTC" % send_amt)
+msg("%s unspent outputs total" % len(unspent))
+
+sel_unspent = select_outputs(unspent,"Choose the outputs to spend: ")
+
+total_in = remove_exponent(sum([o.amount for o in sel_unspent]))
+change   = remove_exponent(total_in - (send_amt + tx_fee))
+
+if change < 0:
+	msg(txmsg['not_enough_btc'] % change)
+	sys.exit(2)
+elif change > 0 and not change_addr:
+	msg(txmsg['throwaway_change'] % (change, total_in-tx_fee))
+	sys.exit(2)
+
+tx_in = [{"txid":i.txid, "vout":i.vout} for i in sel_unspent]
+tx_out = {rcpt_addr:float(send_amt), change_addr:float(change)}
+tx_hex = c.createrawtransaction(tx_in,tx_out)
+
+msg("Transaction successfully created\n")
+prompt = "View decoded transaction?"
+if user_confirm(prompt,default_yes=False):
+	view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex)
+
+prompt = "Save transaction?"
+if user_confirm(prompt,default_yes=True):
+	print_tx_to_file(tx_hex,sel_unspent,send_amt,opts)

+ 118 - 0
mmgen-txsign

@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C) 2013 by philemon <mmgen-py@yandex.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+mmgen-txsign: Sign a Bitcoin transaction generated by mmgen-txcreate
+"""
+
+import sys
+#from hashlib import sha256
+
+from mmgen.Opts import *
+from mmgen.license import *
+from mmgen.config import *
+from mmgen.tx import *
+from mmgen.utils import check_opts, msg, user_confirm, check_infile, get_lines_from_file, my_getpass, my_raw_input
+
+prog_name = sys.argv[0].split("/")[-1]
+
+help_data = {
+	'prog_name': prog_name,
+	'desc':    "Sign a Bitcoin transaction generated by mmgen-txcreate",
+	'usage':   "[opts] <transaction file>",
+	'options': """
+-h, --help                 Print this help message
+-d, --outdir            d  Specify an alternate directory 'd' for output
+-e, --echo-passphrase      Print passphrase to screen when typing it
+-i, --info                 Just show info about the transaction and exit
+-q, --quiet                Suppress warnings; overwrite files without asking
+"""
+}
+
+short_opts = "hd:eiq"
+long_opts  = "help","outdir=","echo_passphrase","info","quiet"
+
+opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
+
+# Exits on invalid input
+check_opts(opts, ('outdir',))
+
+if debug:
+	print "Processed options:     %s" % repr(opts)
+	print "Cmd args:              %s" % repr(cmd_args)
+
+if len(cmd_args) == 1:
+	infile = cmd_args[0]
+	check_infile(infile)
+else: usage(help_data)
+
+# Begin execution
+
+c = connect_to_bitcoind()
+
+tx_data = get_lines_from_file(infile,"transaction data")
+
+timestamp,tx_hex,sig_data,inputs_data = parse_tx_data(tx_data)
+
+if 'info' in opts:
+	view_tx_data(c,inputs_data,tx_hex,timestamp)
+	sys.exit(0)
+
+if not 'quiet' in opts and not 'info' in opts: do_license_msg()
+
+msg("Successfully opened transaction file '%s'" % infile)
+
+if user_confirm("View transaction data? ",default_yes=False):
+	view_tx_data(c,inputs_data,tx_hex,timestamp)
+
+prompt = "Enter bitcoind passphrase: "
+if 'echo_passphrase' in opts:
+	password = my_raw_input(prompt)
+else:
+	password = my_getpass(prompt)
+
+wallet_enc = True
+from bitcoinrpc import exceptions
+
+try:
+	c.walletpassphrase(password, 9999)
+except exceptions.WalletWrongEncState:
+	msg("Wallet is unencrypted")
+	wallet_enc = False
+except exceptions.WalletPassphraseIncorrect:
+	msg("Passphrase incorrect")
+	sys.exit(3)
+except exceptions.WalletAlreadyUnlocked:
+	msg("WARNING: Wallet already unlocked!")
+
+# signrawtransaction <hex string> [{"txid":txid,"vout":n,"scriptPubKey":hex,"redeemScript":hex},...] [<privatekey1>,...] [sighashtype="ALL"]
+try:
+	sig_tx = c.signrawtransaction(tx_hex,sig_data)
+except:
+	msg("Failed to sign transaction")
+	if wallet_enc: c.walletlock()
+	sys.exit(3)
+finally:
+	if wallet_enc: c.walletlock()
+
+if not sig_tx['complete']:
+	msg("signrawtransaction() returned failure")
+ 	sys.exit(3)
+
+prompt = "Save signed transaction?"
+if user_confirm(prompt,default_yes=True):
+	print_signed_tx_to_file(tx_hex,sig_tx['hex'],opts)

+ 1 - 0
mmgen/__init__.py

@@ -28,6 +28,7 @@ __all__ = [
 	'mnemonic.py',
 	'mnemonic.py',
 	'mn_tirosh.py',
 	'mn_tirosh.py',
 	'Opts.py',
 	'Opts.py',
+	'tx.py',
 	'utils.py',
 	'utils.py',
 	'walletgen.py'
 	'walletgen.py'
 ]
 ]

+ 24 - 0
mmgen/bitcoin.py

@@ -66,6 +66,30 @@ def privnum2addr(numpriv):
 	pubkey = hexlify(pko.get_verifying_key().to_string())
 	pubkey = hexlify(pko.get_verifying_key().to_string())
 	return _pubhex2addr('04'+pubkey)
 	return _pubhex2addr('04'+pubkey)
 
 
+def verify_addr(addr):
+
+	if addr[0] != "1":
+		print "%s Invalid address" % addr
+		return False
+
+	addr,lz = addr[1:],0
+	while addr[0] == "1": addr = addr[1:]; lz += 1
+		
+	addr_hex = lz * "00" + hex(_b58tonum(addr))[2:].rstrip("L")
+
+	if len(addr_hex) != 48:
+		print "%s Invalid address" % addr
+		return False
+
+	step1 = sha256(unhexlify('00'+addr_hex[:40])).digest()
+	step2 = sha256(step1).hexdigest()
+
+	if step2[:8] != addr_hex[40:]:
+		print "Invalid checksum in address %s" % addr
+		return False
+
+	return True
+
 # Reworked code from here:
 # Reworked code from here:
 
 
 def _numtob58(num):
 def _numtob58(num):

+ 1 - 0
mmgen/config.py

@@ -31,6 +31,7 @@ mnemonic_lens = [i / 32 * 3 for i in seed_lens]
 from os import getenv
 from os import getenv
 debug = True if getenv("MMGEN_DEBUG") else False
 debug = True if getenv("MMGEN_DEBUG") else False
 
 
+mins_per_block = 9
 passwd_max_tries = 5
 passwd_max_tries = 5
 max_randlen,min_randlen = 80,5
 max_randlen,min_randlen = 80,5
 usr_randlen = 20
 usr_randlen = 20

+ 25 - 33
mmgen/license.py

@@ -21,18 +21,16 @@ license.py:  Show the license
 
 
 import sys
 import sys
 from mmgen.config import proj_name
 from mmgen.config import proj_name
-from mmgen.utils import msg, msg_r
+from mmgen.utils import msg, msg_r, get_char
 
 
 gpl = {
 gpl = {
 	'warning': """
 	'warning': """
-{} Copyright (C) 2013 by philemon <mmgen-py@yandex.com>
-This program comes with ABSOLUTELY NO WARRANTY.
-This is free software, and you are welcome to
-redistribute it under certain conditions.
+{} Copyright (C) 2013 by Philemon <mmgen-py@yandex.com>.  This program
+comes with ABSOLUTELY NO WARRANTY.  This is free software, and you are
+welcome to redistribute it under certain conditions.
 """.format(proj_name),
 """.format(proj_name),
 	'prompt': """
 	'prompt': """
-Press 'c' for conditions, 'w' for warranty info,
-or ENTER to continue:
+Press 'c' for conditions, 'w' for warranty info, or ENTER to continue:
 """,
 """,
 	'conditions': """
 	'conditions': """
                        TERMS AND CONDITIONS
                        TERMS AND CONDITIONS
@@ -620,30 +618,24 @@ copy of the Program in return for a fee.
 """
 """
 }
 }
 
 
+def do_pager(text):
+	import os
+	pager = os.environ['PAGER'] if 'PAGER' in os.environ else 'more'
+
+	p = os.popen(pager, 'w')
+	p.write(text)
+	p.close()
+	msg_r("\r")
+
+
 def do_license_msg():
 def do_license_msg():
-	ls = "\n  "
-	msg("  " + ls.join(gpl['warning'].strip().split("\n")))
-
-	try:
-		import os
-		os.system(
-		"stty -icanon min 1 time 0 -echo -echoe -echok -echonl -crterase noflsh"
-		)
-		pager = os.environ['PAGER'] if 'PAGER' in os.environ else 'more'
-
-		while True:
-			msg_r(ls + ls.join(gpl['prompt'].strip().split("\n")) + " ")
-			reply = sys.stdin.read(1)
-			if   reply == 'c':
-				m = gpl['conditions']
-			elif reply == 'w':
-				m = gpl['warranty']
-			else: break
-			p = os.popen(pager, 'w')
-			p.write(m)
-			p.close()
-	except:
-		msg("\nInterrupted by user")
-		sys.exit(1)
-	finally:
-		os.system("stty sane")
+	msg("%s\n" % gpl['warning'].strip())
+
+	while True:
+
+		prompt = "%s " % gpl['prompt'].strip()
+		reply = get_char(prompt)
+
+		if   reply == 'c': do_pager(gpl['conditions'])
+		elif reply == 'w': do_pager(gpl['warranty'])
+		else: msg("\n"); break

+ 285 - 0
mmgen/tx.py

@@ -0,0 +1,285 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C) 2013 by philemon <mmgen-py@yandex.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+tx.py:  Bitcoin transaction routines
+"""
+
+from binascii import unhexlify
+from mmgen.utils import msg,msg_r,write_to_file,my_raw_input,get_char,make_chksum_8,make_timestamp
+import sys, os
+from bitcoinrpc.connection import *
+from decimal import Decimal
+from mmgen.config import *
+
+txmsg = {
+'not_enough_btc': "Not enough BTC in the inputs for this transaction (%s BTC)",
+'throwaway_change': """
+ERROR: This transaction produces change (%s BTC); however, no change
+address was specified.  Total inputs - transaction fee = %s BTC.
+To create a valid transaction with no change address, send this sum to the
+specified recipient address.
+""".strip()
+}
+
+def connect_to_bitcoind():
+
+	host,port,user,passwd = "localhost",8332,"rpcuser","rpcpassword"
+	cfg = get_cfg_options((user,passwd))
+
+	try:
+		c = BitcoinConnection(cfg[user],cfg[passwd],host,port)
+	except:
+		msg("Unable to establish RPC connection with bitcoind")
+		sys.exit(2)
+
+	return c
+
+
+def remove_exponent(d):
+    '''Remove exponent and trailing zeros.
+    '''
+    return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
+
+def	check_address(rcpt_address):
+	from mmgen.bitcoin import verify_addr
+	if not verify_addr(rcpt_address):
+		sys.exit(3)
+
+def check_btc_amt(send_amt):
+
+	from decimal import Decimal
+	try:
+		retval = Decimal(send_amt)
+	except:
+		msg("%s: Invalid amount" % send_amt)
+		sys.exit(3)
+	
+	if retval.as_tuple()[-1] < -8:
+		msg("%s: Too many decimal places in amount" % send_amt)
+		sys.exit(3)
+	
+	return remove_exponent(retval)
+
+
+def get_cfg_options(cfg_keys):
+
+	cfg_file = "%s/%s" % (os.environ["HOME"], ".bitcoin/bitcoin.conf")
+	try:
+		f = open(cfg_file)
+	except:
+		msg("Unable to open file '%s' for reading" % cfg_file)
+		sys.exit(2)
+
+	cfg = {}
+
+	for line in f.readlines():
+		s = line.translate(None,"\n\t ").split("=")
+		for k in cfg_keys:
+			if s[0] == k: cfg[k] = s[1]
+
+	f.close()
+
+	for k in cfg_keys:
+		if not k in cfg:
+			msg("Configuration option '%s' must be set in %s" % (k,cfg_file))
+			sys.exit(2)
+
+	return cfg
+
+
+def print_tx_to_file(tx,sel_unspent,send_amt,opts):
+	sig_data = [{"txid":i.txid,"vout":i.vout,"scriptPubKey":i.scriptPubKey}
+					for i in sel_unspent]
+	tx_id = make_chksum_8(unhexlify(tx))
+	outfile = "%s[%s].tx" % (tx_id,send_amt)
+	if 'outdir' in opts:
+		outfile = "%s/%s" % (opts['outdir'], outfile)
+	input_data = sel_unspent
+	data = "%s\n%s\n%s\n%s\n" % (
+			make_timestamp(), tx, repr(sig_data),
+			repr([i.__dict__ for i in sel_unspent])
+		)
+	write_to_file(outfile,data,confirm=False)
+	msg("Transaction data saved to file '%s'" % outfile)
+
+
+def print_signed_tx_to_file(tx,sig_tx,opts):
+	tx_id = make_chksum_8(unhexlify(tx))
+	outfile = "%s.txsig" % tx_id
+	if 'outdir' in opts:
+		outfile = "%s/%s" % (opts['outdir'], outfile)
+	write_to_file(outfile,sig_tx+"\n",confirm=False)
+	msg("Signed transaction saved to file '%s'" % outfile)
+
+
+def print_sent_tx_to_file(tx):
+	outfile = "tx.out"
+	write_to_file(outfile,tx+"\n",confirm=False)
+	msg("Transaction ID saved to file '%s'" % outfile)
+
+def sort_and_view(unspent):
+
+	def s_amt(a,b):  return cmp(a.amount,b.amount)
+	def s_txid(a,b):
+		return cmp("%s %03s" % (a.txid,a.vout), "%s %03s" % (b.txid,b.vout))
+	def s_addr(a,b): return cmp(a.address,b.address)
+	def s_age(a,b):  return cmp(b.confirmations,a.confirmations)
+
+	fs = "%-4s %-11s %-2s %-34s %13s"
+	sort,group,reverse = "",False,False
+
+	from copy import deepcopy
+	while True:
+		out = deepcopy(unspent)
+		for i in out: i.skip = ""
+		for n in range(len(out)):
+			if group and n < len(out)-1:
+				a,b = out[n],out[n+1]
+				if sort == "address" and a.address == b.address:
+					out[n+1].skip = "d"
+				elif sort == "txid" and a.txid == b.txid:
+					out[n+1].skip = "t"
+
+		output = []
+		output.append("Sort order: %s%s%s" % (
+				"reverse " if reverse else "",
+				sort if sort else "None",
+	" (grouped)" if group and (sort == "address" or sort == "txid") else ""
+			))
+		output.append(fs % ("Num","TX id","Vout","Address","Amount       "))
+
+		for n,i in enumerate(out):
+			amt = str(remove_exponent(i.amount))
+			fill = 8 - len(amt.split(".")[-1]) if "." in amt else 9
+			addr = " |" + "-"*32 if i.skip == "d" else i.address
+			txid = "       |---" if i.skip == "t" else i.txid[:8]+"..."
+
+			output.append(fs % (str(n+1)+")", txid,i.vout,addr,amt+(" "*fill)))
+
+		while True:
+			reply = get_char("\n".join(output) +
+"""\n
+Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [g]roup
+(Type 'q' to quit sorting): """).strip()
+			if   reply == 'a': unspent.sort(s_amt);  sort = "amount"; break
+			elif reply == 't': unspent.sort(s_txid); sort = "txid"; break
+			elif reply == 'd': unspent.sort(s_addr); sort = "address"; break
+			elif reply == 'A': unspent.sort(s_age);  sort = "age"; break
+			elif reply == 'r': 
+				reverse = False if reverse else True
+				unspent.reverse()
+				break
+			elif reply == 'g': group = False if group else True; break
+			elif reply == 'q': break
+			else: msg("Invalid input")
+
+		msg("\n")
+		if reply == 'q': break
+
+	return unspent
+
+
+def view_tx_data(c,inputs_data,tx_hex,timestamp=""):
+
+	td = c.decoderawtransaction(tx_hex)
+
+	msg("TRANSACTION DATA:\n")
+
+	if timestamp: msg("Timestamp: %s\n" % timestamp)
+
+	msg("Inputs:")
+	total_in = 0
+	for n,i in enumerate(td['vin']):
+		for j in inputs_data:
+			if j['txid'] == i['txid'] and j['vout'] == i['vout']:
+				days = j['confirmations'] * mins_per_block / (60*24)
+				total_in += j['amount']
+				msg("""
+%-3s tx,vout: %s,%s
+    address:        %s
+    amount:         %s BTC
+    confirmations:  %s (around %s days)
+""".strip() %
+	(n+1,i['txid'],i['vout'],j['address'],
+	 remove_exponent(j['amount']),j['confirmations'],days)+"\n")
+				break
+
+	msg("Total input: %s BTC\n" % remove_exponent(total_in))
+
+	total_out = 0
+	msg("Outputs:")
+	for n,i in enumerate(td['vout']):
+		total_out += i['value']
+ 		msg("""
+%-3s address: %s
+    amount:  %s BTC
+""".strip() % (
+		n,
+		i['scriptPubKey']['addresses'][0],
+		remove_exponent(i['value']))
+	+ "\n")
+	msg("Total output: %s BTC" % remove_exponent(total_out))
+	msg("TX fee:       %s BTC\n" % remove_exponent(total_in-total_out))
+
+
+def parse_tx_data(tx_data):
+
+	if len(tx_data) != 4:
+		msg("'%s': not a transaction file" % infile)
+		sys.exit(2)
+
+	try: unhexlify(tx_data[1])
+	except:
+		msg("Transaction data is invalid")
+		sys.exit(2)
+
+	try:
+		sig_data = eval(tx_data[2])
+	except:
+		msg("Signature data is invalid")
+		sys.exit(2)
+
+	try:
+		inputs_data = eval(tx_data[3])
+	except:
+		msg("Inputs data is invalid")
+		sys.exit(2)
+
+	return tx_data[0],tx_data[1],sig_data,inputs_data
+
+
+def select_outputs(unspent,prompt):
+
+	while True:
+		reply = my_raw_input(prompt).strip()
+		if reply:
+			selected = ()
+			try:
+				selected = [int(i) - 1 for i in reply.split()]
+			except: pass
+			else:
+				for i in selected:
+					if i < 0 or i >= len(unspent):
+						msg(
+		"Input must be a number or numbers between 1 and %s" % len(unspent))
+						break
+				else: break
+
+		msg("'%s': Invalid input" % reply)
+
+	return [unspent[i] for i in selected]

+ 79 - 39
mmgen/utils.py

@@ -28,27 +28,44 @@ def msg_r(s): sys.stderr.write(s)
 
 
 def bail(): sys.exit(9)
 def bail(): sys.exit(9)
 
 
-def _my_getpass(prompt):
+def my_getpass(prompt):
 
 
 	from getpass import getpass
 	from getpass import getpass
 	# getpass prompts to stderr, so no trickery required as with raw_input()
 	# getpass prompts to stderr, so no trickery required as with raw_input()
 	try: pw = getpass(prompt)
 	try: pw = getpass(prompt)
 	except:
 	except:
-		msg("\nInterrupted by user")
+		msg("\nUser interrupt")
 		sys.exit(1)
 		sys.exit(1)
 
 
 	return pw
 	return pw
 
 
+def get_char(prompt):
 
 
-def _my_raw_input(prompt):
+	import os
+	msg_r(prompt)
+	os.system(
+"stty -icanon min 1 time 0 -echo -echoe -echok -echonl -crterase noflsh"
+	)
+	try: ch = sys.stdin.read(1)
+	except:
+		os.system("stty sane")
+		msg("\nUser interrupt")
+		sys.exit(1)
+	else:
+		os.system("stty sane")
+
+	return ch
+
+
+def my_raw_input(prompt):
 
 
 	msg_r(prompt)
 	msg_r(prompt)
-	try: pw = raw_input()
+	try: reply = raw_input()
 	except:
 	except:
-		msg("\nInterrupted by user")
+		msg("\nUser interrupt")
 		sys.exit(1)
 		sys.exit(1)
 
 
-	return pw
+	return reply
 
 
 
 
 def _get_hash_params(hash_preset):
 def _get_hash_params(hash_preset):
@@ -175,7 +192,8 @@ future, you must continue using these same parameters
 
 
 def confirm_or_exit(message, question):
 def confirm_or_exit(message, question):
 
 
-	if message.strip(): msg(message.strip())
+	m = message.strip()
+	if m: msg(m)
 	msg("")
 	msg("")
 
 
 	conf_msg = "Type uppercase 'YES' to confirm: "
 	conf_msg = "Type uppercase 'YES' to confirm: "
@@ -185,13 +203,28 @@ def confirm_or_exit(message, question):
 	else:
 	else:
 		prompt = "Are you sure you want to %s?\n%s" % (question,conf_msg)
 		prompt = "Are you sure you want to %s?\n%s" % (question,conf_msg)
 
 
-	if _my_raw_input(prompt).strip() != "YES":
+	if my_raw_input(prompt).strip() != "YES":
 		msg("Program aborted by user")
 		msg("Program aborted by user")
 		sys.exit(2)
 		sys.exit(2)
 
 
 	msg("")
 	msg("")
 
 
 
 
+def user_confirm(prompt,default_yes=False):
+
+	q = "(Y/n)" if default_yes else "(y/N)"
+
+	while True:
+		reply = get_char("%s %s: " % (prompt, q)).strip()
+		msg("")
+
+		if not reply:
+			return True if default_yes else False
+		elif reply in 'yY': return True
+		elif reply in 'nN': return False
+		else: msg("Invalid reply")
+
+
 def set_if_unset_and_typeconvert(opts,item):
 def set_if_unset_and_typeconvert(opts,item):
 
 
 	for opt,var,dtype in item:
 	for opt,var,dtype in item:
@@ -282,10 +315,10 @@ just hit ENTER twice.
 
 
 	for i in range(passwd_max_tries):
 	for i in range(passwd_max_tries):
 		if 'echo_passphrase' in opts:
 		if 'echo_passphrase' in opts:
-			return _my_raw_input("Enter %s: " % what)
+			return my_raw_input("Enter %s: " % what)
 		else:
 		else:
-			ret		= _my_getpass("Enter %s: " % what)
-			if ret == _my_getpass("Repeat %s: " % what):
+			ret		= my_getpass("Enter %s: " % what)
+			if ret == my_getpass("Repeat %s: " % what):
 				s = " (empty)" if not len(ret) else ""
 				s = " (empty)" if not len(ret) else ""
 				msg("%ss match%s" % (what.capitalize(),s))
 				msg("%ss match%s" % (what.capitalize(),s))
 				return ret
 				return ret
@@ -359,6 +392,16 @@ def get_default_wordlist():
 	return wl.strip().split("\n")
 	return wl.strip().split("\n")
 
 
 
 
+def open_file_or_exit(filename,mode):
+	try:
+		f = open(filename, mode)
+	except:
+		what = "reading" if mode == 'r' else "writing"
+		msg("Unable to open file '%s' for %s" % (infile,what))
+		sys.exit(2)
+	return f
+
+
 def write_to_file(outfile,data,confirm=False):
 def write_to_file(outfile,data,confirm=False):
 
 
 	if confirm:
 	if confirm:
@@ -370,11 +413,7 @@ def write_to_file(outfile,data,confirm=False):
 		else:
 		else:
 			confirm_or_exit("","File '%s' already exists\nOverwrite?" % outfile)
 			confirm_or_exit("","File '%s' already exists\nOverwrite?" % outfile)
 
 
-	try:
-		f = open(outfile,'w')
-	except:
-		msg("Failed to open file '%s' for writing" % outfile)
-		sys.exit(2)
+	f = open_file_or_exit(outfile,'w')
 
 
 	try:
 	try:
 		f.write(data)
 		f.write(data)
@@ -446,12 +485,12 @@ def col4(s):
 	nondiv = 1 if len(s) % 4 else 0
 	nondiv = 1 if len(s) % 4 else 0
 	return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)])
 	return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)])
 
 
-
-def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
-
+def make_timestamp():
 	import time
 	import time
 	tv = time.gmtime(time.time())[:6]
 	tv = time.gmtime(time.time())[:6]
-	ts_hdr = "{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}".format(*tv)
+	return "{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}".format(*tv)
+
+def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
 
 
 	seed_id = make_chksum_8(seed)
 	seed_id = make_chksum_8(seed)
 	seed_len = str(len(seed)*8)
 	seed_len = str(len(seed)*8)
@@ -470,7 +509,8 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
 	sf  = b58encode_pad(salt)
 	sf  = b58encode_pad(salt)
 	esf = b58encode_pad(enc_seed)
 	esf = b58encode_pad(enc_seed)
 
 
-	metadata = seed_id.lower(),key_id.lower(),seed_len,pw_status,ts_hdr
+	metadata = seed_id.lower(),key_id.lower(),\
+			   seed_len,pw_status,make_timestamp()
 
 
 	lines = (
 	lines = (
 		label,
 		label,
@@ -560,11 +600,7 @@ def get_data_from_wallet(infile,opts):
 
 
 	msg("Getting {} wallet data from file: {}".format(proj_name,infile))
 	msg("Getting {} wallet data from file: {}".format(proj_name,infile))
 
 
-	try:
-		f = open(infile, 'r')
-	except:
-		msg("Unable to open file '" + infile + "' for reading")
-		sys.exit(2)
+	f = open_file_or_exit(infile, 'r')
 
 
 	lines = [i.strip() for i in f.readlines()]
 	lines = [i.strip() for i in f.readlines()]
 	f.close()
 	f.close()
@@ -602,23 +638,27 @@ def get_data_from_wallet(infile,opts):
 def _get_words_from_user(opts, prompt):
 def _get_words_from_user(opts, prompt):
 	# split() also strips
 	# split() also strips
 	if 'echo_passphrase' in opts:
 	if 'echo_passphrase' in opts:
-		return _my_raw_input(prompt).split()
+		return my_raw_input(prompt).split()
 	else:
 	else:
-		return _my_getpass(prompt).split()
+		return my_getpass(prompt).split()
 
 
 
 
 def _get_words_from_file(infile,what):
 def _get_words_from_file(infile,what):
-	msg("Getting %s data from file '%s'" % (what,infile))
-	try:
-		f = open(infile, 'r')
-	except:
-		msg("Unable to open file '%s' for reading" % infile)
-		sys.exit(2)
-	lines = f.readlines()
-	f.close()
+	msg("Getting %s from file '%s'" % (what,infile))
+	f = open_file_or_exit(infile, 'r')
+	lines = f.readlines(); f.close()
 	# split() also strips
 	# split() also strips
 	return [w for l in lines for w in l.split()]
 	return [w for l in lines for w in l.split()]
 
 
+
+def get_lines_from_file(infile,what):
+	msg("Getting %s from file '%s'" % (what,infile))
+	f = open_file_or_exit(infile,'r')
+	data = f.read(); f.close()
+	# split() doesn't strip final newline
+	return data.strip().split("\n")
+
+
 def get_words(infile,what,prompt,opts):
 def get_words(infile,what,prompt,opts):
 	if infile:
 	if infile:
 		words = _get_words_from_file(infile,what)
 		words = _get_words_from_file(infile,what)
@@ -709,7 +749,7 @@ def decrypt_seed(enc_seed, key, seed_id, key_id):
 def get_seed(infile,opts,no_wallet=False):
 def get_seed(infile,opts,no_wallet=False):
 	if 'from_mnemonic' in opts:
 	if 'from_mnemonic' in opts:
 		prompt = "Enter mnemonic: "
 		prompt = "Enter mnemonic: "
-		words = get_words(infile,"mnemonic",prompt,opts)
+		words = get_words(infile,"mnemonic data",prompt,opts)
 
 
 		wl = get_default_wordlist()
 		wl = get_default_wordlist()
 		from mmgen.mnemonic import get_seed_from_mnemonic
 		from mmgen.mnemonic import get_seed_from_mnemonic
@@ -723,11 +763,11 @@ def get_seed(infile,opts,no_wallet=False):
 					*_get_from_brain_opt_params(opts)),
 					*_get_from_brain_opt_params(opts)),
 			"continue")
 			"continue")
 		prompt = "Enter brainwallet passphrase: "
 		prompt = "Enter brainwallet passphrase: "
-		words = get_words(infile,"brainwallet",prompt,opts)
+		words = get_words(infile,"brainwallet data",prompt,opts)
 		return _get_seed_from_brain_passphrase(words,opts)
 		return _get_seed_from_brain_passphrase(words,opts)
 	elif 'from_seed' in opts:
 	elif 'from_seed' in opts:
 		prompt = "Enter seed in %s format: " % seed_ext
 		prompt = "Enter seed in %s format: " % seed_ext
-		words = get_words(infile,"seed",prompt,opts)
+		words = get_words(infile,"seed data",prompt,opts)
 		return get_seed_from_seed_data(words)
 		return get_seed_from_seed_data(words)
 	elif no_wallet:
 	elif no_wallet:
 		return False
 		return False

+ 61 - 0
scripts/bitcoind-walletunlock.py

@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C) 2013 by philemon <mmgen-py@yandex.com>
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+bitcoind-walletunlock.py: Unlock a Bitcoin wallet securely
+"""
+
+import sys
+from mmgen.Opts import *
+from mmgen.tx import *
+from mmgen.utils import msg, my_getpass, my_raw_input
+
+prog_name = sys.argv[0].split("/")[-1]
+
+help_data = {
+	'prog_name': prog_name,
+	'desc':    "Unlock a Bitcoin wallet securely",
+	'usage':   "[opts]",
+	'options': """
+-h, --help                 Print this help message
+-e, --echo-passphrase      Print passphrase to screen when typing it
+"""
+}
+
+short_opts = "he"
+long_opts  = "help","echo_passphrase"
+
+opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
+
+c = connect_to_bitcoind()
+
+prompt = "Enter passphrase: "
+if 'echo_passphrase' in opts:
+	password = my_raw_input(prompt)
+else:
+	password = my_getpass(prompt)
+
+from bitcoinrpc import exceptions
+
+try:
+	c.walletpassphrase(password, 9999)
+except exceptions.WalletWrongEncState:
+	msg("Wallet is unencrypted")
+except exceptions.WalletPassphraseIncorrect:
+	msg("Passphrase incorrect")
+except exceptions.WalletAlreadyUnlocked:
+	msg("WARNING: Wallet already unlocked!")

+ 5 - 2
setup.py

@@ -3,7 +3,7 @@ from distutils.core import setup
 
 
 setup(
 setup(
 		name         = 'mmgen',
 		name         = 'mmgen',
-		version      = '0.6',
+		version      = '0.6.1',
 		author       = 'Philemon',
 		author       = 'Philemon',
 		author_email = 'mmgen-py@yandex.com',
 		author_email = 'mmgen-py@yandex.com',
 		url          = 'https://github.com/mmgen/mmgen',
 		url          = 'https://github.com/mmgen/mmgen',
@@ -17,6 +17,7 @@ setup(
 			'mmgen.mnemonic',
 			'mmgen.mnemonic',
 			'mmgen.mn_tirosh',
 			'mmgen.mn_tirosh',
 			'mmgen.Opts',
 			'mmgen.Opts',
+			'mmgen.tx',
 			'mmgen.utils',
 			'mmgen.utils',
 			'mmgen.walletgen'
 			'mmgen.walletgen'
 		],
 		],
@@ -25,6 +26,8 @@ setup(
 			'mmgen-keygen',
 			'mmgen-keygen',
 			'mmgen-passchg',
 			'mmgen-passchg',
 			'mmgen-walletchk',
 			'mmgen-walletchk',
-			'mmgen-walletgen'
+			'mmgen-walletgen',
+			'mmgen-txcreate',
+			'mmgen-txsign'
 		])]
 		])]
 	)
 	)