Dynamic transaction fees
This commit is contained in:
parent
72a85e3db8
commit
235cd4d8e2
9 changed files with 109 additions and 23 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ class BitcoinRPCConnection(object):
|
|||
|
||||
|
||||
rpcmethods = (
|
||||
'estimatefee',
|
||||
'getinfo',
|
||||
'getbalance',
|
||||
'getaddressesbyaccount',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -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',
|
||||
|
|
|
|||
23
test/test.py
23
test/test.py
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue