minor changes and fixes throughout

This commit is contained in:
The MMGen Project 2020-05-10 13:39:53 +00:00
commit 57f12cd1cb
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
26 changed files with 312 additions and 197 deletions

View file

@ -2,6 +2,7 @@ include README.md SIGNING_KEYS.pub LICENSE INSTALL
include doc/wiki/using-mmgen/*
include test/*.py
include test/include/*.py
include test/test_py_d/*.py
include test/objtest_py_d/*.py
include test/objattrtest_py_d/*.py

View file

@ -67,7 +67,10 @@ class Token(MMGenObject): # ERC20
if g.debug:
msg('ETH_CALL {}: {}'.format(method_sig,'\n '.join(parse_abi(data))))
ret = g.rpch.eth_call({ 'to': '0x'+self.addr, 'data': '0x'+data })
return int(ret,16) * self.base_unit if toUnit else ret
if toUnit:
return int(ret,16) * self.base_unit
else:
return ret
def balance(self,acct_addr):
return ETHAmt(self.do_call('balanceOf(address)',acct_addr.rjust(64,'0'),toUnit=True))
@ -98,11 +101,11 @@ class Token(MMGenObject): # ERC20
def info(self):
fs = '{:15}{}\n' * 5
return fs.format('token address:',self.addr,
'token symbol:',self.symbol(),
'token name:',self.name(),
'decimals:',self.decimals(),
'total supply:',self.total_supply())
return fs.format('token address:', self.addr,
'token symbol:', self.symbol(),
'token name:', self.name(),
'decimals:', self.decimals(),
'total supply:', self.total_supply())
def code(self):
return g.rpch.eth_getCode('0x'+self.addr)[2:]

View file

@ -95,9 +95,9 @@ class EthereumTrackingWallet(TrackingWallet):
r = self.data_root
if addr in r:
if not r[addr]['mmid'] and label.mmid:
msg("Warning: MMGen ID '{}' was missing in tracking wallet!".format(label.mmid))
msg(f'Warning: MMGen ID {label.mmid!r} was missing in tracking wallet!')
elif r[addr]['mmid'] != label.mmid:
die(3,"MMGen ID '{}' does not match tracking wallet!".format(label.mmid))
die(3,'MMGen ID {label.mmid!r} does not match tracking wallet!')
r[addr] = { 'mmid': label.mmid, 'comment': label.comment }
@write_mode
@ -153,13 +153,14 @@ class EthereumTrackingWallet(TrackingWallet):
if self.data['tokens'][addr]['params'].get('symbol') == sym.upper():
return addr
if no_rpc: return None
if no_rpc:
return None
for addr in self.data['tokens']:
if Token(addr).symbol().upper() == sym.upper():
self.force_set_token_param(addr,'symbol',sym.upper())
return addr
else:
return None
def get_token_param(self,token,param):
@ -180,6 +181,7 @@ class EthereumTrackingWallet(TrackingWallet):
class EthereumTokenTrackingWallet(EthereumTrackingWallet):
desc = 'Ethereum token tracking wallet'
decimals = None
symbol = None
cur_eth_balances = {}
@ -315,7 +317,8 @@ class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
prompt_fs = 'Total to spend: {} {}\n\n'
col_adj = 37
def get_display_precision(self): return 10 # truncate precision for narrow display
def get_display_precision(self):
return 10 # truncate precision for narrow display
def get_unspent_data(self):
super().get_unspent_data()
@ -336,11 +339,12 @@ class EthereumTwAddrList(TwAddrList):
for mmid,d in list(tw_dict.items()):
# if d['confirmations'] < minconf: continue # cannot get confirmations for eth account
label = TwLabel(mmid+' '+d['comment'],on_fail='raise')
if usr_addr_list and (label.mmid not in usr_addr_list): continue
if usr_addr_list and (label.mmid not in usr_addr_list):
continue
bal = self.wallet.get_balance(d['addr'])
if bal == 0 and not showempty:
if not label.comment: continue
if not all_labels: continue
if not label.comment or not all_labels:
continue
self[label.mmid] = {'amt': g.proto.coin_amt('0'), 'lbl': label }
if showbtcaddrs:
self[label.mmid]['addr'] = CoinAddr(d['addr'])

View file

@ -118,7 +118,7 @@ class EthereumMMGenTX(MMGenTX):
self.tx_gas = o['startGas'] # approximate, but better than nothing
self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
self.txobj = o
return d # 'token_addr','decimals' required by subclass
return d # 'token_addr','decimals' required by Token subclass
def get_nonce(self):
return ETHNonce(int(g.rpch.parity_nextNonce('0x'+self.inputs[0].addr),16))
@ -149,7 +149,8 @@ class EthereumMMGenTX(MMGenTX):
self.hex = json.dumps(odict)
self.update_txid()
def del_output(self,idx): pass
def del_output(self,idx):
pass
def update_txid(self):
assert not is_hex_str(self.hex),'update_txid() must be called only when self.hex is not hex data'
@ -160,12 +161,14 @@ class EthereumMMGenTX(MMGenTX):
def process_cmd_args(self,cmd_args,ad_f,ad_w):
lc = len(cmd_args)
if lc == 0 and self.usr_contract_data and not 'Token' in type(self).__name__: return
if lc == 0 and self.usr_contract_data and not 'Token' in type(self).__name__:
return
if lc != 1:
fs = '{} output{} specified, but Ethereum transactions must have exactly one'
die(1,fs.format(lc,suf(lc)))
for a in cmd_args: self.process_cmd_arg(a,ad_f,ad_w)
for a in cmd_args:
self.process_cmd_arg(a,ad_f,ad_w)
def select_unspent(self,unspent):
prompt = 'Enter an account to spend from: '
@ -366,7 +369,8 @@ class EthereumMMGenTX(MMGenTX):
self.get_status()
if prompt_user: self.confirm_send()
if prompt_user:
self.confirm_send()
ret = None if g.bogus_send else g.rpch.eth_sendRawTransaction('0x'+self.hex,on_fail='return')
@ -374,11 +378,14 @@ class EthereumMMGenTX(MMGenTX):
if rpc_error(ret):
msg(yellow(rpc_errmsg(ret)))
msg(red('Send of MMGen transaction {} failed'.format(self.txid)))
if exit_on_fail: sys.exit(1)
if exit_on_fail:
sys.exit(1)
return False
else:
m = 'BOGUS transaction NOT sent: {}' if g.bogus_send else 'Transaction sent: {}'
if not g.bogus_send:
if g.bogus_send:
m = 'BOGUS transaction NOT sent: {}'
else:
m = 'Transaction sent: {}'
assert ret == '0x'+self.coin_txid,'txid mismatch (after sending)'
self.desc = 'sent transaction'
msg(m.format(self.coin_txid.hl()))

View file

@ -56,6 +56,8 @@ class Daemon(MMGenObject):
def exec_cmd(self,cmd,check):
cp = run(cmd,check=False,stdout=PIPE,stderr=PIPE)
if self.debug:
print(cp)
if check and cp.returncode != 0:
raise MMGenCalledProcessError(cp)
return cp
@ -172,8 +174,8 @@ class Daemon(MMGenObject):
return True
time.sleep(0.2)
else:
m = 'Wait for state {!r} timeout exceeded for daemon {} {}'
die(2,m.format(req_state,self.daemon_id.upper(),self.network))
m = 'Wait for state {!r} timeout exceeded for daemon {} {} (port {})'
die(2,m.format(req_state,self.daemon_id.upper(),self.network,self.rpc_port))
@classmethod
def check_implement(cls):
@ -202,6 +204,13 @@ class Daemon(MMGenObject):
die(1,'Flag {!r} not set, so cannot be removed'.format(val))
self._flags.remove(val)
def remove_datadir(self):
if self.state == 'stopped':
import shutil
shutil.rmtree(self.datadir,ignore_errors=True)
else:
msg(f'Cannot remove {self.datadir!r} - daemon is not stopped')
class MoneroWalletDaemon(Daemon):
desc = 'RPC daemon'

View file

@ -40,8 +40,11 @@ if os.getenv('MMGEN_DEBUG') or os.getenv('MMGEN_TEST_SUITE') or os.getenv('MMGEN
class MMGenObject(object):
# Pretty-print any object subclassed from MMGenObject, recursing into sub-objects - WIP
def pmsg(self): print(self.pfmt())
def pdie(self): print(self.pfmt()); sys.exit(0)
def pmsg(self):
print(self.pfmt())
def pdie(self):
print(self.pfmt())
sys.exit(1)
def pfmt(self,lvl=0,id_list=[]):
scalars = (str,int,float,Decimal)
def do_list(out,e,lvl=0,is_dict=False):

View file

@ -37,6 +37,13 @@ class g(object):
if s: sys.stderr.write(s+'\n')
sys.exit(ev)
for k in ('linux','win','msys'):
if sys.platform[:len(k)] == k:
platform = { 'linux':'linux', 'win':'win', 'msys':'win' }[k]
break
else:
die(1,"'{}': platform not supported by {}\n".format(sys.platform,proj_name))
# Constants:
version = '0.12.099'
@ -89,7 +96,7 @@ class g(object):
use_internal_keccak_module = False
chain = None # set by first call to rpc_init()
chains = 'mainnet','testnet','regtest'
chains = ('mainnet','testnet','regtest')
# rpc:
rpc_host = ''
@ -118,13 +125,6 @@ class g(object):
mnemonic_entry_modes = {}
for k in ('linux','win','msys'):
if sys.platform[:len(k)] == k:
platform = { 'linux':'linux', 'win':'win', 'msys':'win' }[k]
break
else:
die(1,"'{}': platform not supported by {}\n".format(sys.platform,proj_name))
color = sys.stdout.isatty()
if os.getenv('HOME'): # Linux or MSYS
@ -154,7 +154,8 @@ class g(object):
# 'long' opts - opt sets global var
common_opts = (
'color','no_license','rpc_host','rpc_port','testnet','rpc_user','rpc_password',
'color','no_license','testnet',
'rpc_host','rpc_port','rpc_user','rpc_password',
'monero_wallet_rpc_host','monero_wallet_rpc_user','monero_wallet_rpc_password',
'daemon_data_dir','force_256_color','regtest','coin','bob','alice',
'accept_defaults','token'

View file

@ -33,6 +33,7 @@ key_fn = 'autosign.key'
from .common import *
prog_name = os.path.basename(sys.argv[0])
opts_data = {
'sets': [('stealth_led', True, 'led', True)],
'text': {
'desc': 'Auto-sign MMGen transactions',
'usage':'[opts] [command]',
@ -43,6 +44,7 @@ opts_data = {
-I, --no-insert-check Don't check for device insertion
-l, --led Use status LED to signal standby, busy and error
-m, --mountpoint=m Specify an alternate mountpoint (default: '{mp}')
-n, --no-summary Don't print a transaction summary
-s, --stealth-led Stealth LED mode - signal busy and error only, and only
after successful authorization.
-S, --full-summary Print a full summary of each signed transaction after
@ -116,9 +118,9 @@ from .protocol import CoinProtocol,init_coin
if g.test_suite:
from .daemon import CoinDaemon
if opt.stealth_led: opt.led = True
if opt.mountpoint:
mountpoint = opt.mountpoint # TODO: make global
if opt.mountpoint: mountpoint = opt.mountpoint # TODO: make global
opt.outdir = tx_dir = os.path.join(mountpoint,'tx')
def check_daemons_running():
@ -132,47 +134,44 @@ def check_daemons_running():
for coin in coins:
g.proto = CoinProtocol(coin,g.testnet)
if g.proto.sign_mode != 'daemon':
continue
if g.proto.sign_mode == 'daemon':
if g.test_suite:
g.proto.daemon_data_dir = 'test/daemons/' + coin.lower()
g.rpc_port = CoinDaemon(get_network_id(coin,g.testnet),test_suite=True).rpc_port
vmsg('Checking {} daemon'.format(coin))
vmsg(f'Checking {coin} daemon')
try:
rpc_init(reinit=True)
g.rpch.getblockcount()
except SystemExit as e:
if e.code != 0:
fs = '{} daemon not running or not listening on port {}'
ydie(1,fs.format(coin,g.proto.rpc_port))
ydie(1,f'{coin} daemon not running or not listening on port {g.proto.rpc_port}')
def get_wallet_files():
m = "Cannot open wallet directory '{}'. Did you run 'mmgen-autosign setup'?"
try: dlist = os.listdir(wallet_dir)
except: die(1,m.format(wallet_dir))
try:
dlist = os.listdir(wallet_dir)
except:
die(1,f"Cannot open wallet directory {wallet_dir!r}. Did you run 'mmgen-autosign setup'?")
wfs = [x for x in dlist if x[-6:] == '.mmdat']
if not wfs:
fns = [x for x in dlist if x.endswith('.mmdat')]
if fns:
return [os.path.join(wallet_dir,w) for w in fns]
else:
die(1,'No wallet files present!')
return [os.path.join(wallet_dir,w) for w in wfs]
def do_mount():
if not os.path.ismount(mountpoint):
if run(['mount',mountpoint],stderr=DEVNULL,stdout=DEVNULL).returncode == 0:
msg('Mounting '+mountpoint)
msg(f'Mounting {mountpoint}')
try:
ds = os.stat(tx_dir)
m1 = "'{}' is not a directory!"
m2 = "'{}' is not read/write for this user!"
assert S_ISDIR(ds.st_mode),m1.format(tx_dir)
assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR,m2.format(tx_dir)
assert S_ISDIR(ds.st_mode), f'{tx_dir!r} is not a directory!'
assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR,f'{tx_dir!r} is not read/write for this user!'
except:
die(1,'{} missing, or not read/writable by user!'.format(tx_dir))
die(1,'{tx_dir!r} missing, or not read/writable by user!')
def do_umount():
if os.path.ismount(mountpoint):
run(['sync'],check=True)
msg('Unmounting '+mountpoint)
msg(f'Unmounting {mountpoint}')
run(['umount',mountpoint],check=True)
def sign_tx_file(txfile,signed_txs):
@ -187,8 +186,8 @@ def sign_tx_file(txfile,signed_txs):
init_coin(tmp_tx.coin,testnet=True)
if hasattr(g.proto,'chain_name'):
m = 'Chains do not match! tx file: {}, proto: {}'
assert tmp_tx.chain == g.proto.chain_name,m.format(tmp_tx.chain,g.proto.chain_name)
if tmp_tx.chain != g.proto.chain_name:
die(2, f'Chains do not match! tx file: {tmp_tx.chain}, proto: {g.proto.chain_name}')
g.chain = tmp_tx.chain
g.token = tmp_tx.dcoin
@ -209,18 +208,17 @@ def sign_tx_file(txfile,signed_txs):
else:
return False
except Exception as e:
msg('An error occurred: {}'.format(e.args[0]))
msg(f'An error occurred: {e.args[0]}')
if g.debug or g.traceback:
print_stack_trace('AUTOSIGN {}'.format(txfile))
print_stack_trace(f'AUTOSIGN {txfile}')
return False
except:
return False
def sign():
dirlist = os.listdir(tx_dir)
raw = [f for f in dirlist if f[-6:] == '.rawtx']
signed = [f[:-6] for f in dirlist if f[-6:] == '.sigtx']
unsigned = [os.path.join(tx_dir,f) for f in raw if f[:-6] not in signed]
raw,signed = [set(f[:-6] for f in dirlist if f.endswith(ext)) for ext in ('.rawtx','.sigtx')]
unsigned = [os.path.join(tx_dir,f+'.rawtx') for f in raw - signed]
if unsigned:
signed_txs,fails = [],[]
@ -233,10 +231,10 @@ def sign():
msg('{} transaction{} signed'.format(len(signed_txs),suf(signed_txs)))
if fails:
rmsg('{} transaction{} failed to sign'.format(len(fails),suf(fails)))
if signed_txs:
if signed_txs and not opt.no_summary:
print_summary(signed_txs)
if fails:
rmsg('{}Failed transactions:'.format('' if opt.full_summary else '\n'))
rmsg('\nFailed transactions:')
rmsg(' ' + '\n '.join(sorted(fails)) + '\n')
return False if fails else True
else:
@ -248,7 +246,6 @@ def decrypt_wallets():
opt.hash_preset = '1'
opt.set_by_user = ['hash_preset']
opt.passwd_file = os.path.join(tx_dir,key_fn)
# opt.passwd_file = '/tmp/key'
from .wallet import Wallet
msg("Unlocking wallet{} with key from '{}'".format(suf(wfs),opt.passwd_file))
fails = 0
@ -261,43 +258,52 @@ def decrypt_wallets():
return False if fails else True
def print_summary(signed_txs):
if opt.full_summary:
bmsg('\nAutosign summary:\n')
def gen():
for tx in signed_txs:
init_coin(tx.coin,tx.chain == 'testnet')
msg_r(tx.format_view(terse=True))
yield tx.format_view(terse=True)
msg_r(''.join(gen()))
return
body = []
def gen():
for tx in signed_txs:
non_mmgen = [o for o in tx.outputs if not o.mmid]
if non_mmgen:
body.append((tx,non_mmgen))
yield (tx,non_mmgen)
body = list(gen())
if body:
bmsg('\nAutosign summary:')
fs = '{} {} {}'
t_wid,a_wid = 6,44
msg(fs.format('TX ID ','Non-MMGen outputs'+' '*(a_wid-17),'Amount'))
msg(fs.format('-'*t_wid, '-'*a_wid, '-'*7))
def gen():
yield fs.format('TX ID ','Non-MMGen outputs'+' '*(a_wid-17),'Amount')
yield fs.format('-'*t_wid, '-'*a_wid, '-'*7)
for tx,non_mmgen in body:
for nm in non_mmgen:
msg(fs.format(
yield fs.format(
tx.txid.fmt(width=t_wid,color=True) if nm is non_mmgen[0] else ' '*t_wid,
nm.addr.fmt(width=a_wid,color=True),
nm.amt.hl() + ' ' + yellow(tx.coin)))
nm.amt.hl() + ' ' + yellow(tx.coin))
msg('\n'.join(gen()))
else:
msg('No non-MMGen outputs')
def do_sign():
if not opt.stealth_led: set_led('busy')
if not opt.stealth_led:
set_led('busy')
do_mount()
key_ok = decrypt_wallets()
if key_ok:
if opt.stealth_led: set_led('busy')
if opt.stealth_led:
set_led('busy')
ret = sign()
do_umount()
set_led(('standby','off','error')[(not ret)*2 or bool(opt.stealth_led)])
@ -305,7 +311,8 @@ def do_sign():
else:
msg('Password is incorrect!')
do_umount()
if not opt.stealth_led: set_led('error')
if not opt.stealth_led:
set_led('error')
return False
def wipe_existing_key():
@ -397,7 +404,8 @@ def set_led(cmd):
led_thread.start()
def get_insert_status():
if opt.no_insert_check: return True
if opt.no_insert_check:
return True
try: os.stat(os.path.join('/dev/disk/by-label',part_label))
except: return False
else: return True
@ -471,11 +479,12 @@ if len(cmd_args) not in (0,1):
opts.usage()
if len(cmd_args) == 1:
if cmd_args[0] in ('gen_key','setup'):
globals()[cmd_args[0]]()
cmd = cmd_args[0]
if cmd in ('gen_key','setup'):
globals()[cmd]()
sys.exit(0)
elif cmd_args[0] != 'wait':
die(1,"'{}': unrecognized command".format(cmd_args[0]))
elif cmd != 'wait':
die(1,f'{cmd!r}: unrecognized command')
check_wipe_present()
wfs = get_wallet_files()

View file

@ -93,7 +93,9 @@ cmd_args = opts.init(opts_data,add_opts=['hidden_incog_input_params','in_fmt','u
g.use_cached_balances = opt.cached_balances
if len(cmd_args) < 1: opts.usage()
if len(cmd_args) < 1:
opts.usage()
cmd = cmd_args.pop(0)
import mmgen.tool as tool

View file

@ -44,9 +44,11 @@ rpc_init()
if len(cmd_args) == 1:
infile = cmd_args[0]; check_infile(infile)
else: opts.usage()
else:
opts.usage()
if not opt.status: do_license_msg()
if not opt.status:
do_license_msg()
from .tx import *

View file

@ -91,8 +91,11 @@ column below:
infiles = opts.init(opts_data,add_opts=['b16'])
if not infiles: opts.usage()
for i in infiles: check_infile(i)
if not infiles:
opts.usage()
for i in infiles:
check_infile(i)
if g.proto.sign_mode == 'daemon':
rpc_init()

View file

@ -112,7 +112,8 @@ def override_globals_from_cfg_file(ucfg):
def override_globals_from_env():
for name in g.env_opts:
if name == 'MMGEN_DEBUG_ALL': continue
if name == 'MMGEN_DEBUG_ALL':
continue
disable = name[:14] == 'MMGEN_DISABLE_'
val = os.getenv(name) # os.getenv() returns None if env var is unset
if val: # exclude empty string values; string value of '0' or 'false' sets variable to False
@ -528,9 +529,9 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
def check_and_set_autoset_opts(): # Raises exception if any check fails
def nocase_str(key,val,asd):
if val.lower() in asd.choices:
return True
else:
try:
return asd.choices.index(val)
except:
return 'one of'
def nocase_pfx(key,val,asd):

View file

@ -67,6 +67,8 @@ def _b58chk_decode(s):
raise ValueError('_b58chk_decode(): incorrect checksum')
return out[:-4]
finfo = namedtuple('fork_info',['height','hash','name','replayable'])
# chainparams.cpp
class BitcoinProtocol(MMGenObject):
name = 'bitcoin'
@ -87,9 +89,9 @@ class BitcoinProtocol(MMGenObject):
daemon_data_subdir = ''
sighash_type = 'ALL'
block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
forks = [ # height, hash, name, replayable
(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch',False),
(None,'','b2x',True)
forks = [
finfo(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','BCH',False),
finfo(None,'','B2X',True),
]
caps = ('rbf','segwit')
mmcaps = ('key','addr','rpc','tx')
@ -241,7 +243,7 @@ class BitcoinCashProtocol(BitcoinProtocol):
mmtypes = ('L','C')
sighash_type = 'ALL|FORKID'
forks = [
(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','btc',False)
finfo(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','BTC',False)
]
caps = ()
coin_amt = BCHAmt
@ -267,7 +269,7 @@ class B2XProtocol(BitcoinProtocol):
coin_amt = B2XAmt
max_tx_fee = B2XAmt('0.1')
forks = [
(None,'','btc',True) # activation: 494784
finfo(None,'','BTC',True) # activation: 494784
]
class B2XTestnetProtocol(B2XProtocol):

View file

@ -264,9 +264,11 @@ class MMGenToolCmdMeta(type):
def __contains__(cls,val):
return cls.methods.__contains__(val)
def classname(cls,cmd_name):
return cls.methods[cmd_name].__qualname__.split('.')[0]
def call(cls,cmd_name,*args,**kwargs):
subcls = cls.classes[cls.methods[cmd_name].__qualname__.split('.')[0]]
return getattr(subcls(),cmd_name)(*args,**kwargs)
return getattr(cls.classes[cls.classname(cmd_name)](),cmd_name)(*args,**kwargs)
@property
def user_commands(cls):
@ -909,7 +911,10 @@ class MMGenToolCmdRPC(MMGenToolCmds):
twuo.do_sort(sort,reverse=reverse)
twuo.age_fmt = age_fmt
twuo.show_mmid = show_mmid
ret = twuo.format_for_printing(color=True,show_confs=wide_show_confs) if wide else twuo.format_for_display()
if wide:
ret = twuo.format_for_printing(color=True,show_confs=wide_show_confs)
else:
ret = twuo.format_for_display()
del twuo.wallet
return ret
@ -940,7 +945,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
Note that the use of these commands requires private data to be exposed on
a network-connected machine in order to unlock the Monero wallets. This is
a violation of MMGen's security policy.
a violation of good security practice.
"""
_monero_chain_height = None
@ -999,7 +1004,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
restore_height = blockheight,
language = 'English' )
pp_msg(ret) if opt.verbose else msg(' Address: {}'.format(ret['address']))
pp_msg(ret) if opt.debug else msg(' Address: {}'.format(ret['address']))
return True
def sync(n,d,fn,c,m):
@ -1043,7 +1048,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
from .obj import XMRAmt
bals[fn] = tuple([XMRAmt(ret[k],from_unit='min_coin_unit') for k in ('balance','unlocked_balance')])
if opt.verbose:
if opt.debug:
pp_msg(ret)
else:
msg(' Balance: {} Unlocked balance: {}'.format(*[b.hl() for b in bals[fn]]))

View file

@ -161,7 +161,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
tr_rpc = []
lbl_id = ('account','label')['label_api' in g.rpch.caps]
for o in us_rpc:
if not lbl_id in o: continue # coinbase outputs have no account field
if not lbl_id in o:
continue # coinbase outputs have no account field
l = get_tw_label(o[lbl_id])
if l:
o.update({
@ -172,6 +173,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
'confs': o['confirmations']
})
tr_rpc.append(o)
self.unspent = self.MMGenTwOutputList(
self.MMGenTwUnspentOutput(
**{k:v for k,v in o.items() if k in dir(self.MMGenTwUnspentOutput)}
@ -668,14 +670,19 @@ class TrackingWallet(MMGenObject):
del tw
atexit.register(del_tw,self)
# TrackingWallet instances must be explicitly destroyed with 'del tw', 'del twuo.wallet'
# and the like to ensure the instance is deleted and wallet is written before global
# vars are destroyed by interpreter at shutdown.
# This is especially important, as exceptions are ignored within __del__():
# /usr/share/doc/python3.6-doc/html/reference/datamodel.html#object.__del__
# This code can only be debugged by examining the program output. Since no exceptions
# are raised, errors will not be caught by the test suite.
def __del__(self):
"""
TrackingWallet instances opened in write or import mode must be explicitly destroyed
with 'del tw', 'del twuo.wallet' and the like to ensure the instance is deleted and
wallet is written before global vars are destroyed by the interpreter at shutdown.
Not that this code can only be debugged by examining the program output, as exceptions
are ignored within __del__():
/usr/share/doc/python3.6-doc/html/reference/datamodel.html#object.__del__
Since no exceptions are raised, errors will not be caught by the test suite.
"""
if g.debug:
print_stack_trace('TW DEL {!r}'.format(self))
@ -684,7 +691,8 @@ class TrackingWallet(MMGenObject):
elif g.debug:
msg('read-only wallet, doing nothing')
def upgrade_wallet_maybe(self): pass
def upgrade_wallet_maybe(self):
pass
@staticmethod
def conv_types(ad):
@ -825,12 +833,14 @@ class TrackingWallet(MMGenObject):
from .addr import AddrData
mmaddr = AddrData(source='tw').coinaddr2mmaddr(coinaddr)
if not mmaddr: mmaddr = '{}:{}'.format(g.proto.base_coin.lower(),coinaddr)
if not mmaddr:
mmaddr = '{}:{}'.format(g.proto.base_coin.lower(),coinaddr)
mmaddr = TwMMGenID(mmaddr)
cmt = TwComment(label,on_fail=on_fail)
if cmt in (False,None): return False
if cmt in (False,None):
return False
lbl = TwLabel(mmaddr + ('',' '+cmt)[bool(cmt)],on_fail=on_fail)

View file

@ -330,7 +330,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
if filename:
self.parse_tx_file(filename,metadata_only=metadata_only,quiet_open=quiet_open)
if metadata_only: return
if metadata_only:
return
self.check_pubkey_scripts()
self.check_sigs() # marks the tx as signed
@ -538,8 +539,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
fee_per_kb = ret['feerate'] if 'feerate' in ret else -2
fe_type = 'estimatesmartfee'
except:
fee_per_kb = g.rpch.estimatefee() if g.coin=='BCH' and g.rpch.daemon_version >= 190100 \
else g.rpch.estimatefee(opt.tx_confs)
args = () if g.coin=='BCH' and g.rpc.daemon_version >= 190100 else (opt.tx_confs,)
fee_per_kb = await g.rpc.call('estimatefee',*args)
fe_type = 'estimatefee'
return fee_per_kb,fe_type
@ -982,7 +983,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
m = errmsg
msg(yellow(m))
msg(red('Send of MMGen transaction {} failed'.format(self.txid)))
if exit_on_fail: sys.exit(1)
if exit_on_fail:
sys.exit(1)
return False
else:
if g.bogus_send:
@ -1267,7 +1269,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
desc = 'send amount in metadata'
self.send_amt = g.proto.coin_amt(send_amt,on_fail='raise')
desc = 'transaction hex data'
desc = 'transaction file hex data'
self.check_txfile_hex_data()
# the following ops will all fail if g.coin doesn't match self.coin
desc = 'coin type in metadata'
@ -1457,7 +1459,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
self.twuo.display_total()
if do_info: sys.exit(0)
if do_info:
sys.exit(0)
self.send_amt = self.sum_outputs()
@ -1515,7 +1518,7 @@ class MMGenBumpTX(MMGenTX):
if not self.is_replaceable():
die(1,"Transaction '{}' is not replaceable".format(self.txid))
# If sending, require tx to have been signed
# If sending, require tx to be signed
if send:
if not self.marked_signed():
die(1,"File '{}' is not a signed {} transaction file".format(filename,g.proj_name))
@ -1568,7 +1571,8 @@ class MMGenBumpTX(MMGenTX):
p = 'Fee will be deducted from output {}{} ({} {})'.format(idx+1,cs,o_amt,g.coin)
if check_sufficient_funds(o_amt):
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
if opt.yes: msg(p)
if opt.yes:
msg(p)
self.bump_output_idx = idx
return idx

View file

@ -150,7 +150,8 @@ def txsign(tx,seed_files,kl,kal,tx_num_str=''):
tmp = KeyAddrList(addrlist=non_mm_addrs)
tmp.add_wifs(kl)
m = tmp.list_missing('sec')
if m: die(2,wmsg['missing_keys_error'].format(suf(m,'es'),'\n '.join(m)))
if m:
die(2,wmsg['missing_keys_error'].format(suf(m,'es'),'\n '.join(m)))
keys += tmp.data
if opt.mmgen_keys_from_file:

View file

@ -96,9 +96,9 @@ def pp_fmt(d):
def pp_msg(d):
msg(pp_fmt(d))
def fmt(s,indent=''):
def fmt(s,indent='',strip_char=None):
"de-indent multiple lines of text, or indent with specified string"
return indent + ('\n'+indent).join([l.strip() for l in s.strip().splitlines()]) + '\n'
return indent + ('\n'+indent).join([l.strip(strip_char) for l in s.strip().splitlines()]) + '\n'
def fmt_list(l,fmt='dfl',indent=''):
"pretty-format a list"
@ -608,6 +608,8 @@ def write_data_to_file( outfile,data,desc='data',
d = ''
finally:
if d != cmp_data:
if g.test_suite:
print_diff(cmp_data,d)
m = "{} in file '{}' has been altered by some other program! Aborting file write"
die(3,m.format(desc,outfile))

View file

@ -20,7 +20,7 @@ import sys,os,subprocess
from shutil import copy2
sys_ver = sys.version_info[:2]
req_ver = (3,7)
req_ver = (3,6)
ver2f = lambda t: float('{}.{:03}'.format(*t))
if ver2f(sys_ver) < ver2f(req_ver):

View file

@ -165,13 +165,11 @@ def iqmsg_r(s):
if not opt.quiet: omsg_r(s)
def start_test_daemons(*network_ids):
if hasattr(opt,'no_daemon_autostart') and opt.no_daemon_autostart:
return
if not opt.no_daemon_autostart:
return test_daemons_ops(*network_ids,op='start')
def stop_test_daemons(*network_ids):
if hasattr(opt,'no_daemon_stop') and opt.no_daemon_stop:
return
if not opt.no_daemon_stop:
return test_daemons_ops(*network_ids,op='stop')
def restart_test_daemons(*network_ids):
@ -179,11 +177,10 @@ def restart_test_daemons(*network_ids):
return start_test_daemons(*network_ids)
def test_daemons_ops(*network_ids,op):
if opt.no_daemon_autostart:
return
if not opt.no_daemon_autostart:
from mmgen.daemon import CoinDaemon
silent = not opt.verbose and not (hasattr(opt,'exact_output') and opt.exact_output)
silent = not opt.verbose and not getattr(opt,'exact_output',False)
for network_id in network_ids:
if network_id not in CoinDaemon.network_ids: # silently ignore invalid IDs
if network_id.lower() not in CoinDaemon.network_ids: # silently ignore invalid IDs
continue
CoinDaemon(network_id,test_suite=True).cmd(op,silent=silent)

View file

@ -60,8 +60,8 @@ def create_shm_dir(data_dir,trash_dir):
dest = os.path.join(shm_dir,os.path.basename(trash_dir))
os.mkdir(dest,0o755)
try: os.unlink(trash_dir)
except: pass
run(f'rm -rf {trash_dir}',shell=True,check=True)
os.symlink(dest,trash_dir)
dest = os.path.join(shm_dir,os.path.basename(data_dir))
@ -115,6 +115,7 @@ opts_data = {
-P, --profile Record the execution time of each script
-q, --quiet Produce minimal output. Suppress dependency info
-r, --resume=c Resume at command 'c' after interrupted run
-R, --resume-after=c Same, but resume at command following 'c'
-s, --system Test scripts and modules installed on system rather
than those in the repo root
-S, --skip-deps Skip dependency checking for command
@ -168,13 +169,19 @@ if not ('resume' in _uopts or 'skip_deps' in _uopts):
check_segwit_opts()
if opt.profile: opt.names = True
if opt.resume: opt.skip_deps = True
if opt.profile:
opt.names = True
if opt.exact_output:
def msg(s): pass
qmsg = qmsg_r = vmsg = vmsg_r = msg_r = msg
if opt.resume or opt.resume_after:
opt.skip_deps = True
resume = opt.resume or opt.resume_after
else:
resume = False
cfgs = { # addr_idx_lists (except 31,32,33,34) must contain exactly 8 addresses
'1': { 'wpasswd': 'Dorian-α',
'kapasswd': 'Grok the blockchain',
@ -514,7 +521,9 @@ class CmdGroupMgr(object):
shared_deps are "implied" dependencies for all cmds in cmd_group that don't appear in
the cmd_group data or cmds' argument lists. Supported only for 3seed tests at present.
"""
if not hasattr(cls,'shared_deps'): return []
if not hasattr(cls,'shared_deps'):
return []
return [k for k,v in cfgs[str(tmpdir_idx)]['dep_generators'].items()
if k in cls.shared_deps and v != cmdname]
@ -543,6 +552,7 @@ class CmdGroupMgr(object):
def gm_init_group(self,trunner,gname,spawn_prog):
kwargs = self.cmd_groups[gname][1]
cls = self.create_group(gname,**kwargs)
cls.group_name = gname
return cls(trunner,cfgs,spawn_prog)
def list_cmd_groups(self):
@ -657,13 +667,15 @@ class TestSuiteRunner(object):
else:
omsg_r('Testing {}: '.format(desc))
if msg_only: return
if msg_only:
return
if opt.log:
try:
self.log_fd.write(cmd_disp+'\n')
except:
self.log_fd.write(ascii(cmd_disp)+'\n')
self.log_fd.write('[{}][{}:{}] {}\n'.format(
g.coin.lower(),
self.ts.group_name,
self.ts.test_name,
cmd_disp))
from test.include.pexpect import MMGenPexpect
return MMGenPexpect(args,no_output=no_output)
@ -710,6 +722,11 @@ class TestSuiteRunner(object):
self.ts = self.gm.gm_init_group(self,gname,self.spawn_wrapper)
if opt.resume_after:
global resume
resume = self.gm.cmd_list[self.gm.cmd_list.index(resume)+1]
omsg(f'INFO → Resuming at command {resume!r}')
if opt.exit_after and opt.exit_after not in self.gm.cmd_list:
die(1,'{!r}: command not recognized'.format(opt.exit_after))
@ -721,7 +738,8 @@ class TestSuiteRunner(object):
if usr_args:
for arg in usr_args:
if arg in self.gm.cmd_groups:
if not self.init_group(arg): continue
if not self.init_group(arg):
continue
clean(self.ts.tmpdir_nums)
for cmd in self.gm.cmd_list:
self.check_needs_rerun(cmd,build=True)
@ -753,8 +771,10 @@ class TestSuiteRunner(object):
if e not in self.gm.cmd_groups_dfl:
die(1,'{!r}: group not recognized'.format(e))
for gname in self.gm.cmd_groups_dfl:
if opt.exclude_groups and gname in exclude: continue
if not self.init_group(gname): continue
if opt.exclude_groups and gname in exclude:
continue
if not self.init_group(gname):
continue
clean(self.ts.tmpdir_nums)
for cmd in self.gm.cmd_list:
self.check_needs_rerun(cmd,build=True)
@ -823,13 +843,13 @@ class TestSuiteRunner(object):
if hasattr(self.ts,'shared_deps'):
arg_list = arg_list[:-len(self.ts.shared_deps)]
if opt.resume:
if cmd == opt.resume:
bmsg('Resuming at {!r}'.format(cmd))
opt.resume = False
opt.skip_deps = False
else:
global resume
if resume:
if cmd != resume:
return
bmsg('Resuming at {!r}'.format(cmd))
resume = False
opt.skip_deps = False
if opt.profile: start = time.time()
@ -938,14 +958,17 @@ if opt.pause:
set_restore_term_at_exit()
set_environ_for_spawned_scripts()
start_test_daemons(network_id)
if network_id not in ('eth','etc'):
start_test_daemons(network_id)
try:
tr = TestSuiteRunner(data_dir,trash_dir)
tr.run_tests(usr_args)
tr.warn_skipped()
if network_id not in ('eth','etc'):
stop_test_daemons(network_id)
except KeyboardInterrupt:
if network_id not in ('eth','etc'):
stop_test_daemons(network_id)
tr.warn_skipped()
die(1,'\ntest.py exiting at user request')

View file

@ -157,12 +157,16 @@ class TestSuiteAutosign(TestSuiteBase):
copy_files(mountpoint,remove_signed_only=True,include_bad_tx=not led_opts)
do_unmount()
do_loop()
imsg(purple('\nKilling wait loop!'))
t.kill(2) # 2 = SIGINT
t.req_exit_val = 1
return t
def do_autosign(opts,mountpoint):
if not opt.skip_deps:
make_wallet(opts)
copy_files(mountpoint,include_bad_tx=True)
t = self.spawn('mmgen-autosign',opts+['--full-summary','wait'],extra_desc='(sign - full summary)')

View file

@ -151,7 +151,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
('txcreate1', 'creating a transaction (spend from dev address to address :1)'),
('txsign1', 'signing the transaction'),
('tx_status0', 'getting the transaction status'),
('tx_status0_bad', 'getting the transaction status'),
('txsign1_ni', 'signing the transaction (non-interactive)'),
('txsend1', 'sending the transaction'),
('bal1', 'the {} balance'.format(g.coin)),
@ -425,7 +425,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1)
def txsign1(self): return self.txsign(add_args=['--use-internal-keccak-module'])
def tx_status0(self):
def tx_status0_bad(self):
return self.tx_status(ext='{}.sigtx',expect_str='neither in mempool nor blockchain',exit_val=1)
def txsign1_ni(self): return self.txsign(ni=True)
def txsend1(self): return self.txsend()

View file

@ -725,11 +725,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
return self.user_txdo('bob',rtFee[4],[pairs[0][1]],'3')
def user_import(self,user,args):
t = self.spawn('mmgen-addrimport',['--quiet','--'+user]+args)
t = self.spawn('mmgen-addrimport',['--'+user]+args)
if g.debug:
t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
t.expect('Importing')
t.expect('OK')
t.read()
return t
def bob_import_addr(self):

View file

@ -437,6 +437,7 @@ try:
die(1,'Only one command may be specified')
cmd = cmd_args[0]
if cmd in cmd_data:
cleandir(cfg['tmpdir'],do_msg=True)
msg('Running tests for {}:'.format(cmd_data[cmd]['desc']))
do_cmds(cmd)
elif cmd == 'clean':

View file

@ -28,7 +28,7 @@ from mmgen.common import *
opts_data = {
'text': {
'desc': "Unit tests for the MMGen suite",
'usage':'[options] [tests]',
'usage':'[options] [tests | test [subtest]]',
'options': """
-h, --help Print this help message
-A, --no-daemon-autostart Don't start and stop daemons automatically
@ -45,7 +45,7 @@ If no test is specified, all available tests are run
}
sys.argv.insert(1,'--skip-cfg-file')
cmd_args = opts.init(opts_data)
cmd_args = opts.init(opts_data,add_opts=['no_daemon_stop'])
def exit_msg():
t = int(time.time()) - start_time
@ -81,20 +81,40 @@ class UnitTestHelpers(object):
else:
rdie(3,m_noraise.format(desc,exc_chk))
try:
for test in cmd_args:
if test not in all_tests:
die(1,"'{}': test not recognized".format(test))
import importlib
for test in (cmd_args or all_tests):
def run_test(test,subtest=None):
modname = 'test.unit_tests_d.ut_{}'.format(test)
mod = importlib.import_module(modname)
gmsg('Running unit test {}'.format(test))
if not mod.unit_test().run_test(test,UnitTestHelpers):
rdie(1,'Unit test {!r} failed'.format(test))
del mod
def run_subtest(subtest):
gmsg(f'Running unit subtest {test}.{subtest}')
t = getattr(mod,'unit_tests')()
if not getattr(t,subtest)(test,UnitTestHelpers):
rdie(1,f'Unit subtest {subtest!r} failed')
pass
if subtest:
run_subtest(subtest)
else:
gmsg(f'Running unit test {test}')
if hasattr(mod,'unit_tests'):
t = getattr(mod,'unit_tests')
subtests = [k for k,v in t.__dict__.items() if type(v).__name__ == 'function']
for subtest in subtests:
run_subtest(subtest)
else:
if not mod.unit_test().run_test(test,UnitTestHelpers):
rdie(1,'Unit test {test!r} failed')
try:
import importlib
if len(cmd_args) == 2 and cmd_args[0] in all_tests and cmd_args[1] not in all_tests:
run_test(*cmd_args) # assume 2nd arg is subtest
else:
for test in cmd_args:
if test not in all_tests:
die(1,f'{test!r}: test not recognized')
for test in (cmd_args or all_tests):
run_test(test)
exit_msg()
except KeyboardInterrupt:
die(1,green('\nExiting at user request'))