Browse Source

addr.py,rpc.py,tw.py,tx.py,traceback.py: minor cleanups and refactoring

MMGen 6 years ago
parent
commit
1b54d425bd
5 changed files with 137 additions and 103 deletions
  1. 5 2
      mmgen/addr.py
  2. 4 1
      mmgen/rpc.py
  3. 36 40
      mmgen/tw.py
  4. 76 53
      mmgen/tx.py
  5. 16 7
      scripts/traceback.py

+ 5 - 2
mmgen/addr.py

@@ -545,12 +545,15 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 		return [d.addr for d in self.data if not getattr(d,key)]
 
 	def generate_addrs_from_keys(self):
-		kg = KeyGenerator('std')
-		ag = AddrGenerator('p2pkh')
+		# assume that the first listed mmtype is valid for flat key list
+		t = MMGenAddrType(g.proto.mmtypes[0])
+		kg = KeyGenerator(t.pubkey_type)
+		ag = AddrGenerator(t.gen_method)
 		d = self.data
 		for n,e in enumerate(d,1):
 			qmsg_r('\rGenerating addresses from keylist: {}/{}'.format(n,len(d)))
 			e.addr = ag.to_addr(kg.to_pubhex(e.sec))
+			if g.debug_addrlist: Msg('generate_addrs_from_keys():\n{}'.format(e.pformat()))
 		qmsg('\rGenerated addresses from keylist: {}/{} '.format(n,len(d)))
 
 	def format(self,enable_comments=False):

+ 4 - 1
mmgen/rpc.py

@@ -213,7 +213,6 @@ class EthereumRPCConnection(CoinDaemonRPCConnection):
 		'eth_blockNumber',
 		'eth_gasPrice',
 		'eth_getBalance',
-		'eth_getBlock',
 		'eth_getBlockByHash',
 		'eth_getBlockByNumber',
 		'eth_getTransactionByHash',
@@ -225,7 +224,11 @@ class EthereumRPCConnection(CoinDaemonRPCConnection):
 		'net_peerCount',
 		'net_version',
 		'parity_chain',
+		# Returns the EIP155 chain ID used for transaction signing at the current best block.
+		# Null is returned if not available.
+		'parity_chainId',
 		'parity_chainStatus',
+		'parity_composeTransaction',
 		'parity_gasCeilTarget',
 		'parity_gasFloorTarget',
 		'parity_localTransactions',

+ 36 - 40
mmgen/tw.py

@@ -29,8 +29,9 @@ CUR_HOME,ERASE_ALL = '\033[H','\033[0J'
 class TwUnspentOutputs(MMGenObject):
 
 	txid_w = 64
-	show_tx = True
+	show_txid = True
 	can_group = True
