Browse Source

tx.py: convert_fee_spec -> process_fee_spec, txview refactor

MMGen 7 years ago
parent
commit
4c9f3aa7bb
2 changed files with 100 additions and 72 deletions
  1. 5 5
      mmgen/opts.py
  2. 95 67
      mmgen/tx.py

+ 5 - 5
mmgen/opts.py

@@ -338,13 +338,13 @@ def init(opts_f,add_opts=[],opt_filter=None):
 
 def opt_is_tx_fee(val,desc):
 	from mmgen.tx import MMGenTX
-	ret = MMGenTX().convert_fee_spec(val,224,on_fail='return')
+	ret = MMGenTX().process_fee_spec(val,224,on_fail='return')
 	if ret == False:
-		msg("'{}': invalid {} (not a {} amount or satoshis-per-byte specification)".format(
-				val,desc,g.coin.upper()))
+		msg("'{}': invalid {}\n(not a {} amount or {} specification)".format(
+				val,desc,g.coin.upper(),MMGenTX().rel_fee_desc))
 	elif ret != None and ret > g.proto.max_tx_fee:
-		msg("'{}': invalid {} (> max_tx_fee ({} {}))".format(
-				val,desc,g.proto.max_tx_fee,g.coin.upper()))
+		msg("'{}': invalid {}\n({} > max_tx_fee ({} {}))".format(
+				val,desc,ret.fmt(fs='1.1'),g.proto.max_tx_fee,g.coin.upper()))
 	else:
 		return True
 	return False

+ 95 - 67
mmgen/tx.py

@@ -224,10 +224,14 @@ class MMGenTX(MMGenObject):
 	sig_ext  = 'sigtx'
 	txid_ext = 'txid'
 	desc     = 'transaction'
-	chg_fs   = 'Transaction produces {} {} in change'
+	chg_msg_fs = 'Transaction produces {} {} in change'
 	fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
 	no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
 	rel_fee_desc = 'satoshis per byte'
+	rel_fee_disp = 'satoshis per byte'
+	txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
+	txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
+	usr_fee_prompt = 'Enter transaction fee: '
 
 	class MMGenTxInput(MMGenListItem):
 		for k in txio_attrs: locals()[k] = txio_attrs[k] # in lieu of inheritance
@@ -471,7 +475,7 @@ class MMGenTX(MMGenObject):
 		vmsg('Relay fee: {} {c}/kB, for transaction: {} {c}'.format(kb_fee,ret,c=g.coin))
 		return ret
 
-	# convert absolute BTC fee to satoshis-per-byte
+	# convert absolute BTC fee to satoshis-per-byte using estimated size
 	def fee_abs2rel(self,abs_fee):
 		return int(abs_fee/g.proto.coin_amt.min_coin_unit/self.estimate_size())
 
@@ -486,21 +490,29 @@ class MMGenTX(MMGenObject):
 
 		return rel_fee,fe_type
 
-	def convert_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
+	# given tx size, rel fee and units, return absolute fee
+	def convert_fee_spec(self,tx_size,units,amt,unit):
+		self.usr_rel_fee = None # TODO
+		return g.proto.coin_amt(int(amt)*tx_size*getattr(g.proto.coin_amt,units[unit])) \
+			if tx_size else None
+
+	# given tx size and absolute fee or fee spec, return absolute fee
+	# relative fee is N+<first letter of unit name>
+	def process_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
+		import re
+		units = dict((u[0],u) for u in g.proto.coin_amt.units)
+		pat = r'([1-9][0-9]*)({})'.format('|'.join(units.keys()))
 		if g.proto.coin_amt(tx_fee,on_fail='silent'):
 			return g.proto.coin_amt(tx_fee)
-		elif len(tx_fee) >= 2 and tx_fee[-1] == 's' and is_int(tx_fee[:-1]) and int(tx_fee[:-1]) >= 1:
-			if tx_size:
-				return g.proto.coin_amt(int(tx_fee[:-1]) * tx_size * g.proto.coin_amt.min_coin_unit)
-			else:
-				return None
+		elif re.match(pat,tx_fee):
+			return self.convert_fee_spec(tx_size,units,*re.match(pat,tx_fee).groups())
 		else:
 			if on_fail == 'return':
 				return False
 			elif on_fail == 'throw':
 				assert False, "'{}': invalid tx-fee argument".format(tx_fee)
 
