tx.py: convert_fee_spec -> process_fee_spec, txview refactor

This commit is contained in:
The MMGen Project 2018-05-30 09:12:11 +00:00
commit 4c9f3aa7bb
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
2 changed files with 100 additions and 72 deletions

View file

@ -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

View file

@ -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,6 +969,61 @@ class MMGenTX(MMGenObject):
def is_rbf(self):
return self.inputs[0].sequence == g.max_int - 2
def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse):
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)):
if ip and blockcount:
confs = e.confs + blockcount - self.blockcount
days = int(confs / confs_per_day)
if e.mmid:
mmid_fmt = e.mmid.fmt(
width=max_mmwid,
encl='()',
color=True,
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,color=True)
if terse:
out += '{:3} {} {} {} {}\n'.format(n+1,
e.addr.fmt(color=True,width=addr_w),
mmid_fmt,e.amt.hl(),g.coin)
else:
icommon = [
((n+1,'')[ip],'address:',e.addr.fmt(color=True,width=addr_w) + ' '+mmid_fmt),
('','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 else '')
] if ip else icommon + [
('','change:',green('True') if e.is_chg else '')]
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()
@ -974,51 +1041,15 @@ class MMGenTX(MMGenObject):
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_io(io):
ip = io is self.inputs
io_out = ''
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:
confs = e.confs + blockcount - self.blockcount
days = int(confs / confs_per_day)
if e.mmid:
mmid_fmt = e.mmid.fmt(
width=max_mmwid,
encl='()',
color=True,
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)
if terse:
io_out += '{:3} {} {} {} {}\n'.format(n+1,
e.addr.fmt(color=True,width=addr_w),
mmid_fmt,e.amt.hl(),g.coin)
else:
icommon = [
((n+1,'')[ip],'address:',e.addr.fmt(color=True,width=addr_w) + ' '+mmid_fmt),
('','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 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 = (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: