9 Commits e379926042 ... a3e7c08f84

Author SHA1 Message Date
  The MMGen Project a3e7c08f84 implement `async_run()` with `aiohttp` backend 2 months ago
  The MMGen Project 23ca4a9733 daemontest.py rpc: reimplement using `async_run()` 2 months ago
  The MMGen Project 504000792f cfg._set_autoset_opts(): cleanups 2 months ago
  The MMGen Project eb7bcef036 util.py: reimplement `async_run()` (stub) 2 months ago
  The MMGen Project 4749fe660b test suite: minor cleanups (async_run) 2 months ago
  The MMGen Project f8d352df33 cmdtest.py: use `isAsync()` 2 months ago
  The MMGen Project 8d314c6abe cmdtest.py rune, runeswap: make txid mismatches into warnings 2 months ago
  The MMGen Project dc3df437ab cmdtest.py: make test methods async where applicable 2 months ago
  The MMGen Project 6ccf29fb21 util.py: new `isAsync()` function 2 months ago

+ 1 - 1
examples/halving-calculator.py

@@ -87,4 +87,4 @@ async def main():
 		f'Est. time until halving: {dhms(cur["time"] + t_rem - clock_time)}\n'
 	)
 
-async_run(main())
+async_run(cfg, main)

+ 9 - 14
mmgen/cfg.py

@@ -147,7 +147,7 @@ class Config(Lockable):
 	  3 - config file
 	"""
 	_autolock = False
-	_set_ok = ('usr_randchars', '_proto')
+	_set_ok = ('usr_randchars', '_proto', 'aiohttp_session')
 	_reset_ok = ('accept_defaults',)
 	_delete_ok = ('_opts',)
 	_use_class_attr = True
@@ -200,6 +200,7 @@ class Config(Lockable):
 	rpc_user              = ''
 	rpc_password          = ''
 	aiohttp_rpc_queue_len = 16
+	aiohttp_session       = None
 	cached_balances       = False
 
 	# daemons
@@ -719,24 +720,18 @@ class Config(Lockable):
 
 		# Check autoset opts, setting if unset
 		for key in self._autoset_opts:
-
-			if key in self._cloned:
-				continue
-
-			assert not hasattr(self, key), f'autoset opt {key!r} is already set, but it shouldn’t be!'
-
 			if key in self._uopts:
 				val, src = (self._uopts[key], 'cmdline')
+				setattr(self, key, get_autoset_opt(key, val, src=src))
+			elif key in self._cloned:
+				pass
 			elif key in cfgfile_autoset_opts:
 				val, src = (cfgfile_autoset_opts[key], 'cfgfile')
-			else:
-				val = None
-
-			if val is None:
-				if key not in self._dfl_none_autoset_opts:
-					setattr(self, key, self._autoset_opts[key].choices[0])
-			else:
 				setattr(self, key, get_autoset_opt(key, val, src=src))
+			elif hasattr(self, key):
+				raise ValueError(f'autoset opt {key!r} is already set, but it shouldn’t be!')
+			elif key not in self._dfl_none_autoset_opts:
+				setattr(self, key, self._autoset_opts[key].choices[0])
 
 	def _set_auto_typeset_opts(self, cfgfile_auto_typeset_opts):
 

+ 1 - 1
mmgen/data/release_date

@@ -1 +1 @@
-September 2025
+October 2025

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev2
+16.1.dev3

+ 1 - 0
mmgen/exception.py

@@ -77,6 +77,7 @@ class SwapCfgValueError(Exception):       mmcode = 2
 # 3: yellow hl, 'MMGen Error' + exception + message
 class RPCFailure(Exception):              mmcode = 3
 class RPCChainMismatch(Exception):        mmcode = 3
+class TxIDMismatch(Exception):            mmcode = 3
 class BadTxSizeEstimate(Exception):       mmcode = 3
 class MaxInputSizeExceeded(Exception):    mmcode = 3
 class MaxFileSizeExceeded(Exception):     mmcode = 3

+ 1 - 1
mmgen/main_addrimport.py

@@ -178,4 +178,4 @@ cfg = Config(opts_data=opts_data, need_amt=False)
 
 proto = cfg._proto
 
-async_run(main())
+async_run(cfg, main)

+ 1 - 1
mmgen/main_autosign.py

@@ -187,7 +187,7 @@ def main(do_loop):
 			ret = await asi.do_sign()
 			asi.at_exit(not ret)
 
-	async_run(do())
+	async_run(cfg, do)
 
 from .cfg import Config
 from .autosign import Autosign

+ 1 - 1
mmgen/main_msg.py

@@ -227,4 +227,4 @@ async def main():
 		case _:
 			die(1, f'{op!r}: unrecognized operation')
 
-async_run(main())
+async_run(cfg, main)

+ 1 - 1
mmgen/main_regtest.py

@@ -86,4 +86,4 @@ elif cmd_args[0] not in ('cli', 'wallet_cli', 'balances'):
 async def main():
 	await MMGenRegtest(cfg, cfg.coin, bdb_wallet=cfg.bdb_wallet).cmd(cmd_args)
 
-async_run(main())
+async_run(cfg, main)

+ 3 - 5
mmgen/main_tool.py

@@ -23,7 +23,7 @@ mmgen-tool:  Perform various MMGen- and cryptocoin-related operations.
 
 import sys, os, importlib
 from .cfg import gc, Config
-from .util import msg, Msg, die, capfirst, suf, async_run
+from .util import msg, Msg, die, capfirst, suf, async_run, isAsync
 
 opts_data = {
 	'filter_codes': ['-'],
@@ -391,10 +391,8 @@ if gc.prog_name.endswith('-tool'):
 
 	args, kwargs = process_args(cmd, args, cls)
 
-	ret = getattr(cls(cfg, cmdname=cmd), cmd)(*args, **kwargs)
-
-	if type(ret).__name__ == 'coroutine':
-		ret = async_run(ret)
+	func = getattr(cls(cfg, cmdname=cmd), cmd)
+	ret = async_run(cfg, func, args=args, kwargs=kwargs) if isAsync(func) else func(*args, **kwargs)
 
 	process_result(
 		ret,

+ 1 - 1
mmgen/main_txbump.py

@@ -217,4 +217,4 @@ async def main():
 			ask_write_default_yes = False,
 			ask_overwrite         = not cfg.yes)
 
-async_run(main())
+async_run(cfg, main)

+ 1 - 1
mmgen/main_txcreate.py

@@ -155,4 +155,4 @@ async def main():
 		ask_overwrite         = not cfg.yes,
 		ask_write_default_yes = False)
 
-async_run(main())
+async_run(cfg, main)

+ 1 - 1
mmgen/main_txdo.py

@@ -196,4 +196,4 @@ async def main():
 	else:
 		die(2, 'Transaction could not be signed')
 
-async_run(main())
+async_run(cfg, main)

+ 1 - 1
mmgen/main_txsend.py

@@ -158,4 +158,4 @@ async def main():
 
 	await tx.send(cfg, asi)
 
-async_run(main())
+async_run(cfg, main)

+ 1 - 1
mmgen/main_txsign.py

@@ -158,4 +158,4 @@ async def main():
 	if bad_tx_count:
 		die(2, f'{bad_tx_count} transaction{suf(bad_tx_count)} could not be signed')
 
-async_run(main())
+async_run(cfg, main)

+ 3 - 3
mmgen/proto/btc/regtest.py

@@ -21,7 +21,7 @@ proto.btc.regtest: Coin daemon regression test mode setup and operations
 """
 
 import os, shutil, json
-from ...util import msg, gmsg, die, capfirst, suf
+from ...util import msg, gmsg, die, capfirst, suf, isAsync
 from ...util2 import cliargs_convert
 from ...protocol import init_proto
 from ...rpc import rpc_init
@@ -256,8 +256,8 @@ class MMGenRegtest(MMGenObject):
 		print(ret if isinstance(ret, str) else json.dumps(ret, cls=json_encoder, indent=4))
 
 	async def cmd(self, args):
-		ret = getattr(self, args[0])(*args[1:])
-		return (await ret) if type(ret).__name__ == 'coroutine' else ret
+		func = getattr(self, args[0])
+		return await func(*args[1:]) if isAsync(func) else func(*args[1:])
 
 	async def fork(self, coin): # currently disabled
 

+ 2 - 11
mmgen/rpc/backends/aiohttp.py

@@ -28,20 +28,11 @@ class aiohttp(base, metaclass=AsyncInit):
 	variables (all are case insensitive).
 	"""
 
-	def __del__(self):
-		self.connector.close()
-		self.session.detach()
-		del self.session
-
 	async def __init__(self, caller):
 		super().__init__(caller)
-		import aiohttp
-		self.connector = aiohttp.TCPConnector(limit_per_host=self.cfg.aiohttp_rpc_queue_len)
-		self.session = aiohttp.ClientSession(
-			headers = {'Content-Type': 'application/json'},
-			connector = self.connector,
-		)
+		self.session = self.cfg.aiohttp_session
 		if caller.auth_type == 'basic':
+			import aiohttp
 			self.auth = aiohttp.BasicAuth(*caller.auth, encoding='UTF-8')
 		else:
 			self.auth = None

+ 6 - 6
mmgen/rpc/local.py

@@ -14,7 +14,7 @@ rpc.local: local RPC client class for the MMGen Project
 
 import sys, json, asyncio, importlib
 
-from ..util import msg, die, fmt, oneshot_warning
+from ..util import msg, die, fmt, oneshot_warning, isAsync
 
 from . import util
 
@@ -55,7 +55,7 @@ class RPCClient:
 		self.timeout = self.cfg.http_timeout or 60
 		self.auth = None
 
-	def _get_backend(self, backend):
+	def _get_backend_cls(self, backend):
 		dfl_backends = {
 			'linux': 'httplib',
 			'darwin': 'httplib',
@@ -63,14 +63,14 @@ class RPCClient:
 		def get_cls(backend_id):
 			return getattr(importlib.import_module(f'mmgen.rpc.backends.{backend_id}'), backend_id)
 		backend_id = backend or self.cfg.rpc_backend
-		return get_cls(dfl_backends[sys.platform] if backend_id == 'auto' else backend_id)(self)
+		return get_cls(dfl_backends[sys.platform] if backend_id == 'auto' else backend_id)
 
 	def set_backend(self, backend=None):
-		self.backend = self._get_backend(backend)
+		self.backend = self._get_backend_cls(backend)(self)
 
 	async def set_backend_async(self, backend=None):
-		ret = self._get_backend(backend)
-		self.backend = (await ret) if type(ret).__name__ == 'coroutine' else ret
+		cls = self._get_backend_cls(backend)
+		self.backend = await cls(self) if isAsync(cls.__init__) else cls(self)
 
 	# Call family of methods - direct-to-daemon RPC call:
 	# - positional params are passed to the daemon, 'timeout' and 'wallet' kwargs to the backend

+ 8 - 6
mmgen/tw/view.py

@@ -27,7 +27,7 @@ from ..cfg import gv
 from ..objmethods import MMGenObject
 from ..obj import get_obj, MMGenIdx, MMGenList
 from ..color import nocolor, yellow, orange, green, red, blue
-from ..util import msg, msg_r, fmt, die, capfirst, suf, make_timestr
+from ..util import msg, msg_r, fmt, die, capfirst, suf, make_timestr, isAsync
 from ..rpc import rpc_init
 from ..base_obj import AsyncInit
 
@@ -288,8 +288,10 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 
 		lbl_id = ('account', 'label')['label_api' in self.rpc.caps]
 
-		res = self.gen_data(rpc_data, lbl_id)
-		self.data = MMGenList(await res if type(res).__name__ == 'coroutine' else res)
+		self.data = MMGenList(
+			await self.gen_data(rpc_data, lbl_id) if isAsync(self.gen_data) else
+			self.gen_data(rpc_data, lbl_id))
+
 		self.disp_data = list(self.filter_data())
 
 		if not self.data:
@@ -607,9 +609,9 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 
 			match reply:
 				case ch if ch in key_mappings:
-					ret = action_classes[ch].run(self, action_methods[ch])
-					if type(ret).__name__ == 'coroutine':
-						await ret
+					func = action_classes[ch].run
+					arg = action_methods[ch]
+					await func(self, arg) if isAsync(func) else func(self, arg)
 				case 'q':
 					msg('')
 					if self.scroll:

+ 2 - 1
mmgen/tx/online.py

@@ -126,7 +126,8 @@ class OnlineSigned(Signed):
 							await asyncio.sleep(1)
 						ret = await self.send_with_node(txhex)
 						msg(f'Transaction sent: {coin_txid.hl()}')
-						assert ret == coin_txid, f'txid mismatch (after sending) ({ret} != {coin_txid})'
+						if ret != coin_txid:
+							die('TxIDMismatch', f'txid mismatch (after sending) ({ret} != {coin_txid})')
 					sent_status = 'no_confirm_post_send'
 
 				if cfg.wait and sent_status:

+ 15 - 2
mmgen/util.py

@@ -441,9 +441,19 @@ def get_subclasses(cls, *, names=False):
 			yield from gen(i)
 	return tuple((c.__name__ for c in gen(cls)) if names else gen(cls))
 
-def async_run(coro):
+def async_run(cfg, func, *, args=(), kwargs={}):
 	import asyncio
-	return asyncio.run(coro)
+	if cfg.rpc_backend == 'aiohttp':
+		async def func2():
+			import aiohttp
+			connector = aiohttp.TCPConnector(limit_per_host=cfg.aiohttp_rpc_queue_len)
+			async with aiohttp.ClientSession(
+					headers = {'Content-Type': 'application/json'},
+					connector = connector) as cfg.aiohttp_session:
+				return await func(*args, **kwargs)
+		return asyncio.run(func2())
+	else:
+		return asyncio.run(func(*args, **kwargs))
 
 def wrap_ripemd160(called=[]):
 	if not called:
@@ -487,3 +497,6 @@ def cached_property(orig_func):
 			setattr(self, attr_name, orig_func(self))
 		return getattr(self, attr_name)
 	return new_func
+
+def isAsync(func):
+	return bool(func.__code__.co_flags & 128)

+ 1 - 1
setup.cfg

@@ -62,7 +62,7 @@ install_requires =
 	cryptography
 	pynacl
 	ecdsa
-	aiohttp==3.12.9 # in later versions, TCPConnector.close() is a coroutine
+	aiohttp
 	requests
 	pexpect
 	lxml

+ 1 - 1
test/cmdtest.py

@@ -343,7 +343,7 @@ if __name__ == '__main__':
 		if hasattr(tr, 'tg'):
 			del tr.tg
 		del tr
-		# if cmdtest.py itself is running under exec_wrapper, re-raise so exec_wrapper can handle exception:
+		# if cmdtest.py itself is running under exec_wrapper, re-raise so wrapper can handle exception:
 		if os.getenv('MMGEN_EXEC_WRAPPER') or not os.getenv('MMGEN_IGNORE_TEST_PY_EXCEPTION'):
 			raise
 		die(1, red('Test script exited with error'))

+ 2 - 2
test/cmdtest_d/automount_eth.py

@@ -91,8 +91,8 @@ class CmdTestAutosignETH(CmdTestAutosignThreaded, CmdTestEthdev, CmdTestEthdevMe
 	def token_addrimport(self):
 		return self._token_addrimport('token_addr1', '11-13', expect='3/3')
 
-	def token_fund_user(self):
-		return self._token_transfer_ops(op='fund_user', mm_idxs=[11])
+	async def token_fund_user(self):
+		return await self._token_transfer_ops(op='fund_user', mm_idxs=[11])
 
 	def token_bal1(self):
 		return self._token_bal_check(pat=r':E:11\s+1000\s+54\.321\s+')

+ 6 - 6
test/cmdtest_d/ethbump.py

@@ -70,8 +70,8 @@ class CmdTestEthBumpMethods:
 			fee_desc = 'or gas price',
 			bad_fee = '0.9G')
 
-	def _token_fund_user(self, *, mm_idxs):
-		return self._token_transfer_ops(
+	async def _token_fund_user(self, *, mm_idxs):
+		return await self._token_transfer_ops(
 			op          = 'fund_user',
 			mm_idxs     = mm_idxs,
 			token_addr  = 'token_addr1',
@@ -330,11 +330,11 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe
 	def bal4(self):
 		return self._bal_check(pat=rf'{dfl_sid}:E:12\s+4444\.3333\s')
 
-	def token_fund_user1(self):
-		return self._token_fund_user(mm_idxs=[1])
+	async def token_fund_user1(self):
+		return await self._token_fund_user(mm_idxs=[1])
 
-	def token_fund_user11(self):
-		return self._token_fund_user(mm_idxs=[11])
+	async def token_fund_user11(self):
+		return await self._token_fund_user(mm_idxs=[11])
 
 	def token_txdo1(self):
 		return self._token_txcreate(cmd='txdo', args=[f'{dfl_sid}:E:2,1.23456', dfl_words_file])

+ 4 - 4
test/cmdtest_d/ethdev.py

@@ -1390,11 +1390,11 @@ class CmdTestEthdev(CmdTestEthdevMethods, CmdTestBase, CmdTestShared):
 	def bal6(self):
 		return self.bal5()
 
-	def token_fund_users(self):
-		return self._token_transfer_ops(op='fund_user', mm_idxs=[11, 21])
+	async def token_fund_users(self):
+		return await self._token_transfer_ops(op='fund_user', mm_idxs=[11, 21])
 
-	def token_user_bals(self):
-		return self._token_transfer_ops(op='show_bals', mm_idxs=[11, 21])
+	async def token_user_bals(self):
+		return await self._token_transfer_ops(op='show_bals', mm_idxs=[11, 21])
 
 	def token_addrgen(self):
 		return self._token_addrgen(mm_idxs=[11, 21], naddrs=3)

+ 4 - 4
test/cmdtest_d/ethswap.py

@@ -71,8 +71,8 @@ class CmdTestEthSwapMethods:
 			gas = 1_000_000,
 			fn  = f'{self.tmpdir}/THORChain_Router.bin')
 
-	def token_fund_user(self):
-		return self._token_transfer_ops(
+	async def token_fund_user(self):
+		return await self._token_transfer_ops(
 			op          = 'fund_user',
 			mm_idxs     = [1, 2, 12],
 			token_addr  = 'token_addr1',
@@ -445,10 +445,10 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
 	def swaptxstatus1(self):
 		return self._swaptxsend(add_opts=['--verbose', '--status'], status=True)
 
-	def swaptxmemo4(self):
+	async def swaptxmemo4(self):
 		import time
 		time.sleep(1)
-		return self._check_token_swaptx_memo('=:b:mkQsXA7mqDtnUpkaXMbDtAL1KMeof4GPw3:0/3/0')
+		return await self._check_token_swaptx_memo('=:b:mkQsXA7mqDtnUpkaXMbDtAL1KMeof4GPw3:0/3/0')
 
 	def swaptxreceipt4(self):
 		return self._swaptxsend(add_opts=['--receipt'], spawn_only=True)

+ 6 - 8
test/cmdtest_d/include/runner.py

@@ -16,7 +16,7 @@ import sys, os, time, asyncio
 
 from mmgen.cfg import gc
 from mmgen.color import red, yellow, green, blue, cyan, gray, nocolor
-from mmgen.util import msg, Msg, rmsg, ymsg, bmsg, die, suf, make_timestr
+from mmgen.util import msg, Msg, rmsg, ymsg, bmsg, die, suf, make_timestr, isAsync
 
 from ...include.common import (
 	cmdtest_py_log_fn,
@@ -346,9 +346,8 @@ class CmdTestRunner:
 									self.check_needs_rerun(cmdname, build=True)
 								except Exception as e: # allow calling of functions not in cmd_group
 									if isinstance(e, KeyError) and e.args[0] == cmdname:
-										ret = getattr(self.tg, cmdname)()
-										if type(ret).__name__ == 'coroutine':
-											ret = asyncio.run(ret)
+										func = getattr(self.tg, cmdname)
+										ret = asyncio.run(func()) if isAsync(func) else func()
 										self.process_retval(cmdname, ret)
 									else:
 										raise
@@ -470,13 +469,12 @@ class CmdTestRunner:
 				if k in self.gm.cfg_attrs:
 					setattr(self.tg, k, test_cfg[k])
 
-		ret = getattr(self.tg, cmd)(*arg_list) # run the test
+		func = getattr(self.tg, cmd)
+		ret = asyncio.run(func(*arg_list)) if isAsync(func) else func(*arg_list) # run the test
+
 		if sub:
 			return ret
 
-		if type(ret).__name__ == 'coroutine':
-			ret = asyncio.run(ret)
-
 		self.process_retval(cmd, ret)
 
 		if self.cfg.profile:

+ 3 - 3
test/cmdtest_d/main.py

@@ -20,9 +20,9 @@
 test.cmdtest_d.main: Basic operations tests for the cmdtest.py test suite
 """
 
-import sys, os
+import sys, os, asyncio
 
-from mmgen.util import msg, msg_r, async_run, capfirst, get_extension, die
+from mmgen.util import msg, msg_r, capfirst, get_extension, die
 from mmgen.color import green, cyan, gray
 from mmgen.fileutil import get_data_from_file, write_data_to_file
 from mmgen.wallet import get_wallet_cls
@@ -388,7 +388,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 	@property
 	def lbl_id(self):
 		if not hasattr(self, '_lbl_id'):
-			rpc = async_run(rpc_init(self.cfg, self.proto))
+			rpc = asyncio.run(rpc_init(self.cfg, self.proto))
 			self._lbl_id = ('account', 'label')['label_api' in rpc.caps]
 		return self._lbl_id
 

+ 4 - 4
test/cmdtest_d/regtest.py

@@ -719,14 +719,14 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 			tweaks = ['confirm_chg_non_mmgen'],
 			skip_passphrase = skip_passphrase)
 
-	def fund_bob_deterministic(self):
-		return self.fund_wallet_deterministic(f'{self._user_sid("bob")}:C:1', '1-11')
+	async def fund_bob_deterministic(self):
+		return await self.fund_wallet_deterministic(f'{self._user_sid("bob")}:C:1', '1-11')
 
-	def fund_alice_deterministic(self):
+	async def fund_alice_deterministic(self):
 		sid = self._user_sid('alice')
 		mmtype = ('L', 'S')[self.proto.cap('segwit')]
 		addr = self.get_addr_from_addrlist('alice', sid, mmtype, 0, addr_range='1-5')