-	# given network fee estimate in BTC/kB and tx size, calculate absolute fee in coin units
+	# given network fee estimate in BTC/kB, return absolute fee using estimated tx size
 	def calculate_fee(self,rel_fee,fe_type=None):
 		tx_size = self.estimate_size()
 		ret = g.proto.coin_amt(rel_fee) * opt.tx_fee_adj * tx_size / 1024
@@ -510,14 +522,14 @@ class MMGenTX(MMGenObject):
 		return ret
 
 	def convert_and_check_fee(self,tx_fee,desc='Missing description'):
-		abs_fee = self.convert_fee_spec(tx_fee,self.estimate_size(),on_fail='return')
+		abs_fee = self.process_fee_spec(tx_fee,self.estimate_size(),on_fail='return')
 		if abs_fee == None:
 			# we shouldn't be calling this if tx size is unknown
-			m = "'{}': cannot convert satoshis-per-byte to {} because transaction size is unknown"
-			assert False, m.format(tx_fee,g.coin)
+			m = "'{}': cannot convert {} to {} because transaction size is unknown"
+			assert False, m.format(tx_fee,self.rel_fee_desc,g.coin)
 		elif abs_fee == False:
-			m = "'{}': invalid TX fee (not a {} amount or satoshis-per-byte specification)"
-			msg(m.format(tx_fee,g.coin))
+			m = "'{}': invalid TX fee (not a {} amount or {} specification)"
+			msg(m.format(tx_fee,g.coin,self.rel_fee_desc))
 			return False
 		elif abs_fee > g.proto.max_tx_fee:
 			m = '{} {c}: {} fee too large (maximum fee: {} {c})'
@@ -544,11 +556,11 @@ class MMGenTX(MMGenObject):
 						abs_fee.hl(),
 						g.coin,
 						pink(str(self.fee_abs2rel(abs_fee))),
-						self.rel_fee_desc)
+						self.rel_fee_disp)
 				if opt.yes or keypress_confirm(p+'OK?',default_yes=True):
 					if opt.yes: msg(p)
 					return abs_fee
-			tx_fee = my_raw_input('Enter transaction fee: ')
+			tx_fee = my_raw_input(self.usr_fee_prompt)
 			desc = 'User-selected'
 
 	def get_fee_from_user(self,have_estimate_fail=[]):
@@ -957,26 +969,12 @@ class MMGenTX(MMGenObject):
 	def is_rbf(self):
 		return self.inputs[0].sequence == g.max_int - 2
 
-	def format_view(self,terse=False):
-		try:
-			rpc_init()
-			blockcount = self.get_blockcount()
-		except:
-			blockcount = None
-
-		def get_max_mmwid(io):
-			if io == self.inputs:
-				sel_f = lambda o: len(o.mmid) + 2 # len('()')
-			else:
-				sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)')
-			return  max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str))
-
-		nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name)
-		max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs))
+	def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse):
 
-		def format_io(io):
-			ip = io is self.inputs
-			io_out = ''
+		def format_io(desc):
+			io = getattr(self,desc)
+			ip = desc == 'inputs'
+			out = desc.capitalize() + ':\n' + enl
 			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)):
@@ -991,9 +989,9 @@ class MMGenTX(MMGenObject):
 						append_chars=('',' (chg)')[bool(not ip and e.is_chg and terse)],
 						append_color='green')
 				else:
-					mmid_fmt = MMGenID.fmtc(nonmm_str,width=max_mmwid)
+					mmid_fmt = MMGenID.fmtc(nonmm_str,width=max_mmwid,color=True)
 				if terse:
-					io_out += '{:3} {} {} {} {}\n'.format(n+1,
+					out += '{:3} {} {} {} {}\n'.format(n+1,
 						e.addr.fmt(color=True,width=addr_w),
 						mmid_fmt,e.amt.hl(),g.coin)
 				else:
@@ -1005,20 +1003,53 @@ class MMGenTX(MMGenObject):
 						('','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'
-			return io_out
+					out += '\n'.join([(u'{:>3} {:<8} {}'.format(*d)) for d in items if d[2]]) + '\n\n'
+			return out
+
+		return  format_io('inputs') + format_io('outputs')
+
+	def format_view_rel_fee(self,terse):
+		return ' ({} {})\n'.format(
+			pink(str(self.fee_abs2rel(self.get_fee_from_tx()))),
+			self.rel_fee_disp)
+
+	def format_view_abs_fee(self):
+		return g.proto.coin_amt(self.get_fee_from_tx()).hl()
+
+	def format_view_verbose_footer(self):
+		ts = len(self.hex)/2 if self.hex else 'unknown'
+		out = 'Transaction size: Vsize {} (estimated), Total {}'.format(self.estimate_size(),ts)
+		if self.marked_signed():
+			ws = DeserializedTX(self.hex)['witness_size']
+			out += ', Base {}, Witness {}'.format(ts-ws,ws)
+		return out + '\n'
+
+	def format_view(self,terse=False):
+		try:
+			rpc_init()
+			blockcount = self.get_blockcount()
+		except:
+			blockcount = None
+
+		def get_max_mmwid(io):
+			if io == self.inputs:
+				sel_f = lambda o: len(o.mmid) + 2 # len('()')
+			else:
+				sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)')
+			return  max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str))
+
+		nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name)
+		max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs))
+
+		out = (self.txview_hdr_fs,self.txview_hdr_fs_short)[bool(terse)].format(
+			i=self.txid.hl(),
+			a=self.send_amt.hl(),
+			c=g.coin,
+			t=self.timestamp,
+			r=(red('False'),green('True'))[self.is_rbf()],
+			s=self.marked_signed(color=True),
+			l=(green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)])
 
