Browse Source

Version 0.7.4

  Additions/improvements:

  mmgen-txsign     - sign multiple transactions in one operation

  mmgen-addrimport - skip rescanning block chain for new addresses

  mmgen-tool - new functions:

    Bitcoind operations:
    listaccounts - like 'bitcoind listaccounts' but shows MMGen wallet balances
                   too
    getbalance   - like 'bitcoind getbalance' but shows confirmed/unconfirmed,
                   spendable/unspendable

    MMGen-specific operations:
    id8          - generate 8-character MMGen ID checksum for file (or stdin)
    id6          - generate 6-character MMGen ID checksum for file (or stdin)
philemon 10 years ago
parent
commit
242ff6acfa
8 changed files with 189 additions and 90 deletions
  1. 3 2
      mmgen-tool
  2. 4 1
      mmgen-txsend
  3. 68 61
      mmgen-txsign
  4. 2 0
      mmgen/config.py
  5. 92 13
      mmgen/tool.py
  6. 16 11
      mmgen/tx.py
  7. 3 1
      mmgen/util.py
  8. 1 1
      setup.py

+ 3 - 2
mmgen-tool

@@ -38,7 +38,8 @@ help_data = {
 -v, --verbose              Produce more verbose output
 
 COMMANDS:{}
-Type '{} <command> --help for usage information on a particular command
+Type '{} <command> --help for usage information on a particular
+command
 """.format(command_help,prog_name)
 }
 
@@ -62,7 +63,7 @@ if cmd_args and cmd_args[0] == '--help':
 	tool_usage(prog_name, command)
 	sys.exit(0)
 
-args = process_args(prog_name, command, cmd_args) if cmd_args else []
+args = process_args(prog_name, command, cmd_args)
 
 #print command + "(" + ", ".join(args) + ")"
 eval(command + "(" + ", ".join(args) + ")")

+ 4 - 1
mmgen-txsend

@@ -77,7 +77,10 @@ qmsg("Signed transaction file '%s' is valid" % infile)
 
 warn   = "Once this transaction is sent, there's no taking it back!"
 what   = "broadcast this transaction to the network"
-expect = "yes, i really want to do this".upper()
+expect =  "YES, I REALLY WANT TO DO THIS"
+
+if g.quiet: warn,expect = "","YES"
+
 confirm_or_exit(warn, what, expect)
 
 msg("Sending transaction")

+ 68 - 61
mmgen-txsign

@@ -29,8 +29,8 @@ from mmgen.util import msg,qmsg
 
 help_data = {
 	'prog_name': sys.argv[0].split("/")[-1],
-	'desc':    "Sign a Bitcoin transaction generated by mmgen-txcreate",
-	'usage':   "[opts] <transaction file> [mmgen wallet/seed/words/brain file]...",
+	'desc':    "Sign Bitcoin transactions generated by mmgen-txcreate",
+	'usage':   "[opts] <transaction file>,.. [mmgen wallet/seed/words/brainwallet file]...",
 	'options': """
 -h, --help               Print this help message
 -d, --outdir          d  Specify an alternate directory 'd' for output
@@ -101,70 +101,77 @@ for i in infiles: check_infile(i)
 
 c = connect_to_bitcoind()
 
-tx_file = infiles.pop(0)
-m = "" if 'tx_id' in opts else "transaction data"
-tx_data = get_lines_from_file(tx_file,m)
-
-metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,tx_file)
-
-if 'tx_id' in opts:
-	msg(metadata[0])
-	sys.exit(0)
-
-if 'info' in opts:
-	view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata)
-	sys.exit(0)
-
-do_license_msg(immed=True)
-
-# Are inputs mmgen addresses?
-mmgen_addrs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]]
-other_addrs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]]
-
-keys = get_lines_from_file(opts['keys_from_file'],"key data",remove_comments=True) \
-	if 'keys_from_file' in opts else []
-
-if other_addrs and not keys and not 'use_wallet_dat' in opts:
-	missing_keys_errormsg(other_addrs)
-	sys.exit(2)
+seeds = {}
+tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext]
+infiles  = list(set(infiles) - set(tx_files))
 
-if other_addrs and keys and not 'skip_key_preverify' in opts:
-	a = [i['address'] for i in other_addrs]
-	preverify_keys(a, keys)
+if not "info" in opts: do_license_msg(immed=True)
 
-seeds = {}
-check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts)
+keys_from_file = get_lines_from_file(opts['keys_from_file'],"key data",
+	remove_comments=True) if 'keys_from_file' in opts else []
 
-qmsg("Successfully opened transaction file '%s'" % tx_file)
+for tx_file in tx_files:
+	m = "" if 'tx_id' in opts else "transaction data"
+	tx_data = get_lines_from_file(tx_file,m)
 
-prompt = "View transaction data? (y)es, (N)o, (v)iew in pager"
-reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True)
-if reply and reply in "YyVv":
-	p = True if reply in "Vv" else False
-	view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata,pager=p)
+	metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,tx_file)
 
-sig_data = [
-	{"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']}
-		for i in inputs_data]
+	if 'tx_id' in opts:
+		msg(metadata[0])
+		sys.exit(0)
 
-if mmgen_addrs:
-	ml = [i['account'].split()[0] for i in mmgen_addrs]
-	keys += get_keys_for_mmgen_addrs(ml,infiles,seeds,opts)
+	if 'info' in opts:
+		view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata)
+		sys.exit(0)
 
-	if 'use_wallet_dat' in opts:
-		sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
-	else:
-		sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
-elif other_addrs:
-	if keys:
-		sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
+# Are inputs mmgen addresses?
+	mmgen_addrs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]]
+	other_addrs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]]
+
+	keys = keys_from_file
+
+	if other_addrs and not keys and not 'use_wallet_dat' in opts:
+		missing_keys_errormsg(other_addrs)
+		sys.exit(2)
+
+	if other_addrs and keys and not 'skip_key_preverify' in opts:
+		a = [i['address'] for i in other_addrs]
+		preverify_keys(a, keys)
+		opts['skip_key_preverify'] = True
+
+	check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts)
+
+	if len(tx_files) > 1: msg("\nTransaction #%s:" % (tx_files.index(tx_file)+1))
+	qmsg("Successfully opened transaction file '%s'" % tx_file)
+
+	prompt = "View transaction data? (y)es, (N)o, (v)iew in pager"
+	reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True)
+	if reply and reply in "YyVv":
+		p = True if reply in "Vv" else False
+		view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata,pager=p)
+
+	sig_data = [
+		{"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']}
+			for i in inputs_data]
+
+	if mmgen_addrs:
+		ml = [i['account'].split()[0] for i in mmgen_addrs]
+		keys += get_keys_for_mmgen_addrs(ml,infiles,seeds,opts)
+
+		if 'use_wallet_dat' in opts:
+			sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
+		else:
+			sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
+	elif other_addrs:
+		if keys:
+			sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
+		else:
+			sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
+
+	if sig_tx['complete']:
+		prompt = "OK\nSave signed transaction?"
+		if user_confirm(prompt,default_yes=True):
+			print_signed_tx_to_file(tx_hex,sig_tx['hex'],metadata,opts)
 	else:
-		sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
-
-if sig_tx['complete']:
-	prompt = "OK\nSave signed transaction?"
-	if user_confirm(prompt,default_yes=True):
-		print_signed_tx_to_file(tx_hex,sig_tx['hex'],metadata,opts)
-else:
-	msg("failed\nSome keys were missing.  Transaction could not be signed.")
-	sys.exit(3)
+		msg("failed\nSome keys were missing.  Transaction could not be signed.")
+		sys.exit(3)

+ 2 - 0
mmgen/config.py

@@ -37,6 +37,8 @@ incog_hex_ext = "mmincox"
 
 seedfile_exts = wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext
 
+rawtx_ext    = "raw"
+sigtx_ext    = "sig"
 addrfile_ext = "addrs"
 keyfile_ext  = "keys"
 

+ 92 - 13
mmgen/tool.py

@@ -51,13 +51,21 @@ commands = {
 	"mn_rand192":   ['wordlist [str="electrum"]'],
 	"mn_rand256":   ['wordlist [str="electrum"]'],
 	"mn_stats":     ['wordlist [str="electrum"]'],
-	"mn_printlist": ['wordlist [str="electrum"]']
+	"mn_printlist": ['wordlist [str="electrum"]'],
+	"id8":          ['<infile> [str]'],
+	"id6":          ['<infile> [str]'],
+	"listaccounts": ['minconf [int=1]'],
+	"getbalance":   ['minconf [int=1]'],
 }
 
 command_help = """
-General operations:
-hexdump      - encode binary data in formatted hexadecimal form
-unhexdump    - decode formatted hexadecimal data
+File operations
+hexdump      - encode data into formatted hexadecimal form (file or stdin)
+unhexdump    - decode formatted hexadecimal data (file or stdin)
+
+MMGen-specific operations
+id8          - generate 8-character MMGen ID checksum for file (or stdin)
+id6          - generate 6-character MMGen ID checksum for file (or stdin)
 
 Bitcoin operations:
 strtob58     - convert a string to base 58
@@ -68,13 +76,20 @@ randwif      - generate a random private key in WIF format
 randpair     - generate a random private key/address pair
 wif2addr     - generate a Bitcoin address from a key in WIF format
 
-Mnemonic operations (choose "electrum" (default), "tirosh" or "all" wordlists):
+Mnemonic operations (choose "electrum" (default), "tirosh" or "all"
+wordlists):
 mn_rand128   - generate random 128-bit mnemonic
 mn_rand192   - generate random 192-bit mnemonic
 mn_rand256   - generate random 256-bit mnemonic
 mn_stats     - show stats for mnemonic wordlist
 mn_printlist - print mnemonic wordlist
 
+Bitcoind operations (bitcoind must be running):
+listaccounts - like 'bitcoind listaccounts' but shows MMGen wallet balances
+               too
+getbalance   - like 'bitcoind getbalance' but shows confirmed/unconfirmed,
+               spendable/unspendable
+
 IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're
 computed using a different algorithm and are NOT Electrum-compatible!
 """
@@ -119,17 +134,23 @@ def process_args(prog_name, command, uargs):
 
 	ret = []
 
+	def normalize_arg(arg, arg_type):
+		if arg_type == "bool":
+			if arg.lower() in ("true","yes","1","on"): return "True"
+			if arg.lower() in ("false","no","0","off"): return "False"
+		return arg
+
 	for i in range(len(cargs_req)):
-		arg,arg_type = uargs_req[i], cargs_req[i][1]
-		if arg == "true" or arg == "false": arg = arg.capitalize()
+		arg_type = cargs_req[i][1]
+		arg = normalize_arg(uargs_req[i], arg_type)
 		if arg_type == "str":
 			ret.append('"%s"' % (arg))
 		elif test_type(arg_type, arg, "#"+str(i+1)):
 			ret.append('%s' % (arg))
 
 	for k in uargs_nam.keys():
-		arg,arg_type = uargs_nam[k], cargs_nam[k][0]
-		if arg == "true" or arg == "false": arg = arg.capitalize()
+		arg_type = cargs_nam[k][0]
+		arg = normalize_arg(uargs_nam[k], arg_type)
 		if arg_type == "str":
 			ret.append('%s="%s"' % (k, arg))
 		elif test_type(arg_type, arg, "'"+k+"'"):
@@ -149,13 +170,13 @@ def print_convert_results(indata,enc,dec,no_recode=False):
 			msg("WARNING! Recoded number doesn't match input stringwise!")
 
 def hexdump(infile, cols=8, line_nums=True):
-	data = get_data_from_file(infile)
-	o = pretty_hexdump(data, 2, cols, line_nums)
+	d = sys.stdin.read() if infile == "-" else get_data_from_file(infile)
+	o = pretty_hexdump(d, 2, cols, line_nums)
 	print o
 
 def unhexdump(infile):
-	data = get_data_from_file(infile)
-	o = decode_pretty_hexdump(data)
+	d = sys.stdin.read() if infile == "-" else get_data_from_file(infile)
+	o = decode_pretty_hexdump(d)
 	sys.stdout.write(o)
 
 def strtob58(s):
@@ -242,3 +263,61 @@ def mn_stats(wordlist="electrum"):
 def mn_printlist(wordlist="electrum"):
 	l = get_wordlist(wordlist)
 	print "%s" % l.strip()
+
+def id8(infile):
+	d = sys.stdin.read() if infile == "-" else get_data_from_file(infile)
+	print make_chksum_8(d)
+
+def id6(infile):
+	d = sys.stdin.read() if infile == "-" else get_data_from_file(infile)
+	print make_chksum_6(d)
+
+
+def listaccounts(minconf=1):
+	from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr
+	def s_mmgen(i):
+		ma = i[0].split(" ")[0] if " " in i[0] else i[0]
+		if is_mmgen_addr(ma):
+			mmid,idx = ma.split(":")
+			return mmid + ":" + ("%04i" % int(idx))
+		else:
+			return "G"+i[0]
+
+	c = connect_to_bitcoind()
+	data = [(a,c.getbalance(a,minconf)) for a in c.listaccounts()]
+	data.sort(key=s_mmgen)
+	col_w = max([len(d[0]) for d in data])
+	fs = "%-"+str(col_w)+"s   %s"
+	print fs % ("ACCOUNT","BALANCE")
+	totals = {}
+	for d in data:
+		ma = d[0].split(" ")[0] if " " in d[0] else d[0]
+		if is_mmgen_addr(ma):
+			mmid = ma.split(":")[0]
+			if mmid not in totals: totals[mmid] = 0
+			totals[mmid] += d[1]
+		print fs % (
+			d[0] if d[0] else 'TOTAL:',
+			trim_exponent(d[1])
+		)
+	print "\nMMGEN WALLET BALANCES"
+	for k in totals.keys():
+		print "%s: %s" % (k, trim_exponent(totals[k]))
+
+def getbalance(minconf=1):
+	from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr
+	c = connect_to_bitcoind()
+	data = c.listunspent(0)
+	o = [0,0,0,0,0,0] # su,sb,sc, uu,ub,uc
+	for d in data:
+		j = 0 if d.spendable else 3
+		if d.confirmations == 0: o[j] += d.amount
+		k = 1 if d.confirmations < minconf else 2
+		o[j+k] += d.amount
+
+	fs = "{}:\n  {:<12} unconfirmed\n  {:<12} <{M}  {C}\n  {:<12} >={M} {C}"
+	for lbl,n in ("Spendable",0),("Unspendable",3):
+		if sum(o[n:3+n]) == 0:
+			print "{}: {}".format(lbl,"NONE")
+		else:
+			print fs.format(lbl,o[n+0],o[n+1],o[n+2],M=minconf,C="confirmations")

+ 16 - 11
mmgen/tx.py

@@ -141,7 +141,7 @@ def get_bitcoind_cfg_options(cfg_keys):
 
 def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts):
 	tx_id = make_chksum_6(unhexlify(tx)).upper()
-	outfile = "tx_%s[%s].raw" % (tx_id,send_amt)
+	outfile = "tx_%s[%s].%s" % (tx_id,send_amt,g.rawtx_ext)
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 	metadata = "%s %s %s" % (tx_id, send_amt, make_timestamp())
@@ -156,7 +156,7 @@ def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts):
 
 def print_signed_tx_to_file(tx,sig_tx,metadata,opts):
 	tx_id = make_chksum_6(unhexlify(tx)).upper()
-	outfile = "tx_{}[{}].sig".format(*metadata[:2])
+	outfile = "tx_%s[%s].%s" % (metadata[0],metadata[1],g.sigtx_ext)
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 	data = "%s\n%s\n" % (" ".join(metadata),sig_tx)
@@ -174,16 +174,17 @@ def print_sent_tx_to_file(tx,metadata,opts):
 
 def format_unspent_outputs_for_printing(out,sort_info,total):
 
-	pfs  = " %-4s %-67s %-34s %-12s %-13s %-10s %s"
+	pfs  = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s"
 	pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID",
-		"Amount (BTC)","Age (days)", "Comment")]
+		"Amount (BTC)","Confirms","Age (days)", "Comment")]
 
 	for n,i in enumerate(out):
 		addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address
 		tx = " " * 63 + "=" \
 			if i.skip == "txid" and "grouped" in sort_info else str(i.txid)
 
-		s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,i.mmid,i.amt,i.days,i.label)
+		s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,
+				i.mmid,i.amt,i.confirmations,i.days,i.label)
 		pout.append(s.rstrip())
 
 	return \
@@ -200,14 +201,16 @@ def sort_and_view(unspent):
 	def s_age(i):   return i.confirmations
 	def s_mmgen(i): return i.account
 
-	sort,group,show_mmaddr,reverse = "",False,False,False
+	sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True
+	unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
+
 	total = trim_exponent(sum([i.amount for i in unspent]))
 
 	hdr_fmt   = "UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s"
 
 	options_msg = """
 Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
-Display options: [g]roup, show [m]mgen addr, r[e]draw screen
+Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 """.strip()
 	prompt = \
 "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
@@ -230,8 +233,8 @@ Display options: [g]roup, show [m]mgen addr, r[e]draw screen
 		addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
 		tx_w = max(11,min(64, cols-addr_w-32))
 		fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s"
-		table_hdr = fs % ("Num","TX id  Vout","","Address",
-							"Amount (BTC)","Age(d)")
+		a = "Age(d)" if show_days else "Confirms"
+		table_hdr = fs % ("Num","TX id  Vout","","Address", "Amount (BTC)",a)
 
 		unsp = deepcopy(unspent)
 		for i in unsp: i.skip = ""
@@ -277,12 +280,13 @@ Display options: [g]roup, show [m]mgen addr, r[e]draw screen
 		out = [hdr_fmt % (" ".join(sort_info), total), table_hdr]
 
 		for n,i in enumerate(unsp):
-			out.append(fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.days))
+			d = i.days if show_days else i.confirmations
+			out.append(fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,d))
 
 		msg("\n".join(out) +"\n\n" + print_to_file_msg + options_msg)
 		print_to_file_msg = ""
 
-		immed_chars = "atdAMrgmeqpvw"
+		immed_chars = "atDdAMrgmeqpvw"
 		skip_prompt = False
 
 		while True:
@@ -290,6 +294,7 @@ Display options: [g]roup, show [m]mgen addr, r[e]draw screen
 
 			if   reply == 'a': unspent.sort(key=s_amt);  sort = "amount"
 			elif reply == 't': unspent.sort(key=s_txid); sort = "txid"
+			elif reply == 'D': show_days = False if show_days else True
 			elif reply == 'd': unspent.sort(key=s_addr); sort = "address"
 			elif reply == 'A': unspent.sort(key=s_age);  sort = "age"
 			elif reply == 'M':

+ 3 - 1
mmgen/util.py

@@ -42,7 +42,9 @@ def vmsg_r(s):
 
 def bail(): sys.exit(9)
 
-def get_extension(f): return f.split(".")[-1]
+def get_extension(f):
+	try: return f.split(".")[-1]
+	except: return ""
 
 def my_raw_input(prompt,echo=True,allowed_chars=""):
 	try:

+ 1 - 1
setup.py

@@ -3,7 +3,7 @@ from distutils.core import setup
 
 setup(
 		name         = 'mmgen',
-		version      = '0.7.3b',
+		version      = '0.7.4',
 		author       = 'Philemon',
 		author_email = 'mmgen-py@yandex.com',
 		url          = 'https://github.com/mmgen/mmgen',