-		return self.fund_wallet_deterministic(addr, '1-11', skip_passphrase=True)
+		return await self.fund_wallet_deterministic(addr, '1-11', skip_passphrase=True)
 
 	def generate_extra_deterministic(self):
 		if not self.deterministic:

+ 6 - 2
test/cmdtest_d/rune.py

@@ -136,14 +136,18 @@ class CmdTestRune(CmdTestEthdevMethods, CmdTestBase, CmdTestShared):
 			t.expect('can be sent')
 		else:
 			t.expect('to confirm: ', 'YES\n')
-			t.written_to_file('Sent transaction')
+			t.expect('Transaction sent: ')
+			if t.expect(['written to file', 'txid mismatch']):
+				self.tr.warn('txid mismatch')
+				return 'ok'
 		return t
 
 	def txhex1(self):
 		t = self._txsend(add_opts=[f'--dump-hex={self.txhex_file}'], dump_hex=True)
 		t.read()
 		txhex = get_data_from_file(self.cfg, self.txhex_file, silent=True)
-		assert md5(txhex.encode()).hexdigest()[:8] == self.txhex_chksum
+		if md5(txhex.encode()).hexdigest()[:8] != self.txhex_chksum:
+			self.tr.warn('txid mismatch')
 		return t
 
 	def rpc_server_stop(self):

+ 8 - 2
test/cmdtest_d/runeswap.py

@@ -119,7 +119,11 @@ class CmdTestRuneSwapRune(CmdTestSwapMethods, CmdTestRune):
 		return self._swaptxsign()
 
 	def swaptxsend1(self):
-		return self._swaptxsend(add_opts=[f'--proxy=localhost:{TestProxy.port}'])
+		t = self._swaptxsend(add_opts=[f'--proxy=localhost:{TestProxy.port}'])
+		if t.expect(['written to file', 'txid mismatch']):
+			self.tr.parent_group.tr.warn('txid mismatch')
+			return 'ok'
+		return t
 
 	def swaptxstatus1(self):
 		return self._swaptxsend(add_opts=['--verbose', '--status'], status=True)
@@ -131,5 +135,7 @@ class CmdTestRuneSwapRune(CmdTestSwapMethods, CmdTestRune):
 		t = self._swaptxsend(add_opts=[f'--dump-hex={self.txhex_file}'], dump_hex=True)
 		t.read()
 		txhex = get_data_from_file(self.cfg, self.txhex_file, silent=True)
-		assert md5(txhex.encode()).hexdigest()[:8] == self.txhex_chksum
+		if md5(txhex.encode()).hexdigest()[:8] != self.txhex_chksum:
+			self.tr.parent_group.tr.warn('txid mismatch')
+			return 'ok'
 		return t

+ 1 - 0
test/cmdtest_d/swap.py