-		hdr_fs = (
-			'TRANSACTION DATA\n\nID={} ({} {}) UTC={} RBF={} Sig={} Locktime={}\n',
-			'TX {} ({} {}) UTC={} RBF={} Sig={} Locktime={}\n'
-		)[bool(terse)]
-		out = hdr_fs.format(self.txid.hl(),
-							self.send_amt.hl(),
-							g.coin,
-							self.timestamp,
-							(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'):
 			out += green('Chain: {}\n'.format(self.chain.upper()))
 		if self.coin_txid:
@@ -1027,25 +1058,22 @@ class MMGenTX(MMGenObject):
 		out += enl
 		if self.label:
 			out += u'Comment: {}\n{}'.format(self.label.hl(),enl)
-		out += 'Inputs:\n' + enl + format_io(self.inputs)
-		out += 'Outputs:\n' + enl + format_io(self.outputs)
+
+		out += self.format_view_body(blockcount,nonmm_str,max_mmwid,enl,terse=terse)
 
 		fs = (
-			'Total input:  {} {c}\nTotal output: {} {c}\nTX fee:       {} {c} ({} satoshis per byte)\n',
-			'In {} {c} - Out {} {c} - Fee {} {c} ({} satoshis/byte)\n'
+			'Total input:  {i} {c}\nTotal output: {o} {c}\nTX fee:       {a} {c}{r}\n',
+			'In {i} {c} - Out {o} {c}\nFee {a} {c}{r}\n'
 		)[bool(terse)]
 
-		t_in,t_out = self.sum_inputs(),self.sum_outputs()
-		fee = t_in-t_out
-		out += fs.format(t_in.hl(),t_out.hl(),fee.hl(),pink(str(self.fee_abs2rel(fee))),c=g.coin)
+		out += fs.format(
+			i=self.sum_inputs().hl(),
+			o=self.sum_outputs().hl(),
+			a=self.format_view_abs_fee(),
+			r=self.format_view_rel_fee(terse),
+			c=g.coin)
 
-		if opt.verbose:
-			ts = len(self.hex)/2 if self.hex else 'unknown'
-			out += 'Transaction size: Vsize {} (estimated), Total {}'.format(self.estimate_size(),ts)
-			if self.marked_signed():
-				ws = DeserializedTX(self.hex)['witness_size']
-				out += ', Base {}, Witness {}'.format(ts-ws,ws)
-			out += '\n'
+		if opt.verbose: out += self.format_view_verbose_footer()
 
 		return out # TX label might contain non-ascii chars
 
@@ -1218,7 +1246,7 @@ class MMGenTX(MMGenObject):
 			change_amt = self.sum_inputs() - self.send_amt - self.fee
 
 			if change_amt >= 0:
-				p = self.chg_fs.format(change_amt.hl(),g.coin)
+				p = self.chg_msg_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
@@ -1345,8 +1373,8 @@ class MMGenBumpTX(MMGenTX):
 	def convert_and_check_fee(self,tx_fee,desc):
 		ret = super(type(self),self).convert_and_check_fee(tx_fee,desc)
 		if ret < self.min_fee:
-			msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} satoshis per byte)'.format(
-				ret,desc,self.min_fee,self.fee_abs2rel(self.min_fee),c=g.coin))
+			msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format(
+				ret,desc,self.min_fee,self.fee_abs2rel(self.min_fee),self.rel_fee_desc,c=g.coin))
 			return False
 		output_amt = self.outputs[self.bump_output_idx].amt
 		if ret >= output_amt: