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 doc/wiki/using-mmgen/*
include test/*.py include test/*.py
include test/include/*.py
include test/test_py_d/*.py include test/test_py_d/*.py
include test/objtest_py_d/*.py include test/objtest_py_d/*.py
include test/objattrtest_py_d/*.py include test/objattrtest_py_d/*.py

View file

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

View file

@ -95,9 +95,9 @@ class EthereumTrackingWallet(TrackingWallet):
r = self.data_root r = self.data_root
if addr in r: if addr in r:
if not r[addr]['mmid'] and label.mmid: 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: 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 } r[addr] = { 'mmid': label.mmid, 'comment': label.comment }
@write_mode @write_mode
@ -153,14 +153,15 @@ class EthereumTrackingWallet(TrackingWallet):
if self.data['tokens'][addr]['params'].get('symbol') == sym.upper(): if self.data['tokens'][addr]['params'].get('symbol') == sym.upper():
return addr return addr
if no_rpc: return None if no_rpc:
return None
for addr in self.data['tokens']: for addr in self.data['tokens']:
if Token(addr).symbol().upper() == sym.upper(): if Token(addr).symbol().upper() == sym.upper():
self.force_set_token_param(addr,'symbol',sym.upper()) self.force_set_token_param(addr,'symbol',sym.upper())
return addr return addr
else:
return None return None
def get_token_param(self,token,param): def get_token_param(self,token,param):
if token in self.data['tokens']: if token in self.data['tokens']:
@ -180,6 +181,7 @@ class EthereumTrackingWallet(TrackingWallet):
class EthereumTokenTrackingWallet(EthereumTrackingWallet): class EthereumTokenTrackingWallet(EthereumTrackingWallet):
desc = 'Ethereum token tracking wallet'
decimals = None decimals = None
symbol = None symbol = None
cur_eth_balances = {} cur_eth_balances = {}
@ -315,7 +317,8 @@ class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
prompt_fs = 'Total to spend: {} {}\n\n' prompt_fs = 'Total to spend: {} {}\n\n'
col_adj = 37 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): def get_unspent_data(self):
super().get_unspent_data() super().get_unspent_data()
@ -336,11 +339,12 @@ class EthereumTwAddrList(TwAddrList):
for mmid,d in list(tw_dict.items()): for mmid,d in list(tw_dict.items()):
# if d['confirmations'] < minconf: continue # cannot get confirmations for eth account # if d['confirmations'] < minconf: continue # cannot get confirmations for eth account
label = TwLabel(mmid+' '+d['comment'],on_fail='raise') 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']) bal = self.wallet.get_balance(d['addr'])
if bal == 0 and not showempty: if bal == 0 and not showempty:
if not label.comment: continue if not label.comment or not all_labels:
if not all_labels: continue continue
self[label.mmid] = {'amt': g.proto.coin_amt('0'), 'lbl': label } self[label.mmid] = {'amt': g.proto.coin_amt('0'), 'lbl': label }
if showbtcaddrs: if showbtcaddrs:
self[label.mmid]['addr'] = CoinAddr(d['addr']) 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.tx_gas = o['startGas'] # approximate, but better than nothing
self.fee = self.fee_rel2abs(o['gasPrice'].toWei()) self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
self.txobj = o self.txobj = o
return d # 'token_addr','decimals' required by subclass return d # 'token_addr','decimals' required by Token subclass
def get_nonce(self): def get_nonce(self):
return ETHNonce(int(g.rpch.parity_nextNonce('0x'+self.inputs[0].addr),16)) 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.hex = json.dumps(odict)
self.update_txid() self.update_txid()
def del_output(self,idx): pass def del_output(self,idx):
pass
def update_txid(self): def update_txid(self):
assert not is_hex_str(self.hex),'update_txid() must be called only when self.hex is not hex data' 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): def process_cmd_args(self,cmd_args,ad_f,ad_w):
lc = len(cmd_args) 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: if lc != 1:
fs = '{} output{} specified, but Ethereum transactions must have exactly one' fs = '{} output{} specified, but Ethereum transactions must have exactly one'
die(1,fs.format(lc,suf(lc))) 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): def select_unspent(self,unspent):
prompt = 'Enter an account to spend from: ' prompt = 'Enter an account to spend from: '
@ -366,7 +369,8 @@ class EthereumMMGenTX(MMGenTX):
self.get_status() 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') 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): if rpc_error(ret):
msg(yellow(rpc_errmsg(ret))) msg(yellow(rpc_errmsg(ret)))
msg(red('Send of MMGen transaction {} failed'.format(self.txid))) 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 return False
else: else:
m = 'BOGUS transaction NOT sent: {}' if g.bogus_send else 'Transaction sent: {}' if g.bogus_send:
if not g.bogus_send: m = 'BOGUS transaction NOT sent: {}'
else:
m = 'Transaction sent: {}'
assert ret == '0x'+self.coin_txid,'txid mismatch (after sending)' assert ret == '0x'+self.coin_txid,'txid mismatch (after sending)'
self.desc = 'sent transaction' self.desc = 'sent transaction'
msg(m.format(self.coin_txid.hl())) msg(m.format(self.coin_txid.hl()))

View file

@ -56,6 +56,8 @@ class Daemon(MMGenObject):
def exec_cmd(self,cmd,check): def exec_cmd(self,cmd,check):
cp = run(cmd,check=False,stdout=PIPE,stderr=PIPE) cp = run(cmd,check=False,stdout=PIPE,stderr=PIPE)
if self.debug:
print(cp)
if check and cp.returncode != 0: if check and cp.returncode != 0:
raise MMGenCalledProcessError(cp) raise MMGenCalledProcessError(cp)
return cp return cp
@ -172,8 +174,8 @@ class Daemon(MMGenObject):
return True return True
time.sleep(0.2) time.sleep(0.2)
else: else:
m = 'Wait for state {!r} timeout exceeded for daemon {} {}' m = 'Wait for state {!r} timeout exceeded for daemon {} {} (port {})'
die(2,m.format(req_state,self.daemon_id.upper(),self.network)) die(2,m.format(req_state,self.daemon_id.upper(),self.network,self.rpc_port))
@classmethod @classmethod
def check_implement(cls): def check_implement(cls):
@ -202,6 +204,13 @@ class Daemon(MMGenObject):
die(1,'Flag {!r} not set, so cannot be removed'.format(val)) die(1,'Flag {!r} not set, so cannot be removed'.format(val))
self._flags.remove(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): class MoneroWalletDaemon(Daemon):
desc = 'RPC 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): class MMGenObject(object):
# Pretty-print any object subclassed from MMGenObject, recursing into sub-objects - WIP # Pretty-print any object subclassed from MMGenObject, recursing into sub-objects - WIP
def pmsg(self): print(self.pfmt()) def pmsg(self):
def pdie(self): print(self.pfmt()); sys.exit(0) print(self.pfmt())
def pdie(self):
print(self.pfmt())
sys.exit(1)
def pfmt(self,lvl=0,id_list=[]): def pfmt(self,lvl=0,id_list=[]):
scalars = (str,int,float,Decimal) scalars = (str,int,float,Decimal)
def do_list(out,e,lvl=0,is_dict=False): 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') if s: sys.stderr.write(s+'\n')
sys.exit(ev) 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: # Constants:
version = '0.12.099' version = '0.12.099'
@ -89,7 +96,7 @@ class g(object):
use_internal_keccak_module = False use_internal_keccak_module = False
chain = None # set by first call to rpc_init() chain = None # set by first call to rpc_init()
chains = 'mainnet','testnet','regtest' chains = ('mainnet','testnet','regtest')
# rpc: # rpc:
rpc_host = '' rpc_host = ''
@ -118,13 +125,6 @@ class g(object):
mnemonic_entry_modes = {} 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() color = sys.stdout.isatty()
if os.getenv('HOME'): # Linux or MSYS if os.getenv('HOME'): # Linux or MSYS
@ -154,7 +154,8 @@ class g(object):
# 'long' opts - opt sets global var # 'long' opts - opt sets global var
common_opts = ( 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', 'monero_wallet_rpc_host','monero_wallet_rpc_user','monero_wallet_rpc_password',
'daemon_data_dir','force_256_color','regtest','coin','bob','alice', 'daemon_data_dir','force_256_color','regtest','coin','bob','alice',
'accept_defaults','token' 'accept_defaults','token'

View file

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

View file

@ -44,9 +44,11 @@ rpc_init()
if len(cmd_args) == 1: if len(cmd_args) == 1:
infile = cmd_args[0]; check_infile(infile) 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 * from .tx import *

View file

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

View file

@ -112,7 +112,8 @@ def override_globals_from_cfg_file(ucfg):
def override_globals_from_env(): def override_globals_from_env():
for name in g.env_opts: for name in g.env_opts:
if name == 'MMGEN_DEBUG_ALL': continue if name == 'MMGEN_DEBUG_ALL':
continue
disable = name[:14] == 'MMGEN_DISABLE_' disable = name[:14] == 'MMGEN_DISABLE_'
val = os.getenv(name) # os.getenv() returns None if env var is unset 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 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 check_and_set_autoset_opts(): # Raises exception if any check fails
def nocase_str(key,val,asd): def nocase_str(key,val,asd):
if val.lower() in asd.choices: try:
return True return asd.choices.index(val)
else: except:
return 'one of' return 'one of'
def nocase_pfx(key,val,asd): def nocase_pfx(key,val,asd):

View file

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

View file

@ -264,9 +264,11 @@ class MMGenToolCmdMeta(type):
def __contains__(cls,val): def __contains__(cls,val):
return cls.methods.__contains__(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): def call(cls,cmd_name,*args,**kwargs):
subcls = cls.classes[cls.methods[cmd_name].__qualname__.split('.')[0]] return getattr(cls.classes[cls.classname(cmd_name)](),cmd_name)(*args,**kwargs)
return getattr(subcls(),cmd_name)(*args,**kwargs)
@property @property
def user_commands(cls): def user_commands(cls):
@ -909,7 +911,10 @@ class MMGenToolCmdRPC(MMGenToolCmds):
twuo.do_sort(sort,reverse=reverse) twuo.do_sort(sort,reverse=reverse)
twuo.age_fmt = age_fmt twuo.age_fmt = age_fmt
twuo.show_mmid = show_mmid 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 del twuo.wallet
return ret return ret
@ -940,7 +945,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
Note that the use of these commands requires private data to be exposed on 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 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 _monero_chain_height = None
@ -999,7 +1004,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
restore_height = blockheight, restore_height = blockheight,
language = 'English' ) 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 return True
def sync(n,d,fn,c,m): def sync(n,d,fn,c,m):
@ -1043,7 +1048,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
from .obj import XMRAmt from .obj import XMRAmt
bals[fn] = tuple([XMRAmt(ret[k],from_unit='min_coin_unit') for k in ('balance','unlocked_balance')]) 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) pp_msg(ret)
else: else:
msg(' Balance: {} Unlocked balance: {}'.format(*[b.hl() for b in bals[fn]])) 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 = [] tr_rpc = []
lbl_id = ('account','label')['label_api' in g.rpch.caps] lbl_id = ('account','label')['label_api' in g.rpch.caps]
for o in us_rpc: 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]) l = get_tw_label(o[lbl_id])
if l: if l:
o.update({ o.update({
@ -172,6 +173,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
'confs': o['confirmations'] 'confs': o['confirmations']
}) })
tr_rpc.append(o) tr_rpc.append(o)
self.unspent = self.MMGenTwOutputList( self.unspent = self.MMGenTwOutputList(
self.MMGenTwUnspentOutput( self.MMGenTwUnspentOutput(
**{k:v for k,v in o.items() if k in dir(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 del tw
atexit.register(del_tw,self) 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): 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: if g.debug:
print_stack_trace('TW DEL {!r}'.format(self)) print_stack_trace('TW DEL {!r}'.format(self))
@ -684,7 +691,8 @@ class TrackingWallet(MMGenObject):
elif g.debug: elif g.debug:
msg('read-only wallet, doing nothing') msg('read-only wallet, doing nothing')
def upgrade_wallet_maybe(self): pass def upgrade_wallet_maybe(self):
pass
@staticmethod @staticmethod
def conv_types(ad): def conv_types(ad):
@ -825,12 +833,14 @@ class TrackingWallet(MMGenObject):
from .addr import AddrData from .addr import AddrData
mmaddr = AddrData(source='tw').coinaddr2mmaddr(coinaddr) 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) mmaddr = TwMMGenID(mmaddr)
cmt = TwComment(label,on_fail=on_fail) 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) lbl = TwLabel(mmaddr + ('',' '+cmt)[bool(cmt)],on_fail=on_fail)

View file

@ -313,7 +313,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
self.outputs = MMGenTxOutputList() self.outputs = MMGenTxOutputList()
self.send_amt = g.proto.coin_amt('0') # total amt minus change self.send_amt = g.proto.coin_amt('0') # total amt minus change
self.fee = g.proto.coin_amt('0') self.fee = g.proto.coin_amt('0')
self.hex = '' # raw serialized hex transaction self.hex = '' # raw serialized hex transaction
self.label = MMGenTXLabel('') self.label = MMGenTXLabel('')
self.txid = '' self.txid = ''
self.coin_txid = '' self.coin_txid = ''
@ -330,7 +330,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
if filename: if filename:
self.parse_tx_file(filename,metadata_only=metadata_only,quiet_open=quiet_open) 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_pubkey_scripts()
self.check_sigs() # marks the tx as signed 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 fee_per_kb = ret['feerate'] if 'feerate' in ret else -2
fe_type = 'estimatesmartfee' fe_type = 'estimatesmartfee'
except: except:
fee_per_kb = g.rpch.estimatefee() if g.coin=='BCH' and g.rpch.daemon_version >= 190100 \ args = () if g.coin=='BCH' and g.rpc.daemon_version >= 190100 else (opt.tx_confs,)
else g.rpch.estimatefee(opt.tx_confs) fee_per_kb = await g.rpc.call('estimatefee',*args)
fe_type = 'estimatefee' fe_type = 'estimatefee'
return fee_per_kb,fe_type 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 m = errmsg
msg(yellow(m)) msg(yellow(m))
msg(red('Send of MMGen transaction {} failed'.format(self.txid))) 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 return False
else: else:
if g.bogus_send: 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' desc = 'send amount in metadata'
self.send_amt = g.proto.coin_amt(send_amt,on_fail='raise') 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() self.check_txfile_hex_data()
# the following ops will all fail if g.coin doesn't match self.coin # the following ops will all fail if g.coin doesn't match self.coin
desc = 'coin type in metadata' 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() self.twuo.display_total()
if do_info: sys.exit(0) if do_info:
sys.exit(0)
self.send_amt = self.sum_outputs() self.send_amt = self.sum_outputs()
@ -1515,7 +1518,7 @@ class MMGenBumpTX(MMGenTX):
if not self.is_replaceable(): if not self.is_replaceable():
die(1,"Transaction '{}' is not replaceable".format(self.txid)) 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 send:
if not self.marked_signed(): if not self.marked_signed():
die(1,"File '{}' is not a signed {} transaction file".format(filename,g.proj_name)) 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) p = 'Fee will be deducted from output {}{} ({} {})'.format(idx+1,cs,o_amt,g.coin)
if check_sufficient_funds(o_amt): if check_sufficient_funds(o_amt):
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): 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 self.bump_output_idx = idx
return 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 = KeyAddrList(addrlist=non_mm_addrs)
tmp.add_wifs(kl) tmp.add_wifs(kl)
m = tmp.list_missing('sec') 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 keys += tmp.data
if opt.mmgen_keys_from_file: if opt.mmgen_keys_from_file:

View file

@ -96,9 +96,9 @@ def pp_fmt(d):
def pp_msg(d): def pp_msg(d):
msg(pp_fmt(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" "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=''): def fmt_list(l,fmt='dfl',indent=''):
"pretty-format a list" "pretty-format a list"
@ -608,6 +608,8 @@ def write_data_to_file( outfile,data,desc='data',
d = '' d = ''
finally: finally:
if d != cmp_data: 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" m = "{} in file '{}' has been altered by some other program! Aborting file write"
die(3,m.format(desc,outfile)) die(3,m.format(desc,outfile))

View file

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

View file

@ -165,25 +165,22 @@ def iqmsg_r(s):
if not opt.quiet: omsg_r(s) if not opt.quiet: omsg_r(s)
def start_test_daemons(*network_ids): def start_test_daemons(*network_ids):
if hasattr(opt,'no_daemon_autostart') and opt.no_daemon_autostart: if not opt.no_daemon_autostart:
return return test_daemons_ops(*network_ids,op='start')
return test_daemons_ops(*network_ids,op='start')
def stop_test_daemons(*network_ids): def stop_test_daemons(*network_ids):
if hasattr(opt,'no_daemon_stop') and opt.no_daemon_stop: if not opt.no_daemon_stop:
return return test_daemons_ops(*network_ids,op='stop')
return test_daemons_ops(*network_ids,op='stop')
def restart_test_daemons(*network_ids): def restart_test_daemons(*network_ids):
stop_test_daemons(*network_ids) stop_test_daemons(*network_ids)
return start_test_daemons(*network_ids) return start_test_daemons(*network_ids)
def test_daemons_ops(*network_ids,op): def test_daemons_ops(*network_ids,op):
if opt.no_daemon_autostart: if not opt.no_daemon_autostart:
return from mmgen.daemon import CoinDaemon
from mmgen.daemon import CoinDaemon silent = not opt.verbose and not getattr(opt,'exact_output',False)
silent = not opt.verbose and not (hasattr(opt,'exact_output') and opt.exact_output) for network_id in network_ids:
for network_id in network_ids: if network_id.lower() not in CoinDaemon.network_ids: # silently ignore invalid IDs
if network_id not in CoinDaemon.network_ids: # silently ignore invalid IDs continue
continue CoinDaemon(network_id,test_suite=True).cmd(op,silent=silent)
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)) dest = os.path.join(shm_dir,os.path.basename(trash_dir))
os.mkdir(dest,0o755) 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) os.symlink(dest,trash_dir)
dest = os.path.join(shm_dir,os.path.basename(data_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 -P, --profile Record the execution time of each script
-q, --quiet Produce minimal output. Suppress dependency info -q, --quiet Produce minimal output. Suppress dependency info
-r, --resume=c Resume at command 'c' after interrupted run -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 -s, --system Test scripts and modules installed on system rather
than those in the repo root than those in the repo root
-S, --skip-deps Skip dependency checking for command -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() check_segwit_opts()
if opt.profile: opt.names = True if opt.profile:
if opt.resume: opt.skip_deps = True opt.names = True
if opt.exact_output: if opt.exact_output:
def msg(s): pass def msg(s): pass
qmsg = qmsg_r = vmsg = vmsg_r = msg_r = msg 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 cfgs = { # addr_idx_lists (except 31,32,33,34) must contain exactly 8 addresses
'1': { 'wpasswd': 'Dorian-α', '1': { 'wpasswd': 'Dorian-α',
'kapasswd': 'Grok the blockchain', '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 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. 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() return [k for k,v in cfgs[str(tmpdir_idx)]['dep_generators'].items()
if k in cls.shared_deps and v != cmdname] 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): def gm_init_group(self,trunner,gname,spawn_prog):
kwargs = self.cmd_groups[gname][1] kwargs = self.cmd_groups[gname][1]
cls = self.create_group(gname,**kwargs) cls = self.create_group(gname,**kwargs)
cls.group_name = gname
return cls(trunner,cfgs,spawn_prog) return cls(trunner,cfgs,spawn_prog)
def list_cmd_groups(self): def list_cmd_groups(self):
@ -657,13 +667,15 @@ class TestSuiteRunner(object):
else: else:
omsg_r('Testing {}: '.format(desc)) omsg_r('Testing {}: '.format(desc))
if msg_only: return if msg_only:
return
if opt.log: if opt.log:
try: self.log_fd.write('[{}][{}:{}] {}\n'.format(
self.log_fd.write(cmd_disp+'\n') g.coin.lower(),
except: self.ts.group_name,
self.log_fd.write(ascii(cmd_disp)+'\n') self.ts.test_name,
cmd_disp))
from test.include.pexpect import MMGenPexpect from test.include.pexpect import MMGenPexpect
return MMGenPexpect(args,no_output=no_output) 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) 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: if opt.exit_after and opt.exit_after not in self.gm.cmd_list:
die(1,'{!r}: command not recognized'.format(opt.exit_after)) die(1,'{!r}: command not recognized'.format(opt.exit_after))
@ -721,7 +738,8 @@ class TestSuiteRunner(object):
if usr_args: if usr_args:
for arg in usr_args: for arg in usr_args:
if arg in self.gm.cmd_groups: 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) clean(self.ts.tmpdir_nums)
for cmd in self.gm.cmd_list: for cmd in self.gm.cmd_list:
self.check_needs_rerun(cmd,build=True) self.check_needs_rerun(cmd,build=True)
@ -753,8 +771,10 @@ class TestSuiteRunner(object):
if e not in self.gm.cmd_groups_dfl: if e not in self.gm.cmd_groups_dfl:
die(1,'{!r}: group not recognized'.format(e)) die(1,'{!r}: group not recognized'.format(e))
for gname in self.gm.cmd_groups_dfl: for gname in self.gm.cmd_groups_dfl:
if opt.exclude_groups and gname in exclude: continue if opt.exclude_groups and gname in exclude:
if not self.init_group(gname): continue continue
if not self.init_group(gname):
continue
clean(self.ts.tmpdir_nums) clean(self.ts.tmpdir_nums)
for cmd in self.gm.cmd_list: for cmd in self.gm.cmd_list:
self.check_needs_rerun(cmd,build=True) self.check_needs_rerun(cmd,build=True)
@ -823,13 +843,13 @@ class TestSuiteRunner(object):
if hasattr(self.ts,'shared_deps'): if hasattr(self.ts,'shared_deps'):
arg_list = arg_list[:-len(self.ts.shared_deps)] arg_list = arg_list[:-len(self.ts.shared_deps)]
if opt.resume: global resume
if cmd == opt.resume: if resume:
bmsg('Resuming at {!r}'.format(cmd)) if cmd != resume:
opt.resume = False
opt.skip_deps = False
else:
return return
bmsg('Resuming at {!r}'.format(cmd))
resume = False
opt.skip_deps = False
if opt.profile: start = time.time() if opt.profile: start = time.time()
@ -938,15 +958,18 @@ if opt.pause:
set_restore_term_at_exit() set_restore_term_at_exit()
set_environ_for_spawned_scripts() set_environ_for_spawned_scripts()
start_test_daemons(network_id) if network_id not in ('eth','etc'):
start_test_daemons(network_id)
try: try:
tr = TestSuiteRunner(data_dir,trash_dir) tr = TestSuiteRunner(data_dir,trash_dir)
tr.run_tests(usr_args) tr.run_tests(usr_args)
tr.warn_skipped() tr.warn_skipped()
stop_test_daemons(network_id) if network_id not in ('eth','etc'):
stop_test_daemons(network_id)
except KeyboardInterrupt: except KeyboardInterrupt:
stop_test_daemons(network_id) if network_id not in ('eth','etc'):
stop_test_daemons(network_id)
tr.warn_skipped() tr.warn_skipped()
die(1,'\ntest.py exiting at user request') die(1,'\ntest.py exiting at user request')
except TestSuiteException as e: except TestSuiteException as e:

View file

@ -157,12 +157,16 @@ class TestSuiteAutosign(TestSuiteBase):
copy_files(mountpoint,remove_signed_only=True,include_bad_tx=not led_opts) copy_files(mountpoint,remove_signed_only=True,include_bad_tx=not led_opts)
do_unmount() do_unmount()
do_loop() do_loop()
imsg(purple('\nKilling wait loop!'))
t.kill(2) # 2 = SIGINT t.kill(2) # 2 = SIGINT
t.req_exit_val = 1 t.req_exit_val = 1
return t return t
def do_autosign(opts,mountpoint): def do_autosign(opts,mountpoint):
make_wallet(opts)
if not opt.skip_deps:
make_wallet(opts)
copy_files(mountpoint,include_bad_tx=True) copy_files(mountpoint,include_bad_tx=True)
t = self.spawn('mmgen-autosign',opts+['--full-summary','wait'],extra_desc='(sign - full summary)') 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)'), ('txcreate1', 'creating a transaction (spend from dev address to address :1)'),
('txsign1', 'signing the transaction'), ('txsign1', 'signing the transaction'),
('tx_status0', 'getting the transaction status'), ('tx_status0_bad', 'getting the transaction status'),
('txsign1_ni', 'signing the transaction (non-interactive)'), ('txsign1_ni', 'signing the transaction (non-interactive)'),
('txsend1', 'sending the transaction'), ('txsend1', 'sending the transaction'),
('bal1', 'the {} balance'.format(g.coin)), ('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) 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 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) 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 txsign1_ni(self): return self.txsign(ni=True)
def txsend1(self): return self.txsend() 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') return self.user_txdo('bob',rtFee[4],[pairs[0][1]],'3')
def user_import(self,user,args): def user_import(self,user,args):
t = self.spawn('mmgen-addrimport',['--quiet','--'+user]+args) t = self.spawn('mmgen-addrimport',['--'+user]+args)
if g.debug: if g.debug:
t.expect("Type uppercase 'YES' to confirm: ",'YES\n') t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
t.expect('Importing') t.expect('Importing')
t.expect('OK') t.expect('OK')
t.read()
return t return t
def bob_import_addr(self): def bob_import_addr(self):

View file

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

View file

@ -28,7 +28,7 @@ from mmgen.common import *
opts_data = { opts_data = {
'text': { 'text': {
'desc': "Unit tests for the MMGen suite", 'desc': "Unit tests for the MMGen suite",
'usage':'[options] [tests]', 'usage':'[options] [tests | test [subtest]]',
'options': """ 'options': """
-h, --help Print this help message -h, --help Print this help message
-A, --no-daemon-autostart Don't start and stop daemons automatically -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') 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(): def exit_msg():
t = int(time.time()) - start_time t = int(time.time()) - start_time
@ -81,20 +81,40 @@ class UnitTestHelpers(object):
else: else:
rdie(3,m_noraise.format(desc,exc_chk)) rdie(3,m_noraise.format(desc,exc_chk))
def run_test(test,subtest=None):
modname = 'test.unit_tests_d.ut_{}'.format(test)
mod = importlib.import_module(modname)
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: try:
for test in cmd_args:
if test not in all_tests:
die(1,"'{}': test not recognized".format(test))
import importlib import importlib
for test in (cmd_args or all_tests): if len(cmd_args) == 2 and cmd_args[0] in all_tests and cmd_args[1] not in all_tests:
modname = 'test.unit_tests_d.ut_{}'.format(test) run_test(*cmd_args) # assume 2nd arg is subtest
mod = importlib.import_module(modname) else:
gmsg('Running unit test {}'.format(test)) for test in cmd_args:
if not mod.unit_test().run_test(test,UnitTestHelpers): if test not in all_tests:
rdie(1,'Unit test {!r} failed'.format(test)) die(1,f'{test!r}: test not recognized')
del mod for test in (cmd_args or all_tests):
run_test(test)
exit_msg() exit_msg()
except KeyboardInterrupt: except KeyboardInterrupt:
die(1,green('\nExiting at user request')) die(1,green('\nExiting at user request'))