@@ -318,6 +318,7 @@ class CmdTestSwapMethods:
 		t = trunner
 		ret = CmdTestRunner(cfg, t.repo_root, t.data_dir, t.trash_dir, t.trash_dir2)
 		ret.init_group(self.cross_group)
+		ret.parent_group = self
 		return ret
 
 class CmdTestSwap(CmdTestSwapMethods, CmdTestRegtest, CmdTestAutosignThreaded):

+ 22 - 27
test/cmdtest_d/xmr_autosign.py

@@ -13,10 +13,9 @@
 test.cmdtest_d.xmr_autosign: xmr autosigning tests for the cmdtest.py test suite
 """
 
-import re
+import re, asyncio
 
 from mmgen.color import blue, cyan, brown
-from mmgen.util import async_run
 
 from ..include.common import imsg, silence, end_silence
 from .include.common import get_file_with_ext
@@ -139,8 +138,8 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 		kal.file.write(ask_overwrite=False)
 		fn = get_file_with_ext(data.udir, 'akeys')
 		m = op('create', self.alice_cfg, fn, '1-2')
-		async_run(m.main())
-		async_run(m.stop_wallet_daemon())
+		asyncio.run(m.main())
+		asyncio.run(m.stop_wallet_daemon())
 		end_silence()
 		return 'ok'
 
@@ -214,17 +213,17 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 	def delete_dump_files(self):
 		return self._delete_files('.dump')
 
-	def fund_alice1(self):
-		return self.fund_alice(wallet=1)
+	async def fund_alice1(self):
+		return await self.fund_alice(wallet=1)
 
-	def check_bal_alice1(self):
-		return self.check_bal_alice(wallet=1)
+	async def check_bal_alice1(self):
+		return await self.check_bal_alice(wallet=1)
 
-	def fund_alice2(self):
-		return self.fund_alice(wallet=2)
+	async def fund_alice2(self):
+		return await self.fund_alice(wallet=2)
 
-	def check_bal_alice2(self):
-		return self.check_bal_alice(wallet=2)
+	async def check_bal_alice2(self):
+		return await self.check_bal_alice(wallet=2)
 
 	def autosign_setup(self):
 		return self.run_setup(
@@ -340,26 +339,19 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 			'1-2',
 			lambda n, b, ub: b == ub and ((n == 1 and 0.8 < b < 0.86) or (n == 2 and b > 1.23)))
 
-	def _mine_chk(self, desc):
-		bal_type = {'locked':'b', 'unlocked':'ub'}[desc]
-		return self.mine_chk(
-			'alice', 1, 0,
-			lambda x: 0 < getattr(x, bal_type) < 1.234567891234,
-			f'{desc} balance 0 < 1.234567891234')
+	async def submit_transfer_tx1(self):
+		return await self._submit_transfer_tx()
 
-	def submit_transfer_tx1(self):
-		return self._submit_transfer_tx()
-
-	def resubmit_transfer_tx1(self):
-		return self._submit_transfer_tx(
+	async def resubmit_transfer_tx1(self):
+		return await self._submit_transfer_tx(
 				relay_parm = self.tx_relay_daemon_proxy_parm,
 				op         = 'resubmit',
 				check_bal  = False)
 
-	def submit_transfer_tx2(self):
-		return self._submit_transfer_tx(relay_parm=self.tx_relay_daemon_parm)
+	async def submit_transfer_tx2(self):
+		return await self._submit_transfer_tx(relay_parm=self.tx_relay_daemon_parm)
 
-	def _submit_transfer_tx(self, relay_parm=None, ext=None, op='submit', check_bal=True):
+	async def _submit_transfer_tx(self, relay_parm=None, ext=None, op='submit', check_bal=True):
 		t = self._xmr_autosign_op(
 			op            = op,
 			add_opts      = [f'--tx-relay-daemon={relay_parm}'] if relay_parm else [],
@@ -372,7 +364,10 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 		self.remove_device_online() # device was inserted by _xmr_autosign_op()
 		if check_bal:
 			t.ok()
-			return self._mine_chk('unlocked')
+			return await self.mine_chk(
+				'alice', 1, 0,
+				lambda x: 0 < x.ub < 1.234567891234,
+				'unlocked balance 0 < 1.234567891234')
 		else:
 			return t
 

+ 12 - 12
test/cmdtest_d/xmrwallet.py

@@ -24,7 +24,7 @@ import sys, os, time, re, atexit, asyncio, shutil
 from subprocess import run, PIPE
 from collections import namedtuple
 
-from mmgen.util import async_run, capfirst, is_int, die, list_gen
+from mmgen.util import capfirst, is_int, die, list_gen
 from mmgen.obj import MMGenRange
 from mmgen.amt import XMRAmt
 from mmgen.addrlist import ViewKeyAddrList, KeyAddrList, AddrIdxList
@@ -51,7 +51,7 @@ def stop_daemons(self):
 		v.md.stop()
 
 def stop_miner_wallet_daemon(self):
-	async_run(self.users['miner'].wd_rpc.stop_daemon())
+	asyncio.run(self.users['miner'].wd_rpc.stop_daemon())
 
 class CmdTestXMRWallet(CmdTestBase):
 	"""
@@ -505,25 +505,25 @@ class CmdTestXMRWallet(CmdTestBase):
 
 		return t if do_ret else t.ok()
 