+	hdr_fmt = 'UNSPENT OUTPUTS (sort order: {}) Total {}: {}'
 	prompt = """
 Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
 Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
@@ -175,21 +176,19 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 					if self.sort_key == k and getattr(a,k) == getattr(b,k):
 						b.skip = (k,'addr')[k=='twmmid']
 
-		hdr_fmt = 'UNSPENT OUTPUTS (sort order: {}) Total {}: {}'
-		out  = [hdr_fmt.format(' '.join(self.sort_info()),g.coin,self.total.hl())]
+		out  = [self.hdr_fmt.format(' '.join(self.sort_info()),g.coin,self.total.hl())]
 		if g.chain in ('testnet','regtest'):
 			out += [green('Chain: {}'.format(g.chain.upper()))]
-		if self.show_tx:
+		if self.show_txid:
 			fs = u' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (col1_w,tx_w)
 		else:
 			fs = u' {n:%s} {a} {A} {c:<}' % col1_w
-		out += [fs.format(
-				n='Num',
-				t='TXid'.ljust(tx_w - 5) + ' Vout',
-				v='',
-				a='Address'.ljust(addr_w),
-				A='Amt({})'.format(g.coin).ljust(g.proto.coin_amt.max_prec+4),
-				c=('Confs','Age(d)')[self.show_days])]
+		out += [fs.format(  n='Num',
+							t='TXid'.ljust(tx_w - 5) + ' Vout',
+							v='',
+							a='Address'.ljust(addr_w),
+							A='Amt({})'.format(g.coin).ljust(g.proto.coin_amt.max_prec+4),
+							c=('Confs','Age(d)')[self.show_days])]
 
 		for n,i in enumerate(unsp):
 			addr_dots = '|' + '.'*(addr_w-1)
@@ -209,13 +208,12 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 			tx = ' ' * (tx_w-4) + '|...' if i.skip == 'txid' \
 					else i.txid[:tx_w-len(txdots)]+txdots
 
-			out.append(fs.format(
-						n=str(n+1)+')',
-						t=tx,
-						v=i.vout,
-						a=addr_out,
-						A=i.amt.fmt(color=True),
-						c=i.days if self.show_days else i.confs))
+			out.append(fs.format(   n=str(n+1)+')',
+									t=tx,
+									v=i.vout,
+									a=addr_out,
+									A=i.amt.fmt(color=True),
+									c=i.days if self.show_days else i.confs))
 
 		self.fmt_display = '\n'.join(out) + '\n'
 #		unsp.pdie()
@@ -225,36 +223,34 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 
 		addr_w = max(len(i.addr) for i in self.unspent)
 		mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.unspent) or 12 # DEADBEEF:S:1
-		if self.show_tx:
+		if self.show_txid:
 			fs  = ' {n:4} {t:%s} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % (self.txid_w+3,g.proto.coin_amt.max_prec+4)
 		else:
 			fs  = ' {n:4} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % (g.proto.coin_amt.max_prec+4)
-		out = [fs.format(
-				n='Num',
-				t='Tx ID,Vout',
-				a='Address'.ljust(addr_w),
-				m='MMGen ID'.ljust(mmid_w+1),
-				A='Amount({})'.format(g.coin),
-				c='Confs',
-				g='Age(d)',
-				l='Label')]
+		out = [fs.format(   n='Num',
+							t='Tx ID,Vout',
+							a='Address'.ljust(addr_w),
+							m='MMGen ID'.ljust(mmid_w+1),
+							A='Amount({})'.format(g.coin),
+							c='Confs',
+							g='Age(d)',
+							l='Label')]
 
 		max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [1])
 		for n,i in enumerate(self.unspent):
 			addr = '|'+'.' * addr_w if i.skip == 'addr' and self.group else i.addr.fmt(color=color,width=addr_w)
 			tx = '|'+'.' * 63 if i.skip == 'txid' and self.group else str(i.txid)
-			out.append(
-				fs.format(
-					n=str(n+1)+')',
-					t=tx+','+str(i.vout),
-					a=addr,
-					m=MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen'
-						else 'Non-{}'.format(g.proj_name),width=mmid_w,color=color),
-					A=i.amt.fmt(color=color),
-					c=i.confs,
-					g=i.days,
-					l=i.label.hl(color=color) if i.label else
-						TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len)).rstrip())
+			out.append(fs.format(
+						n=str(n+1)+')',
+						t=tx+','+str(i.vout),
+						a=addr,
+						m=MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen'
+							else 'Non-{}'.format(g.proj_name),width=mmid_w,color=color),
+						A=i.amt.fmt(color=color),
+						c=i.confs,
+						g=i.days,
+						l=i.label.hl(color=color) if i.label else
+							TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len)).rstrip())
 
 		fs = 'Unspent outputs ({} UTC)\nSort order: {}\n{}\n\nTotal {}: {}\n'
 		self.fmt_print = fs.format(

+ 76 - 53
mmgen/tx.py

@@ -80,16 +80,6 @@ def strfmt_locktime(num,terse=False):
 	else:
 		die(2,"'{}': invalid locktime value!".format(num))
 
-def select_unspent(unspent,prompt):
-	while True:
-		reply = my_raw_input(prompt).strip()
-		if reply:
-			selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return')
-			if selected:
-				if selected[-1] <= len(unspent):
-					return selected
-				msg('Unspent output number must be <= {}'.format(len(unspent)))
-
 def mmaddr2coinaddr(mmaddr,ad_w,ad_f):
 
 	# assume mmaddr has already been checked
@@ -234,6 +224,8 @@ class MMGenTX(MMGenObject):
 	sig_ext  = 'sigtx'
 	txid_ext = 'txid'
 	desc     = 'transaction'
+	chg_fs   = 'Transaction produces {} {} in change'
+	no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
 
 	class MMGenTxInput(MMGenListItem):
 		for k in txio_attrs: locals()[k] = txio_attrs[k] # in lieu of inheritance
@@ -275,6 +267,7 @@ class MMGenTX(MMGenObject):
 		self.inputs      = self.MMGenTxInputList()
 		self.outputs     = self.MMGenTxOutputList()
 		self.send_amt    = g.proto.coin_amt('0')  # total amt minus change
+		self.fee         = g.proto.coin_amt('0')
 		self.hex         = ''           # raw serialized hex transaction
 		self.label       = MMGenTXLabel('')
 		self.txid        = ''
@@ -295,11 +288,15 @@ class MMGenTX(MMGenObject):
 			self.check_sigs() # marks the tx as signed
 
 		# repeat with sign and send, because coin daemon could be restarted
-		self.die_if_incorrect_chain()
+		self.check_correct_chain(on_fail='die')
 
-	def die_if_incorrect_chain(self):
-		if self.chain and g.chain and self.chain != g.chain:
-			die(2,'Transaction is for {}, but current chain is {}!'.format(self.chain,g.chain))
+	def check_correct_chain(self,on_fail='return'):
+		assert on_fail in ('return','die'),"'{}': invalid value for 'on_fail'".format(on_fail)
+		m = 'Transaction is for {}, but current chain is {}!'.format(self.chain,g.chain)
+		bad = self.chain and g.chain and self.chain != g.chain
+		if bad:
+			msg(m) if on_fail == 'return' else die(2,m)
+		return not bad
 
 	def add_output(self,coinaddr,amt,is_chg=None):
 		self.outputs.append(MMGenTX.MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg))
@@ -564,8 +561,11 @@ class MMGenTX(MMGenObject):
 		assert type(val) == int,'locktime value not an integer'
 		self.hex = self.hex[:-8] + hexlify(unhexlify('{:08x}'.format(val))[::-1])
 
+	def get_blockcount(self):
+		return int(g.rpch.getblockcount())
+
 	def add_blockcount(self):
-		self.blockcount = int(g.rpch.getblockcount())
+		self.blockcount = self.get_blockcount()
 
 	def format(self):
 		self.inputs.check_coin_mismatch()
@@ -604,12 +604,15 @@ class MMGenTX(MMGenObject):
 	def sign(self,tx_num_str,keys):
 
 		if self.marked_signed():
-			die(1,'Transaction is already signed!')
+			msg('Transaction is already signed!')
+			return False
 
-		self.die_if_incorrect_chain()
+		if not self.check_correct_chain(on_fail='return'):
+			return False
 
 		if (self.has_segwit_inputs() or self.has_segwit_outputs()) and not g.proto.cap('segwit'):
-			die(2,yellow("TX has Segwit inputs or outputs, but {} doesn't support Segwit!".format(g.coin)))
+			ymsg("TX has Segwit inputs or outputs, but {} doesn't support Segwit!".format(g.coin))
+			return False
 
 		qmsg('Passing {} key{} to {}'.format(len(keys),suf(keys,'s'),g.proto.daemon_name))
 
@@ -793,7 +796,7 @@ class MMGenTX(MMGenObject):
 		if not self.marked_signed():
 			die(1,'Transaction is not signed!')
 
-		self.die_if_incorrect_chain()
+		self.check_correct_chain(on_fail='die')
 
 		self.check_hex_tx_matches_mmgen_tx(DeserializedTX(self.hex))
 
@@ -908,7 +911,7 @@ class MMGenTX(MMGenObject):
 	def format_view(self,terse=False):
 		try:
 			rpc_init()
-			blockcount = g.rpch.getblockcount()
+			blockcount = self.get_blockcount()
 		except:
 			blockcount = None
 
@@ -928,7 +931,7 @@ class MMGenTX(MMGenObject):
 			addr_w = max(len(e.addr) for e in io)
 			confs_per_day = 60*60*24 / g.proto.secs_per_block
 			for n,e in enumerate(sorted(io,key=lambda o: o.mmid.sort_key if o.mmid else o.addr)):
-				if ip and blockcount != None:
+				if ip and blockcount:
 					confs = e.confs + blockcount - self.blockcount
 					days = int(confs / confs_per_day)
 				if e.mmid:
@@ -950,7 +953,7 @@ class MMGenTX(MMGenObject):
 						('','comment:',e.label.hl() if e.label else ''),
 						('','amount:','{} {}'.format(e.amt.hl(),g.coin))]
 					items = [(n+1, 'tx,vout:','{},{}'.format(e.txid,e.vout))] + icommon + [
-						('','confirmations:','{} (around {} days)'.format(confs,days) if blockcount!=None else '')
+						('','confirmations:','{} (around {} days)'.format(confs,days) if blockcount else '')
 					] if ip else icommon + [
 						('','change:',green('True') if e.is_chg else '')]
 					io_out += '\n'.join([(u'{:>3} {:<8} {}'.format(*d)) for d in items if d[2]]) + '\n\n'
@@ -964,8 +967,7 @@ class MMGenTX(MMGenObject):
 							self.send_amt.hl(),
 							g.coin,
 							self.timestamp,
-							(red('False'),
-							green('True'))[self.is_rbf()],
+							(red('False'),green('True'))[self.is_rbf()],
 							self.marked_signed(color=True),
 							(green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)])
 		if self.chain in ('testnet','regtest'):
@@ -998,6 +1000,9 @@ class MMGenTX(MMGenObject):
 
 		return out # TX label might contain non-ascii chars
 
+	def check_tx_hex_data(self):
+		self.hex = HexStr(self.hex,on_fail='raise')
+
 	def parse_tx_file(self,infile,md_only=False,silent_open=False):
 
 		def eval_io_data(raw_data,desc):
@@ -1069,7 +1074,7 @@ class MMGenTX(MMGenObject):
 			desc = 'block count in metadata'
 			self.blockcount = int(blockcount)
 			desc = 'transaction hex data'
-			self.hex = HexStr(self.hex,on_fail='raise')
+			self.check_tx_hex_data()
 			if md_only: return # the following ops will all fail if g.coin doesn't match self.coin
 			desc = 'coin type in metadata'
 			assert self.coin == g.coin,'invalid coin type'
@@ -1093,38 +1098,27 @@ class MMGenTX(MMGenObject):
 			try:
 				ret = g.rpch.estimatesmartfee(opt.tx_confs,on_fail='raise')
 			except:
-				fetype = 'estimatefee'
+				fe_type = 'estimatefee'
 				fee_per_kb = g.rpch.estimatefee(opt.tx_confs)
 			else:
-				fetype = 'estimatesmartfee'
+				fe_type = 'estimatesmartfee'
 				fee_per_kb = ret['feerate'] if 'feerate' in ret else -2
 
 			if fee_per_kb < 0:
 				if not have_estimate_fail:
-					msg('Network fee estimation for {} confirmations failed ({})'.format(opt.tx_confs,fetype))
+					msg('Network fee estimation for {} confirmations failed ({})'.format(opt.tx_confs,fe_type))
 					have_estimate_fail.append(True)
 				start_fee = None
 			else:
 				start_fee = g.proto.coin_amt(fee_per_kb) * opt.tx_fee_adj * self.estimate_size() / 1024
 				if opt.verbose:
 					msg('{} fee for {} confirmations: {} {}/kB'.format(
-						fetype.upper(),opt.tx_confs,fee_per_kb,g.coin))
+						fe_type.upper(),opt.tx_confs,fee_per_kb,g.coin))
 					msg('TX size (estimated): {}'.format(self.estimate_size()))
 
 		return self.get_usr_fee_interactive(start_fee,desc=desc)
 
-	def get_outputs_from_cmdline(self,cmd_args):
-		from mmgen.addr import AddrList,AddrData
-		addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext]
-		cmd_args = set(cmd_args) - set(addrfiles)
-
-		ad_f = AddrData()
-		for a in addrfiles:
-			check_infile(a)
-			ad_f.add(AddrList(a))
-
-		ad_w = AddrData(source='tw')
-
+	def process_cmd_args(self,cmd_args,ad_f,ad_w):
 		for a in cmd_args:
 			if ',' in a:
 				a1,a2 = a.split(',',1)
@@ -1141,24 +1135,48 @@ class MMGenTX(MMGenObject):
 			else:
 				die(2,'{}: invalid command-line argument'.format(a))
 
-		if not self.outputs:
-			die(2,'At least one output must be specified on the command line')
-
 		if self.get_chg_output_idx() == None:
 			die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(self.outputs) == 1])
 
-		self.add_mmaddrs_to_outputs(ad_w,ad_f)
-		self.check_dup_addrs('outputs')
-
 		if not segwit_is_active() and self.has_segwit_outputs():
 			fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
 			rdie(2,fs.format(g.proj_name))
 
+	def get_outputs_from_cmdline(self,cmd_args):
+		from mmgen.addr import AddrList,AddrData
+		addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext]
+		cmd_args = set(cmd_args) - set(addrfiles)
+
+		ad_f = AddrData()
+		for a in addrfiles:
+			check_infile(a)
+			ad_f.add(AddrList(a))
+
+		ad_w = AddrData(source='tw')
+
+		self.process_cmd_args(cmd_args,ad_f,ad_w)
+
+		if not self.outputs:
+			die(2,'At least one output must be specified on the command line')
+
+		self.add_mmaddrs_to_outputs(ad_w,ad_f)
+		self.check_dup_addrs('outputs')
+
+	def select_unspent(self,unspent):
+		prompt = 'Enter a range or space-separated list of outputs to spend: '
+		while True:
+			reply = my_raw_input(prompt).strip()
+			if reply:
+				selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return')
+				if selected:
+					if selected[-1] <= len(unspent):
+						return selected
+					msg('Unspent output number must be <= {}'.format(len(unspent)))
+
 	def get_inputs_from_user(self,tw):
 
 		while True:
-			m = 'Enter a range or space-separated list of outputs to spend: '
-			sel_nums = select_unspent(tw.unspent,m)
+			sel_nums = self.select_unspent(tw.unspent)
 			msg('Selected output{}: {}'.format(suf(sel_nums,'s'),' '.join(map(str,sel_nums))))
 
 			sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums])
@@ -1176,16 +1194,21 @@ class MMGenTX(MMGenObject):
 
 			self.copy_inputs_from_tw(sel_unspent)  # makes self.inputs
 
-			change_amt = self.sum_inputs() - self.send_amt - self.get_fee_from_user()
+			self.fee = self.get_fee_from_user()
+
+			change_amt = self.sum_inputs() - self.send_amt - self.fee
 
 			if change_amt >= 0:
-				p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin)
+				p = self.chg_fs.format(change_amt.hl(),g.coin)
 				if opt.yes or keypress_confirm(p+'.  OK?',default_yes=True):
 					if opt.yes: msg(p)
 					return change_amt
 			else:
 				msg(wmsg['not_enough_coin'].format(abs(change_amt)))
 
+	def check_fee(self):
+		assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee
+
 	def create(self,cmd_args,locktime,do_info=False):
 		assert type(locktime) == int
 
@@ -1217,7 +1240,7 @@ class MMGenTX(MMGenObject):
 		chg_idx = self.get_chg_output_idx()
 
 		if change_amt == 0:
-			msg('Warning: Change address will be deleted as transaction produces no change')
+			msg(self.no_chg_msg)
 			self.del_output(chg_idx)
 		else:
 			self.update_output_amt(chg_idx,g.proto.coin_amt(change_amt))
@@ -1239,7 +1262,7 @@ class MMGenTX(MMGenObject):
 		self.add_blockcount()
 		self.chain = g.chain
 
-		assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee
+		self.check_fee()
 
 		qmsg('Transaction successfully created')
 

+ 16 - 7
scripts/traceback.py

@@ -6,7 +6,18 @@ if 'TMUX' in os.environ: del os.environ['TMUX']
 os.environ['MMGEN_TRACEBACK'] = '1'
 
 tb_source = open(sys.argv[1])
-tb_file = open('my.err','w')
+tb_file = os.path.join(os.environ['PWD'],'my.err')
+
+def process_exception(es):
+	l = traceback.format_exception(*es)
+	l_save = l[:]
+	exc = l.pop()
+	if exc[:11] == 'SystemExit:': l.pop()
+	def red(s):    return '{e}[31;1m{}{e}[0m'.format(s,e='\033')
+	def yellow(s): return '{e}[33;1m{}{e}[0m'.format(s,e='\033')
+	sys.stdout.write('{}{}'.format(yellow(''.join(l)),red(exc)))
+	with open(tb_file,'w') as f:
+		f.write(''.join(l_save))
 
 try:
 	sys.argv.pop(0)
@@ -14,12 +25,10 @@ try:
 except SystemExit:
 #	pass
 	e = sys.exc_info()
+	if int(str(e[1])) != 0:
+		process_exception(e)
 	sys.exit(int(str(e[1])))
 except:
-	l = traceback.format_exception(*sys.exc_info())
-	exc = l.pop()
-	def red(s):    return '{e}[31;1m{}{e}[0m'.format(s,e='\033')
-	def yellow(s): return '{e}[33;1m{}{e}[0m'.format(s,e='\033')
-	sys.stdout.write('{}{}'.format(yellow(''.join(l)),red(exc)))
-	traceback.print_exc(file=tb_file)
+	e = sys.exc_info()
+	process_exception(e)
 	sys.exit(1)