Dynamic transaction fees

This commit is contained in:
philemon 2016-06-25 18:27:45 +03:00
commit 235cd4d8e2
9 changed files with 109 additions and 23 deletions

View file

@ -36,8 +36,9 @@ bogus_wallet_data = os.getenv('MMGEN_BOGUS_WALLET_DATA')
disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT')
from decimal import Decimal
tx_fee = Decimal('0.00005')
tx_fee = Decimal('0.0003')
max_tx_fee = Decimal('0.01')
tx_confs = 3
seed_len = 256
http_timeout = 60
@ -49,7 +50,7 @@ prog_name = os.path.basename(sys.argv[0])
author = 'Philemon'
email = '<mmgen-py@yandex.com>'
Cdates = '2013-2016'
version = '0.8.3'
version = '0.8.4'
required_opts = [
'quiet','verbose','debug','outdir','echo_passphrase','passwd_file',
@ -87,7 +88,7 @@ default_wordlist = 'electrum'
#default_wordlist = 'tirosh'
# Global value sets user opt
dfl_vars = 'seed_len','hash_preset','usr_randchars','debug'
dfl_vars = 'seed_len','hash_preset','usr_randchars','debug','tx_fee','tx_confs'
seed_lens = 128,192,256
mn_lens = [i / 32 * 3 for i in seed_lens]

View file

@ -35,9 +35,10 @@ opts_data = {
'options': """
-h, --help Print this help message
-c, --comment-file= f Source the transaction's comment from file 'f'
-C, --tx-confs= c Estimated confirmations (default: {g.tx_confs})
-d, --outdir= d Specify an alternate directory 'd' for output
-e, --echo-passphrase Print passphrase to screen when typing it
-f, --tx-fee= f Transaction fee (default: {g.tx_fee} BTC)
-f, --tx-fee= f Transaction fee (default: {g.tx_fee} BTC (but see below))
-i, --info Display unspent outputs and exit
-q, --quiet Suppress warnings; overwrite files without
prompting
@ -48,6 +49,11 @@ opts_data = {
Transaction inputs are chosen from a list of the user's unpent outputs
via an interactive menu.
If not specified by the user, transaction fees are calculated using
bitcoind's "estimatefee" function for the default (or user-specified)
number of confirmations. Only if "estimatefee" fails is the default fee
of {g.tx_fee} BTC used.
Ages of transactions are approximate based on an average block creation
interval of {g.mins_per_block} minutes.
@ -104,9 +110,9 @@ was specified.
def format_unspent_outputs_for_printing(out,sort_info,total):
pfs = ' %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s'
pout = [pfs % ('Num','TX id,Vout','Address','{pnm} ID'.format(pnm=pnm),
'Amount (BTC)','Conf.','Age (days)', 'Comment')]
pfs = ' %-4s %-67s %-34s %-14s %-12s %-8s %-6s %s'
pout = [pfs % ('Num','Tx ID,Vout','Address','{pnm} ID'.format(pnm=pnm),
'Amount(BTC)','Conf.','Age(d)', 'Comment')]
for n,i in enumerate(out):
addr = '=' if i['skip'] == 'addr' and 'grouped' in sort_info else i['address']
@ -114,7 +120,7 @@ def format_unspent_outputs_for_printing(out,sort_info,total):
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['confirmations'],i['days'],i['comment'])
i['mmid'],i['amt'].strip(),i['confirmations'],i['days'],i['comment'])
pout.append(s.rstrip())
return \
@ -317,6 +323,50 @@ def make_b2m_map(inputs_data,tx_out,ail_w,ail_f):
d.update(ail_f.make_reverse_dict(tx_out.keys()))
return d
def get_fee_estimate():
if 'tx_fee' in opt.set_by_user:
return None
else:
ret = c.estimatefee(opt.tx_confs)
if ret != -1:
return ret
else:
m = """
Fee estimation failed!
Your possible courses of action (from best to worst):
1) Re-run script with a different '--tx-confs' parameter (now '{c}')
2) Re-run script with the '--tx-fee' option (specify fee manually)
3) Accept the global default fee of {f} BTC
Accept the global default fee of {f} BTC?
""".format(c=opt.tx_confs,f=opt.tx_fee).strip()
if keypress_confirm(m):
return None
else:
die(1,'Exiting at user request')
# see: https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
def get_tx_size_and_fee(inputs,outputs):
tx_size = len(inputs)*180 + len(outputs)*34 + 10
if fee_estimate:
ftype,fee = 'Calculated','{:.8f}'.format(fee_estimate * tx_size / 1024)
else:
ftype,fee = 'User-selected',opt.tx_fee
if not keypress_confirm('{} TX fee: {} BTC. OK?'.format(ftype,fee),default_yes=True):
while True:
ufee = my_raw_input('Enter transaction fee: ')
if normalize_btc_amt(ufee):
if Decimal(ufee) > g.max_tx_fee:
msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,g.max_tx_fee))
else:
fee = ufee
break
vmsg('Inputs:{} Outputs:{} TX size:{}'.format(len(sel_unspent),len(tx_out),tx_size))
vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs))
vmsg('TX fee: {}'.format(fee))
return tx_size,normalize_btc_amt(fee)
# main(): execution begins here
cmd_args = opts.init(opts_data)
if opt.comment_file:
@ -367,10 +417,11 @@ if not opt.info:
if not tx_out:
die(2,'At least one output must be specified on the command line')
tx_fee = (g.tx_fee,opt.tx_fee)[bool(opt.tx_fee)]
tx_fee = normalize_btc_amt(tx_fee)
if tx_fee > g.max_tx_fee:
die(2,'Transaction fee too large: %s > %s' % (tx_fee,g.max_tx_fee))
if opt.tx_fee > g.max_tx_fee:
die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,g.max_tx_fee))
fee_estimate = get_fee_estimate()
if g.bogus_wallet_data: # for debugging purposes only
us = eval(get_data_from_file(g.bogus_wallet_data))
@ -404,15 +455,16 @@ while True:
sel_unspent = [unspent[i-1] for i in sel_nums]
mmaddrs = set([i['mmid'] for i in sel_unspent])
mmaddrs.discard('')
if mmaddrs and len(mmaddrs) < len(sel_unspent):
if '' in mmaddrs and len(mmaddrs) > 1:
mmaddrs.discard('')
msg(wmsg['mixed_inputs'] % ', '.join(sorted(mmaddrs)))
if not keypress_confirm('Accept?'):
continue
total_in = trim_exponent(sum([i['amount'] for i in sel_unspent]))
change = trim_exponent(total_in - (send_amt + tx_fee))
tx_size,tx_fee = get_tx_size_and_fee(sel_unspent,tx_out)
change = trim_exponent(total_in - (send_amt + tx_fee))
if change >= 0:
prompt = 'Transaction produces %s BTC in change. OK?' % change

View file

@ -110,6 +110,7 @@ class BitcoinRPCConnection(object):
rpcmethods = (
'estimatefee',
'getinfo',
'getbalance',
'getaddressesbyaccount',

View file

@ -46,6 +46,21 @@ def mk_tmpdir(cfg):
if e.errno != 17: raise
else: msg("Created directory '%s'" % cfg['tmpdir'])
def mk_tmpdir_path(path,cfg):
try:
name = os.path.split(cfg['tmpdir'])[-1]
src = os.path.join(path,name)
try:
os.unlink(cfg['tmpdir'])
except OSError as e:
if e.errno != 2: raise
finally:
os.mkdir(src)
os.symlink(src,cfg['tmpdir'])
except OSError as e:
if e.errno != 17: raise
else: msg("Created directory '%s'" % cfg['tmpdir'])
def get_tmpfile_fn(cfg,fn):
return os.path.join(cfg['tmpdir'],fn)

View file

@ -417,7 +417,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
addrs[key][2] = addr[0]
if not addrs:
die(1,('No addresses with balances!','No tracked addresses!')[showempty])
die(0,('No addresses with balances!','No tracked addresses!')[showempty])
fs = '%-{}s %-{}s %-{}s %s'.format(
max(len(k) for k in addrs),

View file

@ -42,7 +42,7 @@ def normalize_btc_amt(amt):
try:
ret = Decimal(amt)
except:
msg('%s: Invalid amount' % amt)
msg('%s: Invalid BTC amount' % amt)
return False
dmsg('Decimal(amt): %s\nAs tuple: %s' % (amt,repr(ret.as_tuple())))
@ -146,7 +146,7 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False,pause
fs = (
'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n',
'Transaction {} - {} BTC - {} GMT\n'
'Transaction {} - {} BTC - {} UTC\n'
)[bool(terse)]
out = fs.format(*metadata)

View file

@ -21,7 +21,7 @@ from distutils.core import setup
setup(
name = 'mmgen',
description = 'A complete Bitcoin cold-storage solution for the command line',
version = '0.8.3',
version = '0.8.4',
author = 'Philemon',
author_email = 'mmgen-py@yandex.com',
url = 'https://github.com/mmgen/mmgen',

View file

@ -559,7 +559,7 @@ def my_expect(p,s,t='',delay=send_delay,regex=False,nonl=False):
if s == '': ret = 0
else:
f = (p.expect_exact,p.expect)[bool(regex)]
ret = f(s,timeout=3)
ret = f(s,timeout=60)
except pexpect.TIMEOUT:
errmsg(red('\nERROR. Expect %s%s%s timed out. Exiting' % (quo,s,quo)))
sys.exit(1)
@ -1141,7 +1141,8 @@ class MMGenTestSuite(object):
t.expect('Enter a range or space-separated list of outputs to spend: ',
' '.join([str(i) for i in outputs_list])+'\n')
if non_mmgen_input: t.expect('Accept? (y/N): ','y')
t.expect('OK? (Y/n): ','y')
t.expect('OK? (Y/n): ','y') # fee OK?
t.expect('OK? (Y/n): ','y') # change OK?
t.expect('Add a comment to transaction? (y/N): ','\n')
t.tx_view()
t.expect('Save transaction? (y/N): ','y')
@ -1757,7 +1758,23 @@ if opt.pause:
start_time = int(time.time())
ts = MMGenTestSuite()
for cfg in sorted(cfgs): mk_tmpdir(cfgs[cfg])
# Laggy flash media cause pexpect to crash, so read and write all temporary
# files to volatile memory in '/dev/shm'
if sys.platform[:3] == 'win':
for cfg in sorted(cfgs): mk_tmpdir(cfgs[cfg])
else:
d,pfx = '/dev/shm','mmgen-test-'
try:
import subprocess
subprocess.call('rm -rf %s/%s*'%(d,pfx),shell=True)
except Exception as e:
die(2,'Unable to delete directory tree %s/%s* (%s)'%(d,pfx,e))
try:
import tempfile
shm_dir = tempfile.mkdtemp('',pfx,d)
except Exception as e:
die(2,'Unable to create temporary directory in %s (%s)'%(d,e))
for cfg in sorted(cfgs): mk_tmpdir_path(shm_dir,cfgs[cfg])
try:
if cmd_args:

View file

@ -232,7 +232,7 @@ class MMGenToolTestSuite(object):
ret = self.run_cmd(name,([],[carg])[bool(carg)],kwargs=kwargs,extra_msg=extra_msg)
if carg: vmsg('In: ' + repr(carg))
vmsg('Out: ' + (repr(ret),ret)[literal])
if ret:
if ret or ret == '':
write_to_tmpfile(cfg,'%s%s.out' % (name,fn_idx),ret+'\n')
if chkdata:
cmp_or_die(ret,chkdata)