-	def sweep_to_wallet(self):
+	async def sweep_to_wallet(self):
 		self.do_op('sweep', 'alice', '1:0,2')
-		return self.mine_chk('alice', 2, 1, lambda x: x.ub > 1, 'unlocked balance > 1')
+		return await self.mine_chk('alice', 2, 1, lambda x: x.ub > 1, 'unlocked balance > 1')
 
-	def sweep_to_account(self):
+	async def sweep_to_account(self):
 		self.do_op('sweep', 'alice', '2:1,2:0', use_existing=True)
-		return self.mine_chk('alice', 2, 0, lambda x: x.ub > 1, 'unlocked balance > 1')
+		return await self.mine_chk('alice', 2, 0, lambda x: x.ub > 1, 'unlocked balance > 1')
 
-	def sweep_to_wallet_account(self):
+	async def sweep_to_wallet_account(self):
 		self.do_op('sweep', 'alice', '2:0,3:0', use_existing=True, add_opts=['-Ee', '--full-address'])
-		return self.mine_chk('alice', 3, 0, lambda x: x.ub > 1, 'unlocked balance > 1')
+		return await self.mine_chk('alice', 3, 0, lambda x: x.ub > 1, 'unlocked balance > 1')
 
-	def sweep_to_wallet_account_proxy(self):
+	async def sweep_to_wallet_account_proxy(self):
 		self.do_op('sweep', 'alice', '3:0,2:1', self.tx_relay_daemon_proxy_parm, add_opts=['--priority=3', '-Ee'])
-		return self.mine_chk('alice', 2, 1, lambda x: x.ub > 1, 'unlocked balance > 1')
+		return await self.mine_chk('alice', 2, 1, lambda x: x.ub > 1, 'unlocked balance > 1')
 
-	def sweep_to_same_account_noproxy(self):
+	async def sweep_to_same_account_noproxy(self):
 		self.do_op('sweep', 'alice', '2:1', self.tx_relay_daemon_parm)
-		return self.mine_chk('alice', 2, 1, lambda x: x.ub > 0.9, 'unlocked balance > 0.9')
+		return await self.mine_chk('alice', 2, 1, lambda x: x.ub > 0.9, 'unlocked balance > 0.9')
 
 	async def transfer_to_miner_proxy(self):
 		addr = read_from_file(self.users['miner'].addrfile_fs.format(2))

+ 24 - 23
test/daemontest_d/rpc.py

@@ -4,11 +4,11 @@
 test.daemontest_d.rpc: RPC unit test for the MMGen suite
 """
 
-import sys, os
+import sys, os, asyncio
 
 from mmgen.cfg import Config
 from mmgen.color import yellow, cyan
-from mmgen.util import msg, gmsg, make_timestr, pp_fmt, die
+from mmgen.util import msg, gmsg, make_timestr, pp_fmt, die, async_run
 from mmgen.protocol import init_proto
 from mmgen.rpc import rpc_init
 from mmgen.daemon import CoinDaemon
@@ -124,9 +124,9 @@ class init_test:
 
 	etc = eth
 
-async def run_test(network_ids, test_cf_auth=False, daemon_ids=None, cfg_override=None):
+def run_test(network_ids, test_cf_auth=False, daemon_ids=None, cfg_override=None):
 
-	async def do_test(d, cfg):
+	def do_test(d, cfg):
 
 		d.wait = True
 
@@ -140,17 +140,18 @@ async def run_test(network_ids, test_cf_auth=False, daemon_ids=None, cfg_overrid
 
 		for n, backend in enumerate(cfg._autoset_opts['rpc_backend'].choices):
 			test = getattr(init_test, d.proto.coin.lower())
-			rpc = await test(cfg, d, backend, cfg_override)
+			cfg_b = Config({'_clone': cfg, 'rpc_backend': backend})
+			rpc = async_run(cfg_b, test, args=(cfg_b, d, backend, cfg_override))
 			if not n and cfg.verbose:
-				await print_daemon_info(rpc)
+				asyncio.run(print_daemon_info(rpc))
 
 		if not cfg.no_daemon_stop:
 			d.stop()
 			d.remove_datadir()
 
 		if test_cf_auth and sys.platform != 'win32':
-			await cfg_file_auth_test(cfg, d)
-			await cfg_file_auth_test(cfg, d, bad_auth=True)
+			asyncio.run(cfg_file_auth_test(cfg, d))
+			asyncio.run(cfg_file_auth_test(cfg, d, bad_auth=True))
 
 		qmsg('')
 
@@ -161,7 +162,7 @@ async def run_test(network_ids, test_cf_auth=False, daemon_ids=None, cfg_overrid
 		all_ids = CoinDaemon.get_daemon_ids(my_cfg, proto.coin)
 		ids = set(daemon_ids) & set(all_ids) if daemon_ids else all_ids
 		for daemon_id in ids:
-			await do_test(CoinDaemon(my_cfg, proto=proto, test_suite=True, daemon_id=daemon_id), my_cfg)
+			do_test(CoinDaemon(my_cfg, proto=proto, test_suite=True, daemon_id=daemon_id), my_cfg)
 
 	return True
 
@@ -172,8 +173,8 @@ class unit_tests:
 	riscv_skip = ('parity',) # no prebuilt binaries for RISC-V
 	fast_skip = ('reth', 'erigon')
 
-	async def btc(self, name, ut):
-		return await run_test(
+	def btc(self, name, ut):
+		return run_test(
 			['btc', 'btc_tn'],
 			test_cf_auth = True,
 			cfg_override = {
@@ -186,15 +187,15 @@ class unit_tests:
 				'eth_mainnet_chain_names': ['also', 'ignored'],
 		})
 
-	async def ltc(self, name, ut):
-		return await run_test(['ltc', 'ltc_tn'], test_cf_auth=True)
+	def ltc(self, name, ut):
+		return run_test(['ltc', 'ltc_tn'], test_cf_auth=True)
 
-	async def bch(self, name, ut):
-		return await run_test(['bch', 'bch_tn'], test_cf_auth=True)
+	def bch(self, name, ut):
+		return run_test(['bch', 'bch_tn'], test_cf_auth=True)
 
-	async def geth(self, name, ut):
+	def geth(self, name, ut):
 		# mainnet returns EIP-155 error on empty blockchain:
-		return await run_test(
+		return run_test(
 			['eth_tn', 'eth_rt'],
 			daemon_ids = ['geth'],
 			cfg_override = {
@@ -206,17 +207,17 @@ class unit_tests:
 				'eth_testnet_chain_names': ['goerli', 'holesky', 'foo', 'bar', 'baz'],
 		})
 
-	async def reth(self, name, ut):
-		return await run_test(['eth', 'eth_rt'], daemon_ids=['reth']) # TODO: eth_tn
+	def reth(self, name, ut):
+		return run_test(['eth', 'eth_rt'], daemon_ids=['reth']) # TODO: eth_tn
 
-	async def erigon(self, name, ut):
-		return await run_test(['eth', 'eth_tn', 'eth_rt'], daemon_ids=['erigon'])
+	def erigon(self, name, ut):
+		return run_test(['eth', 'eth_tn', 'eth_rt'], daemon_ids=['erigon'])
 
-	async def parity(self, name, ut):
+	def parity(self, name, ut):
 		if in_nix_environment() and not test_exec('parity --help'):
 			ut.skip_msg('Nix environment')
 			return True
-		return await run_test(['etc'])
+		return run_test(['etc'])
 
 	async def xmrwallet(self, name, ut):
 

+ 7 - 7
test/include/unit_test.py

@@ -32,7 +32,7 @@ if not os.getenv('MMGEN_DEVTOOLS'):
 
 from mmgen.cfg import Config, gc, gv
 from mmgen.color import gray, brown, orange, yellow, red
-from mmgen.util import msg, msg_r, gmsg, ymsg, Msg
+from mmgen.util import msg, msg_r, gmsg, ymsg, Msg, isAsync
 
 from test.include.common import set_globals, end_msg
 
@@ -151,9 +151,7 @@ class UnitTestHelpers:
 		for (desc, exc_chk, emsg_chk, func) in data:
 			try:
 				cfg._util.vmsg_r('  {}{:{w}}'.format(pfx, desc+':', w=desc_w+1))
-				ret = func()
-				if type(ret).__name__ == 'coroutine':
-					asyncio.run(ret)
+				asyncio.run(func()) if isAsync(func) else func()
 			except Exception as e:
 				exc = type(e).__name__
 				emsg = e.args[0] if e.args else '(unspecified error)'
@@ -187,9 +185,11 @@ def run_test(test, subtest=None):
 				elif not cfg.quiet:
 					msg_r(f'Testing {func.__defaults__[0]}...')
 
-			ret = func(test, UnitTestHelpers(subtest))
-			if type(ret).__name__ == 'coroutine':
-				ret = asyncio.run(ret)
+			if isAsync(func):
+				ret = asyncio.run(func(test, UnitTestHelpers(subtest)))
+			else:
+				ret = func(test, UnitTestHelpers(subtest))
+
 			if do_desc and not cfg.quiet:
 				msg('OK\n' if cfg.verbose else 'OK')
 		except:

+ 2 - 4
test/tooltest2.py

@@ -36,7 +36,7 @@ from test.include.common import set_globals, end_msg, init_coverage
 from mmgen import main_tool
 from mmgen.cfg import Config
 from mmgen.color import green, blue, purple, cyan, gray
-from mmgen.util import msg, msg_r, Msg, die
+from mmgen.util import msg, msg_r, Msg, die, isAsync
 
 skipped_tests = ['mn2hex_interactive']
 coin_dependent_groups = ('Coin', 'File')
@@ -134,9 +134,7 @@ def call_method(cls, method, cmd_name, args, mmtype, stdin_input):
 			vmsg(f'Input: {stdin_input!r}')
 			sys.exit(0)
 	else:
-		ret = method(*aargs, **kwargs)
-		if type(ret).__name__ == 'coroutine':
-			ret = asyncio.run(ret)
+		ret = asyncio.run(method(*aargs, **kwargs)) if isAsync(method) else method(*aargs, **kwargs)
 		cfg._set_quiet(oq_save)
 		return ret