9 Commits 76a34a05b5 ... 3faef52096

Author SHA1 Message Date
  The MMGen Project 3faef52096 mmgen-txsend: unrecognize the --coin and --token options 9 months ago
  The MMGen Project 215eb43e29 mmgen-txsign: unrecognize the --coin and --token options 9 months ago
  The MMGen Project 816781ec7a minor changes, whitespace 9 months ago
  The MMGen Project 6a369f79d7 test suite: replace invalid transaction file 9 months ago
  The MMGen Project 14b2eb48ab nix/shell.nix: add PYTHONPYCACHEPREFIX to environment 9 months ago
  The MMGen Project fe9664412d cmdtest.py: reimplement etherscan server using WSGI framework 9 months ago
  The MMGen Project 90544d677c cmdtest.py: reimplement thornode server using WSGI framework 9 months ago
  The MMGen Project 88f204a0bd cmdtest.py: new WSGI http server framework 9 months ago
  The MMGen Project 79e008ce43 cmdtest.spawn_wrapper(): make `cmd` a keyword arg 9 months ago

+ 1 - 0
MANIFEST.in

@@ -13,6 +13,7 @@ include nix/*
 
 include test/*.py
 include test/*/*.py
+include test/*/*/*.py
 include test/ref/*
 include test/ref/*/*
 include test/ref/*/*/*/*

+ 2 - 2
mmgen/cfg.py

@@ -77,8 +77,8 @@ class GlobalConstants(Lockable):
 		'txbump':       _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
 		'txcreate':     _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
 		'txdo':         _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
-		'txsend':       _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
-		'txsign':       _cc(True,  True,  True,  None,    ['tw'],  'lmw'),
+		'txsend':       _cc(True,  True,  False, '-rRb',  ['tw'],  'lmw'),
+		'txsign':       _cc(True,  True,  False, '-rRb',  ['tw'],  'lmw'),
 		'walletchk':    _cc(False, False, False, None,    [],      'lmw'),
 		'walletconv':   _cc(False, False, False, None,    [],      'lmw'),
 		'walletgen':    _cc(False, False, False, None,    [],      'lmw'),

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev21
+15.1.dev22

+ 9 - 5
mmgen/main_txsend.py

@@ -78,10 +78,6 @@ if cfg.mark_sent and not cfg.autosign:
 if cfg.test and cfg.dump_hex:
 	die(1, '--test cannot be used in combination with --dump-hex')
 
-if cfg.tx_proxy:
-	from .tx.tx_proxy import check_client
-	check_client(cfg)
-
 if cfg.dump_hex and cfg.dump_hex != '-':
 	from .fileutil import check_outfile_dir
 	check_outfile_dir(cfg.dump_hex)
@@ -125,6 +121,8 @@ async def post_send(tx):
 
 async def main():
 
+	global cfg
+
 	if cfg.status and cfg.autosign:
 		tx = await si.get_last_created()
 	else:
@@ -134,8 +132,14 @@ async def main():
 			automount  = cfg.autosign,
 			quiet_open = True)
 
+	cfg = Config({'_clone': cfg, 'proto': tx.proto, 'coin': tx.proto.coin})
+
+	if cfg.tx_proxy:
+		from .tx.tx_proxy import check_client
+		check_client(cfg)
+
 	from .rpc import rpc_init
-	tx.rpc = await rpc_init(cfg, tx.proto)
+	tx.rpc = await rpc_init(cfg)
 
 	cfg._util.vmsg(f'Getting {tx.desc} ‘{tx.infile}’')
 

+ 12 - 5
mmgen/proto/eth/tx/unsigned.py

@@ -15,7 +15,7 @@ proto.eth.tx.unsigned: Ethereum unsigned transaction class
 import json
 
 from ....tx import unsigned as TxBase
-from ....util import msg, msg_r
+from ....util import msg, msg_r, die
 from ....obj import CoinTxID, ETHNonce, Int, HexStr
 from ....addr import CoinAddr, TokenAddr
 from ..contract import Token
@@ -74,11 +74,18 @@ class Unsigned(Completed, TxBase.Unsigned):
 
 		o = self.txobj
 
-		m = 'mismatch -- a compromised online installation may have altered your serialized data!'
-		assert o['from'] == self.inputs[0].addr, f'from_addr {m}'
+		def do_mismatch_err(io, j, k, desc):
+			m = 'A compromised online installation may have altered your serialized data!'
+			fs = '\n{} mismatch!\n{}\n  orig:       {}\n  serialized: {}'
+			die(3, fs.format(desc.upper(), m, getattr(io[0], k), o[j]))
+
+		if o['from'] != self.inputs[0].addr:
+			do_mismatch_err(self.inputs, 'from', 'addr', 'from-address')
 		if self.outputs:
-			assert o['to'] == self.outputs[0].addr, f'to_addr {m}'
-			assert o['amt'] == self.outputs[0].amt, f'to_amt {m}'
+			if o['to'] != self.outputs[0].addr:
+				do_mismatch_err(self.outputs, 'to', 'addr', 'to-address')
+			if o['amt'] != self.outputs[0].amt:
+				do_mismatch_err(self.outputs, 'amt', 'amt', 'amount')
 
 		msg_r(f'Signing transaction{tx_num_str}...')
 

+ 1 - 0
nix/shell.nix

@@ -38,6 +38,7 @@ pkgs.mkShellNoCC {
 
         pwd=$(pwd)
         export PYTHONPATH=$pwd
+        export PYTHONPYCACHEPREFIX=$HOME/.cache/pycache
         export PATH=$pwd/cmds:$pwd/.bin-override:$HOME/.local/bin:$PATH
         export LANG="en_US.UTF-8"
 

+ 7 - 2
test/cmdtest.py

@@ -543,7 +543,7 @@ class CmdTestRunner:
 
 	def spawn_wrapper(
 			self,
-			cmd,
+			cmd             = '',
 			args            = [],
 			extra_desc      = '',
 			no_output       = False,
@@ -569,11 +569,16 @@ class CmdTestRunner:
 			cmd if cfg.system # cfg.system is broken for main test group with overlay tree
 			else os.path.relpath(os.path.join(repo_root, cmd_dir, cmd)))
 
+		passthru_opts = (
+			self.passthru_opts if not no_passthru_opts else
+			[] if no_passthru_opts is True else
+			[o for o in self.passthru_opts if o[2:].split('=')[0] not in no_passthru_opts])
+
 		args = (
 			self.pre_args +
 			([] if no_exec_wrapper else ['scripts/exec_wrapper.py']) +
 			[cmd_path] +
-			([] if no_passthru_opts else self.passthru_opts) +
+			passthru_opts +
 			args)
 
 		try:

+ 3 - 1
test/cmdtest_d/ct_automount.py

@@ -107,6 +107,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		t = self.spawn(
 				'mmgen-txsend',
 				['--quiet', '--abort'],
+				no_passthru_opts = ['coin'],
 				exit_val = 2 if err else 1 if send_resp == 'n' else None)
 		if err:
 			t.expect(expect)
@@ -173,6 +174,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 		t = self.spawn(
 				'mmgen-txsend',
 				['--alice', '--autosign', '--status', '--verbose'],
+				no_passthru_opts = ['coin'],
 				exit_val = exit_val)
 		t.expect(expect)
 		if not exit_val:
@@ -199,7 +201,7 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
 
 	def alice_txsend_bad_no_unsent(self):
 		self.insert_device_online()
-		t = self.spawn('mmgen-txsend', ['--quiet', '--autosign'], exit_val=2)
+		t = self.spawn('mmgen-txsend', ['--quiet', '--autosign'], exit_val=2, no_passthru_opts=['coin'])
 		t.expect('No unsent transactions')
 		t.read()
 		self.remove_device_online()

+ 2 - 2
test/cmdtest_d/ct_automount_eth.py

@@ -99,7 +99,7 @@ class CmdTestAutosignETH(CmdTestAutosignThreaded, CmdTestEthdev):
 	def send_tx(self, add_args=[]):
 		self._wait_signed('transaction')
 		self.insert_device_online()
-		t = self.spawn('mmgen-txsend', self.txop_opts + add_args)
+		t = self.spawn('mmgen-txsend', self.txop_opts + add_args, no_passthru_opts=['coin'])
 		t.view_tx('t')
 		t.expect('(y/N): ', 'n')
 		self._do_confirm_send(t, quiet=True)
@@ -140,4 +140,4 @@ class CmdTestAutosignETH(CmdTestAutosignThreaded, CmdTestEthdev):
 		return t
 
 	def send_token_tx(self):
-		return self.send_tx(add_args=['--token=MM1'])
+		return self.send_tx()

+ 13 - 10
test/cmdtest_d/ct_autosign.py

@@ -181,12 +181,12 @@ class CmdTestAutosignBase(CmdTestBase):
 			pass
 
 	def start_daemons(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		start_test_daemons(*self.network_ids)
 		return 'ok'
 
 	def stop_daemons(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		stop_test_daemons(*self.network_ids, remove_datadir=True)
 		return 'ok'
 
@@ -378,7 +378,7 @@ class CmdTestAutosignClean(CmdTestAutosignBase):
 
 	def _clean(self, coins):
 
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 
 		self.insert_device()
 
@@ -515,7 +515,8 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
 			else [])
 
 		self.insert_device_online()
-		t = self.spawn('mmgen-txsend', [f'--{user}', '--quiet', '--autosign'] + extra_opt)
+		t = self.spawn('mmgen-txsend',
+			[f'--{user}', '--quiet', '--autosign'] + extra_opt, no_passthru_opts=['coin'])
 
 		if mark_sent:
 			t.written_to_file('Sent automount transaction')
@@ -589,7 +590,7 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
 		return self._mount_ops('asi_online', 'do_umount', *args, **kwargs)
 
 	async def txview(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		self.insert_device()
 		self.do_mount()
 		src = Path(self.asi.txauto_dir)
@@ -692,8 +693,10 @@ class CmdTestAutosign(CmdTestAutosignBase):
 		self.bad_msg_count = 0
 
 		if self.simulate_led:
-			LEDControl.create_dummy_control_files()
 			db = LEDControl.boards['dummy']
+			for fn in (db.control, db.trigger):
+				run(f'sudo rm -f {fn}'.split(), check=True)
+			LEDControl.create_dummy_control_files()
 			usrgrp = {'linux': 'root:root', 'darwin': 'root:wheel'}[sys.platform]
 			for fn in (db.control, db.trigger): # trigger the auto-chmod feature
 				run(f'sudo chmod 644 {fn}'.split(), check=True)
@@ -754,7 +757,7 @@ class CmdTestAutosign(CmdTestAutosignBase):
 		return t
 
 	def copy_tx_files(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		return self.tx_file_ops('copy')
 
 	def remove_signed_txfiles(self):
@@ -822,7 +825,7 @@ class CmdTestAutosign(CmdTestAutosignBase):
 		self.insert_device()
 		self.do_mount()
 		# create or delete 2 bad tx files
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		fns = [joinpath(self.asi.tx_dir, f'bad{n}.rawtx') for n in (1, 2)]
 		if op == 'create':
 			for fn in fns:
@@ -853,7 +856,7 @@ class CmdTestAutosign(CmdTestAutosignBase):
 		return self.msgfile_ops('remove_invalid')
 
 	def msgfile_ops(self, op):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		destdir = joinpath(self.asi.mountpoint, 'msg')
 		self.insert_device()
 		self.do_mount()
@@ -1060,7 +1063,7 @@ class CmdTestAutosignLive(CmdTestAutosignBTC):
 			info_msg = 'Running ‘mmgen-autosign wait’'
 			insert_msg = 'Insert removable device '
 
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 
 		self.do_umount()
 		prompt_remove()

+ 1 - 1
test/cmdtest_d/ct_base.py

@@ -125,7 +125,7 @@ class CmdTestBase:
 		return [f'--cashaddr={val}'] if self.proto.coin == 'BCH' else []
 
 	def _kill_process_from_pid_file(self, fn, desc):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		pid = int(self.read_from_tmpfile(fn))
 		self.delete_tmpfile(fn)
 		from signal import SIGTERM

+ 51 - 34
test/cmdtest_d/ct_ethdev.py

@@ -58,7 +58,9 @@ from .common import (
 )
 from .ct_base import CmdTestBase
 from .ct_shared import CmdTestShared
-from .etherscan import run_etherscan_server
+from .httpd.etherscan import EtherscanServer
+
+etherscan_server = EtherscanServer()
 
 del_addrs = ('4', '1')
 dfl_sid = '98831F3A'
@@ -179,12 +181,6 @@ token_bals_getbalance = lambda k: {
 
 coin = cfg.coin
 
-def etherscan_server_start():
-	import threading
-	t = threading.Thread(target=run_etherscan_server, name='Etherscan server thread')
-	t.daemon = True
-	t.start()
-
 class CmdTestEthdev(CmdTestBase, CmdTestShared):
 	'Ethereum transacting, token deployment and tracking wallet operations'
 	networks = ('eth', 'etc')
@@ -244,8 +240,12 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		('txview1_sig',          'viewing the signed transaction'),
 		('tx_status0_bad',       'getting the transaction status'),
 		('txsign1_ni',           'signing the transaction (non-interactive)'),
+
+		('etherscan_server_start','starting the Etherscan server'),
 		('txsend_etherscan_test','sending the transaction via Etherscan (simulation, with --test)'),
 		('txsend_etherscan',     'sending the transaction via Etherscan (simulation)'),
+		('etherscan_server_stop','stopping the Etherscan server'),
+
 		('txsend1',              'sending the transaction'),
 		('bal1',                 f'the {coin} balance'),
 
@@ -459,9 +459,6 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		self.message = 'attack at dawn'
 		self.spawn_env['MMGEN_BOGUS_SEND'] = ''
 
-		if type(self) is CmdTestEthdev:
-			etherscan_server_start() # TODO: stop server when test group finishes executing
-
 	@property
 	async def rpc(self):
 		from mmgen.rpc import rpc_init
@@ -472,7 +469,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 			time.sleep(0.5)
 
 	async def setup(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 
 		d = self.daemon
 
@@ -718,19 +715,19 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		txfile = self.get_file_with_ext(ext, no_dot=True)
 		t = self.spawn(
 			'mmgen-txsign',
-			self.eth_args
-			+ [f'--coin={self.proto.coin}']
-			+ ['--rpc-host=bad_host'] # ETH signing must work without RPC
-			+ ([], ['--yes'])[ni]
-			+ ([f'--keys-from-file={keyfile}'] if dev_send else [])
-			+ add_args
-			+ [txfile, dfl_words_file])
+				self.eth_args
+				+ ['--rpc-host=bad_host'] # ETH signing must work without RPC
+				+ ([], ['--yes'])[ni]
+				+ ([f'--keys-from-file={keyfile}'] if dev_send else [])
+				+ add_args
+				+ [txfile, dfl_words_file],
+			no_passthru_opts = ['coin'])
 		return self.txsign_ui_common(t, ni=ni, has_label=True)
 
 	def txsend(self, ext='{}.regtest.sigtx', add_args=[], test=False):
 		ext = ext.format('-α' if cfg.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext, no_dot=True)
-		t = self.spawn('mmgen-txsend', self.eth_args + add_args + [txfile])
+		t = self.spawn('mmgen-txsend', self.eth_args + add_args + [txfile], no_passthru_opts=['coin'])
 		self.txsend_ui_common(
 			t,
 			quiet      = not cfg.debug,
@@ -778,10 +775,20 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		return self.tx_status(ext='{}.regtest.sigtx', expect_str='neither in mempool nor blockchain', exit_val=1)
 	def txsign1_ni(self):
 		return self.txsign(ni=True, dev_send=True)
+
+	def etherscan_server_start(self):
+		self.spawn(msg_only=True)
+		etherscan_server.start()
+		return 'ok'
 	def txsend_etherscan_test(self):
 		return self.txsend(add_args=['--tx-proxy=ether', '--test'], test='tx_proxy')
 	def txsend_etherscan(self):
 		return self.txsend(add_args=['--tx-proxy=ethersc'])
+	def etherscan_server_stop(self):
+		self.spawn(msg_only=True)
+		etherscan_server.stop()
+		return 'ok'
+
 	def txsend1(self):
 		return self.txsend()
 	def txview1_sig(self): # do after send so that TxID is displayed
@@ -816,6 +823,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		t = self.spawn(
 			'mmgen-txsend',
 			self.eth_args + add_args + ['--status', txfile],
+			no_passthru_opts = ['coin'],
 			exit_val = exit_val)
 		t.expect(expect_str)
 		if expect_str2:
@@ -848,7 +856,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		if not self.daemon.id == 'geth':
 			return 'skip'
 
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 
 		sig = '0x' + create_signature_mmgen()
 		sig_chk = await create_signature_rpc()
@@ -1002,7 +1010,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		if not self.using_solc:
 			imsg(f'Using precompiled contract data in {odir}')
 			return 'skip' if os.path.exists(odir) else False
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		cmd_args = [f'--{k}={v}' for k, v in list(token_data.items())]
 		imsg("Compiling solidity token contract '{}' with 'solc'".format(token_data['symbol']))
 		try:
@@ -1063,10 +1071,13 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 			t.written_to_file('transaction')
 			ext = '[0,8000]{}.regtest.rawtx'.format('-α' if cfg.debug_utf8 else '')
 			txfile = self.get_file_with_ext(ext, no_dot=True)
-			t = self.spawn('mmgen-txsign', self.eth_args + ['--yes', '-k', keyfile, txfile], no_msg=True)
+			t = self.spawn(
+				'mmgen-txsign',
+				self.eth_args + ['--yes', '-k', keyfile, txfile], no_msg=True, no_passthru_opts=['coin'])
 			self.txsign_ui_common(t, ni=True)
 			txfile = txfile.replace('.rawtx', '.sigtx')
-			t = self.spawn('mmgen-txsend', self.eth_args + [txfile], no_msg=True)
+			t = self.spawn('mmgen-txsend',
+				self.eth_args + [txfile], no_msg=True, no_passthru_opts=['coin'])
 
 		txid = self.txsend_ui_common(t,
 			caller = mmgen_cmd,
@@ -1103,7 +1114,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		return await self.token_deploy(num=2, key='Token',   gas=4_000_000)
 
 	async def token_transfer_ops(self, op, amt=1000, num_tokens=2):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		sid = dfl_sid
 		from mmgen.tool.wallet import tool_cmd
 		usr_mmaddrs = [f'{sid}:E:{i}' for i in (11, 21)][:num_tokens]
@@ -1214,18 +1225,25 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 	def token_bal1(self):
 		return self.token_bal(n='1')
 
-	def token_txcreate(self, args=[], token='', inputs='1', fee='50G', file_desc='Unsigned transaction'):
+	def token_txcreate(
+			self,
+			args      = [],
+			token     = '',
+			inputs    = '1',
+			fee       = '50G',
+			file_desc = 'Unsigned transaction'):
 		return self.txcreate_ui_common(
-			self.spawn('mmgen-txcreate', self.eth_args + [f'--token={token}', '-B', f'--fee={fee}'] + args),
+			self.spawn('mmgen-txcreate',
+				self.eth_args + [f'--token={token}', '-B', f'--fee={fee}'] + args),
 			menu              = [],
 			inputs            = inputs,
 			input_sels_prompt = 'to spend from',
 			add_comment       = tx_comment_lat_cyr_gr,
 			file_desc         = file_desc)
 	def token_txsign(self, ext='', token='', add_args=[], ni=True):
-		return self.txsign(ni=ni, ext=ext, add_args=[f'--token={token}'] + add_args)
+		return self.txsign(ni=ni, ext=ext, add_args=add_args)
 	def token_txsend(self, ext='', token=''):
-		return self.txsend(ext=ext, add_args=['--token='+token])
+		return self.txsend(ext=ext)
 
 	def token_txcreate1(self):
 		return self.token_txcreate(args=['98831F3A:E:12,1.23456'], token='mm1')
@@ -1245,7 +1263,6 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 	def tx_status3(self):
 		return self.tx_status(
 			ext         = '1.23456,50000]{}.regtest.sigtx',
-			add_args    = ['--token=mm1'],
 			expect_str  = 'successfully executed',
 			expect_str2 = 'has 1 confirmation')
 
@@ -1503,7 +1520,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		return t
 
 	async def twmove(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		from mmgen.tw.ctl import TwCtl
 		twctl = await TwCtl(cfg, self.proto, no_wallet_init=True)
 		imsg('Moving tracking wallet')
@@ -1526,7 +1543,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		return self.twimport(add_args=['ignore_checksum=true'], expect_str='ignoring incorrect checksum')
 
 	def tw_chktotal(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		from mmgen.tw.json import TwJSON
 		fn = joinpath(self.tmpdir, TwJSON.Base(cfg, self.proto).dump_fn)
 		res = json.loads(read_from_file(fn))
@@ -1534,7 +1551,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		return 'ok'
 
 	async def twcompare(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		from mmgen.tw.ctl import TwCtl
 		twctl = await TwCtl(cfg, self.proto, no_wallet_init=True)
 		fn = twctl.tw_path
@@ -1545,7 +1562,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		return 'ok'
 
 	def edit_json_twdump(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		from mmgen.tw.json import TwJSON
 		fn = TwJSON.Base(cfg, self.proto).dump_fn
 		text = json.loads(self.read_from_tmpfile(fn))
@@ -1555,7 +1572,7 @@ class CmdTestEthdev(CmdTestBase, CmdTestShared):
 		return 'ok'
 
 	def stop(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		if not cfg.no_daemon_stop:
 			if not stop_test_daemons(self.proto.coin+'_rt', remove_datadir=True):
 				return False

+ 1 - 1
test/cmdtest_d/ct_input.py

@@ -108,7 +108,7 @@ class CmdTestInput(CmdTestBase):
 	}
 
 	def get_seed_from_stdin(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		from subprocess import run, PIPE
 		cmd = ['python3', 'cmds/mmgen-walletconv', '--skip-cfg-file', '--in-fmt=words', '--out-fmt=words', '--outdir=test/trash']
 		mn = sample_mn['mmgen']['mn']

+ 9 - 6
test/cmdtest_d/ct_main.py

@@ -433,7 +433,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 			return 'skip'
 		for wf in [f for f in os.listdir(cfg.data_dir) if f[-6:]=='.mmdat']:
 			os.unlink(joinpath(cfg.data_dir, wf))
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		self.have_dfl_wallet = False
 		return 'ok'
 
@@ -802,7 +802,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		return t
 
 	def txsend(self, sigfile, extra_opts=[]):
-		t = self.spawn('mmgen-txsend', extra_opts + ['-d', self.tmpdir, sigfile])
+		t = self.spawn('mmgen-txsend', extra_opts + ['-d', self.tmpdir, sigfile], no_passthru_opts=['coin'])
 		self.txsend_ui_common(t, view='t', add_comment='')
 		return t
 
@@ -931,7 +931,8 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 			args=['-H', f'{rf},{hincog_offset}', '-l', str(hincog_seedlen)])
 
 	def txsign_keyaddr(self, keyaddr_file, txfile):
-		t = self.spawn('mmgen-txsign', ['-d', self.tmpdir, '-p1', '-M', keyaddr_file, txfile])
+		t = self.spawn('mmgen-txsign',
+			['-d', self.tmpdir, '-p1', '-M', keyaddr_file, txfile], no_passthru_opts=['coin'])
 		t.license()
 		t.view_tx('n')
 		t.do_decrypt_ka_data(pw=self.kapasswd)
@@ -951,7 +952,8 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		return self.txcreate_common(sources=['2'])
 
 	def txsign2(self, wf1, txf1, wf2, txf2):
-		t = self.spawn('mmgen-txsign', ['-d', self.tmpdir, txf1, wf1, txf2, wf2])
+		t = self.spawn('mmgen-txsign',
+			['-d', self.tmpdir, txf1, wf1, txf2, wf2], no_passthru_opts=['coin'])
 		t.license()
 		for cnum, wf in (('1', wf1), ('2', wf2)):
 			wcls = get_wallet_cls(ext=get_extension(wf))
@@ -973,7 +975,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		return self.txcreate_common(sources=['1', '3'])
 
 	def txsign3(self, wf1, wf2, txf2):
-		t = self.spawn('mmgen-txsign', ['-d', self.tmpdir, wf1, wf2, txf2])
+		t = self.spawn('mmgen-txsign', ['-d', self.tmpdir, wf1, wf2, txf2], no_passthru_opts=['coin'])
 		t.license()
 		t.view_tx('n')
 		for cnum, wf in (('1', wf1), ('3', wf2)):
@@ -1021,7 +1023,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 			'--keys-from-file=' + non_mm_file,
 			'--mmgen-keys-from-file=' + f6,
 			f1, f2, f3, f4, f5]
-		t = self.spawn('mmgen-txsign', add_args)
+		t = self.spawn('mmgen-txsign', add_args, no_passthru_opts=['coin'])
 		t.license()
 		t.view_tx('t')
 		t.do_decrypt_ka_data(pw=self.cfgs['14']['kapasswd'])
@@ -1084,6 +1086,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		t = self.spawn(
 			'mmgen-txsign',
 			add_args + ['-d', self.tmpdir, '-k', non_mm_file, txf, wf],
+			no_passthru_opts = ['coin'],
 			exit_val = 2 if bad_vsize else None)
 		t.license()
 		t.view_tx('n')

+ 1 - 1
test/cmdtest_d/ct_ref.py

@@ -91,7 +91,7 @@ class CmdTestRef(CmdTestBase, CmdTestShared):
 			),
 			'eth': (
 				'88FEFD-ETH[23.45495,40000].rawtx',
-				'B472BD-ETH[23.45495,40000].testnet.rawtx'
+				'76CF8C-ETH[99.99895,50000].regtest.rawtx'
 			),
 			'mm1': (
 				'5881D2-MM1[1.23456,50000].rawtx',

+ 1 - 0
test/cmdtest_d/ct_ref_altcoin.py

@@ -109,6 +109,7 @@ class CmdTestRefAltcoin(CmdTestRef, CmdTestBase):
 				t = self.spawn(
 					'mmgen-txsign',
 					['--outdir=test/trash', '--yes', f'--passwd-file={passfile}', dfl_words_file, txfile],
+					no_passthru_opts = ['coin'],
 					extra_desc = f'{proto.coin}{token_desc} {proto.network}')
 				t.read()
 				t.ok()

+ 12 - 8
test/cmdtest_d/ct_regtest.py

@@ -723,7 +723,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 	async def bob_recreate_tracking_wallet(self):
 		if not self.deterministic:
 			return 'skip'
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		await self.rt.stop()
 		from shutil import rmtree
 		imsg('Deleting Bob’s old tracking wallet')
@@ -907,7 +907,8 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 
 	def bob_subwallet_txsign(self):
 		fn = get_file_with_ext(self.tmpdir, 'rawtx')
-		t = self.spawn('mmgen-txsign', ['-d', self.tmpdir, '--bob', '--subseeds=127', fn])
+		t = self.spawn('mmgen-txsign',
+			['-d', self.tmpdir, '--bob', '--subseeds=127', fn], no_passthru_opts=['coin'])
 		t.view_tx('t')
 		t.passphrase(dfl_wcls.desc, rt_pw)
 		t.do_comment(None)
@@ -1051,6 +1052,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		t = self.spawn(
 				'mmgen-txsend',
 				['-d', self.tmpdir, '--'+user, '--status'] + extra_args + [tx_file],
+				no_passthru_opts = ['coin'],
 				exit_val = exit_val)
 		if exp1:
 			t.expect(exp1, regex=True)
@@ -1253,7 +1255,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 
 	def _get_mempool(self, do_msg=False):
 		if do_msg:
-			self.spawn('', msg_only=True)
+			self.spawn(msg_only=True)
 		return self._do_mmgen_regtest(['mempool'], decode_json=True)
 
 	def _get_mempool_compare_txid(self, txid1, txid2):
@@ -1534,7 +1536,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		return self._bob_twprune_selected(resp='sssSpP', npruned=7)
 
 	def bob_edit_json_twdump(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		from mmgen.tw.json import TwJSON
 		fn = TwJSON.Base(cfg, self.proto).dump_fn
 		text = json.loads(self.read_from_tmpfile(fn))
@@ -2217,13 +2219,15 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 	def bob_dump_hex_sign(self):
 		txfile = get_file_with_ext(self.dump_hex_subdir, 'rawtx')
 		return self.txsign_ui_common(
-			self.spawn('mmgen-txsign', ['-d', self.dump_hex_subdir, '--bob', txfile]),
+			self.spawn('mmgen-txsign',
+				['-d', self.dump_hex_subdir, '--bob', txfile], no_passthru_opts=['coin']),
 			do_passwd = True,
 			passwd    = rt_pw)
 
 	def _bob_dump_hex_dump(self, file):
 		txfile = get_file_with_ext(self.dump_hex_subdir, 'sigtx')
-		t = self.spawn('mmgen-txsend', ['-d', self.dump_hex_subdir, f'--dump-hex={file}', '--bob', txfile])
+		t = self.spawn('mmgen-txsend',
+			['-d', self.dump_hex_subdir, f'--dump-hex={file}', '--bob', txfile], no_passthru_opts=['coin'])
 		t.expect('view: ', '\n')
 		t.expect('(y/N): ', '\n') # add comment?
 		t.written_to_file('Sent transaction')
@@ -2244,7 +2248,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 
 	def bob_dump_hex_test(self):
 		txfile = get_file_with_ext(self.dump_hex_subdir, 'sigtx')
-		t = self.spawn('mmgen-txsend', ['--bob', '--test', txfile])
+		t = self.spawn('mmgen-txsend', ['--bob', '--test', txfile], no_passthru_opts=['coin'])
 		self.txsend_ui_common(t, bogus_send=False, test=True)
 		return t
 
@@ -2257,7 +2261,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		return self._user_bal_cli('bob', chks=['499.99990287', '46.51845565'])
 
 	def stop(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		if cfg.no_daemon_stop:
 			msg_r(f'(leaving regtest daemon{suf(self.protos)} running by user request)')
 			imsg('')

+ 1 - 0
test/cmdtest_d/ct_shared.py

@@ -215,6 +215,7 @@ class CmdTestShared:
 			'mmgen-txsign',
 			opts,
 			extra_desc,
+			no_passthru_opts = ['coin'],
 			exit_val = None if save or (wcls.enc and wcls.type != 'brain') else 1)
 		t.license()
 		t.view_tx(view)

+ 11 - 9
test/cmdtest_d/ct_swap.py

@@ -17,20 +17,16 @@ from pathlib import Path
 from mmgen.protocol import init_proto
 from ..include.common import make_burn_addr, gr_uc
 from .common import dfl_bip39_file
-from .thornode import run_thornode_server
+from .httpd.thornode import ThornodeServer
 
 from .ct_autosign import CmdTestAutosign, CmdTestAutosignThreaded
 from .ct_regtest import CmdTestRegtest, rt_data, dfl_wcls, rt_pw, cfg, strip_ansi_escapes
 
+thornode_server = ThornodeServer()
+
 sample1 = gr_uc[:24]
 sample2 = '00010203040506'
 
-def thornode_server_start():
-	import threading
-	t = threading.Thread(target=run_thornode_server, name='Thornode server thread')
-	t.daemon = True
-	t.start()
-
 class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 	bdb_wallet = True
 	networks = ('btc',)
@@ -48,6 +44,7 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 		('subgroup.signsend',     ['init_swap']),
 		('subgroup.signsend_bad', ['init_swap']),
 		('subgroup.autosign',     ['init_data', 'signsend']),
+		('thornode_server_stop',  'stopping the Thornode server'),
 		('stop',                  'stopping regtest daemons'),
 	)
 	cmd_subgroups = {
@@ -175,7 +172,7 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 
 		self.protos = [init_proto(cfg, k, network='regtest', need_amt=True) for k in ('btc', 'ltc', 'bch')]
 
-		thornode_server_start() # TODO: stop server when test group finishes executing
+		thornode_server.start()
 
 		self.opts.append('--bob')
 
@@ -728,7 +725,12 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 		return self._mempool(2)
 
 	def _mempool(self, proto_idx):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		data = self._do_cli(['getrawmempool'], add_opts=[f'--coin={self.protos[proto_idx].coin}'])
 		assert data
 		return 'ok'
+
+	def thornode_server_stop(self):
+		self.spawn(msg_only=True)
+		thornode_server.stop()
+		return 'ok'

+ 3 - 3
test/cmdtest_d/ct_xmr_autosign.py

@@ -123,7 +123,7 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 		self.spawn_env['MMGEN_TEST_SUITE_XMR_AUTOSIGN'] = '1'
 
 	def create_tmp_wallets(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		data = self.users['alice']
 		from mmgen.wallet import Wallet
 		from mmgen.xmrwallet import op
@@ -191,7 +191,7 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 
 	def _delete_files(self, *ext_list):
 		data = self.users['alice']
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		for ext in ext_list:
 			get_file_with_ext(data.udir, ext, no_dot=True, delete_all=True)
 		return 'ok'
@@ -432,7 +432,7 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 		return t
 
 	def txview_all(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		self.insert_device()
 		self.do_mount()
 		imsg(blue('Opening transaction directory: ') + cyan(f'{self.asi.xmr_tx_dir}'))

+ 4 - 8
test/cmdtest_d/ct_xmrwallet.py

@@ -423,14 +423,14 @@ class CmdTestXMRWallet(CmdTestBase):
 			r'Creating new address for wallet .*4.*, account .*#2.* with label .*‘Alice’s new address .*y/N\): ')
 
 	async def mine_initial_coins(self):
-		self.spawn('', msg_only=True, extra_desc='(opening wallet)')
+		self.spawn(msg_only=True, extra_desc='(opening wallet)')
 		await self.open_wallet_user('miner', 1)
 		ok()
 		# NB: a large balance is required to avoid ‘insufficient outputs’ error
 		return await self.mine_chk('miner', 1, 0, lambda x: x.ub > 2000, 'unlocked balance > 2000')
 
 	async def fund_alice(self, wallet=1, amt=1234567891234):
-		self.spawn('', msg_only=True, extra_desc='(transferring funds from Miner wallet)')
+		self.spawn(msg_only=True, extra_desc='(transferring funds from Miner wallet)')
 		await self.transfer(
 			'miner',
 			amt,
@@ -887,11 +887,7 @@ class CmdTestXMRWallet(CmdTestBase):
 		return ret['status']
 
 	def do_msg(self, extra_desc=None):
-		self.spawn(
-			'',
-			msg_only = True,
-			extra_desc = f'({extra_desc})' if extra_desc else None
-		)
+		self.spawn(msg_only=True, extra_desc=f'({extra_desc})' if extra_desc else None)
 
 	async def transfer(self, user, amt, addr):
 		return self.users[user].wd_rpc.call('transfer', destinations=[{'amount':amt, 'address':addr}])
@@ -904,7 +900,7 @@ class CmdTestXMRWallet(CmdTestBase):
 			v.md.start()
 
 	def stop_daemons(self):
-		self.spawn('', msg_only=True)
+		self.spawn(msg_only=True)
 		if cfg.no_daemon_stop:
 			omsg('[not stopping daemons at user request]')
 		else:

+ 0 - 32
test/cmdtest_d/etherscan.py

@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-
-from http.server import HTTPServer, CGIHTTPRequestHandler
-
-from mmgen.util import msg
-from mmgen.util2 import port_in_use
-
-class handler(CGIHTTPRequestHandler):
-	header = b'HTTP/1.1 200 OK\nContent-type: text/html\n\n'
-
-	def do_response(self, target):
-		with open(f'test/ref/ethereum/etherscan-{target}.html') as fh:
-			text = fh.read()
-		self.wfile.write(self.header + text.encode())
-
-	def do_GET(self):
-		return self.do_response('form')
-
-	def do_POST(self):
-		return self.do_response('result')
-
-def run_etherscan_server(server_class=HTTPServer, handler_class=handler):
-
-	if port_in_use(28800):
-		msg('Port 28800 in use. Assuming etherscan server is running')
-		return True
-
-	msg('Etherscan server listening on port 28800')
-	server_address = ('localhost', 28800)
-	httpd = server_class(server_address, handler_class)
-	httpd.serve_forever()
-	msg('Etherscan server exiting')

+ 61 - 0
test/cmdtest_d/httpd/__init__.py

@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+#
+# MMGen Wallet, a terminal-based cryptocurrency wallet
+# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+test.cmdtest_d.httpd: WSGI http server
+"""
+
+from wsgiref.simple_server import make_server, WSGIRequestHandler
+
+from mmgen.util import msg
+from mmgen.util2 import port_in_use
+
+class SilentRequestHandler(WSGIRequestHandler):
+
+	def log_request(self, code='-', size='-'):
+		return None
+
+class HTTPD:
+
+	def start(self):
+
+		if port_in_use(self.port):
+			msg(f'\nPort {self.port} in use. Assuming {self.name} is running')
+			return True
+
+		self.httpd = make_server(
+			'localhost',
+			self.port,
+			self.application,
+			handler_class = SilentRequestHandler)
+
+		import threading
+		t = threading.Thread(target=self.httpd.serve_forever, name=f'{self.name} thread')
+		t.daemon = True
+		t.start()
+
+	def stop(self):
+		self.httpd.server_close()
+
+	def application(self, environ, start_response):
+
+		method = environ['REQUEST_METHOD']
+
+		response_body = self.make_response_body(method, environ)
+
+		status = '200 OK'
+		response_headers = [
+			('Content-Type', self.content_type),
+			('Content-Length', str(len(response_body)))
+		]
+
+		start_response(status, response_headers)
+
+		return [response_body]

+ 25 - 0
test/cmdtest_d/httpd/etherscan.py

@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+#
+# MMGen Wallet, a terminal-based cryptocurrency wallet
+# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+test.cmdtest_d.httpd.etherscan: Etherscan WSGI http server
+"""
+
+from . import HTTPD
+
+class EtherscanServer(HTTPD):
+	name = 'etherscan server'
+	port = 28800
+	content_type = 'text/html'
+
+	def make_response_body(self, method, environ):
+		targets = {'GET': 'form', 'POST': 'result'}
+		with open(f'test/ref/ethereum/etherscan-{targets[method]}.html') as fh:
+			return fh.read().encode()

+ 45 - 47
test/cmdtest_d/thornode.py → test/cmdtest_d/httpd/thornode.py

@@ -1,21 +1,29 @@
 #!/usr/bin/env python3
+#
+# MMGen Wallet, a terminal-based cryptocurrency wallet
+# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
 
-import json, re, time
-from http.server import HTTPServer, BaseHTTPRequestHandler
+"""
+test.cmdtest_d.httpd.thornode: Thornode WSGI http server
+"""
+
+import time, re, json
 
 from mmgen.cfg import Config
-from mmgen.util import msg, make_timestr
+
+from . import HTTPD
 
 cfg = Config()
 
-def make_inbound_addr(proto, mmtype):
-	from mmgen.tool.coin import tool_cmd
-	n = int(time.time()) // (60 * 60 * 24) # increments once every 24 hrs
-	return tool_cmd(
-		cfg     = cfg,
-		cmdname = 'pubhash2addr',
-		proto   = proto,
-		mmtype  = mmtype).pubhash2addr(f'{n:040x}')
+# https://thornode.ninerealms.com/thorchain/quote/swap?from_asset=BCH.BCH&to_asset=LTC.LTC&amount=1000000
+sample_request = 'GET /thorchain/quote/swap?from_asset=BCH.BCH&to_asset=LTC.LTC&amount=1000000000'
+request_pat = r'/thorchain/quote/swap\?from_asset=(\S+)\.(\S+)&to_asset=(\S+)\.(\S+)&amount=(\d+)'
+prices = { 'BTC': 97000, 'LTC': 115, 'BCH': 330 }
 
 data_template = {
 	'inbound_address': None,
@@ -45,45 +53,35 @@ data_template = {
 	'total_swap_seconds': 2430
 }
 
-# https://thornode.ninerealms.com/thorchain/quote/swap?from_asset=BCH.BCH&to_asset=LTC.LTC&amount=1000000
-
-sample_request = 'GET /thorchain/quote/swap?from_asset=BCH.BCH&to_asset=LTC.LTC&amount=1000000000 HTTP/1.1'
+def make_inbound_addr(proto, mmtype):
+	from mmgen.tool.coin import tool_cmd
+	n = int(time.time()) // (60 * 60 * 24) # increments once every 24 hrs
+	return tool_cmd(
+		cfg     = cfg,
+		cmdname = 'pubhash2addr',
+		proto   = proto,
+		mmtype  = mmtype).pubhash2addr(f'{n:040x}')
 
-request_pat = r'/thorchain/quote/swap\?from_asset=(\S+)\.(\S+)&to_asset=(\S+)\.(\S+)&amount=(\d+) HTTP/'
+class ThornodeServer(HTTPD):
+	name = 'thornode server'
+	port = 18800
+	content_type = 'application/json'
 
-prices = { 'BTC': 97000, 'LTC': 115, 'BCH': 330 }
+	def make_response_body(self, method, environ):
+		from wsgiref.util import request_uri
 
-def create_data(request_line):
-	m = re.search(request_pat, request_line)
-	try:
+		m = re.search(request_pat, request_uri(environ))
 		_, send_coin, _, recv_coin, amt_atomic = m.groups()
-	except Exception as e:
-		msg(f'{type(e)}: {e}')
-		return {}
-
-	from mmgen.protocol import init_proto
-	send_proto = init_proto(cfg, send_coin, network='regtest', need_amt=True)
-	in_amt = send_proto.coin_amt(int(amt_atomic), from_unit='satoshi')
-	out_amt = in_amt * (prices[send_coin] / prices[recv_coin])
-
-	addr = make_inbound_addr(send_proto, send_proto.preferred_mmtypes[0])
-	expiry = int(time.time()) + (10 * 60)
-	return data_template | {
-		'expected_amount_out': str(out_amt.to_unit('satoshi')),
-		'expiry': expiry,
-		'inbound_address': addr,
-	}
-
-class handler(BaseHTTPRequestHandler):
-	header = b'HTTP/1.1 200 OK\nContent-type: application/json\n\n'
 
-	def do_GET(self):
-		# print(f'Thornode server received:\n  {self.requestline}')
-		self.wfile.write(self.header + json.dumps(create_data(self.requestline)).encode())
+		from mmgen.protocol import init_proto
+		send_proto = init_proto(cfg, send_coin, network='regtest', need_amt=True)
+		in_amt = send_proto.coin_amt(int(amt_atomic), from_unit='satoshi')
+		out_amt = in_amt * (prices[send_coin] / prices[recv_coin])
 
-def run_thornode_server(server_class=HTTPServer, handler_class=handler):
-	print('Thornode server listening on port 18800')
-	server_address = ('localhost', 18800)
-	httpd = server_class(server_address, handler_class)
-	httpd.serve_forever()
-	print('Thornode server exiting')
+		addr = make_inbound_addr(send_proto, send_proto.preferred_mmtypes[0])
+		data = data_template | {
+			'expected_amount_out': str(out_amt.to_unit('satoshi')),
+			'expiry': int(time.time()) + (10 * 60),
+			'inbound_address': addr,
+		}
+		return json.dumps(data).encode()

+ 0 - 2
test/modtest_d/ut_tx.py

@@ -94,8 +94,6 @@ class unit_tests:
 				'ethereum/5881D2-MM1[1.23456,50000].rawtx',
 				'ethereum/6BDB25-MM1[1.23456,50000].testnet.rawtx',
 				'ethereum/88FEFD-ETH[23.45495,40000].rawtx',
-				'ethereum/B472BD-ETH[23.45495,40000].testnet.rawtx',
-				'ethereum/B472BD-ETH[23.45495,40000].testnet.sigtx',
 				'litecoin/A5A1E0-LTC[1454.64322,1453,tl=1320969600].testnet.rawtx',
 				'litecoin/AF3CDF-LTC[620.76194,1453,tl=1320969600].rawtx',
 			)

+ 1 - 0
test/ref/ethereum/76CF8C-ETH[99.99895,50000].regtest.rawtx

@@ -0,0 +1 @@
+{"MMGenTransaction":{"coin_id":"ETH","chain":"developmentchain","txid":"76CF8C","send_amt":"99.99895","timestamp":"20250317_045533","blockcount":16,"serialized":"{\"from\": \"1f2f0641c80c1f6a13edf22e184292584ddcc9d9\", \"to\": \"657afae1dfa9a866fb29b3e52d7b3eafdf1d55f9\", \"amt\": \"99.99895\", \"gasPrice\": \"0.00000005\", \"startGas\": \"0.000000000000021\", \"nonce\": \"1\", \"chainId\": \"1337\", \"data\": \"\"}","inputs":[{"amt":"100","comment":"","addr":"1f2f0641c80c1f6a13edf22e184292584ddcc9d9","confs":0,"mmid":"98831F3A:E:1"}],"outputs":[{"addr":"657afae1dfa9a866fb29b3e52d7b3eafdf1d55f9","amt":"99.99895","is_chg":true,"mmid":"98831F3A:E:12"}],"comment":"\u5fc5\u8981\u306a\u306e\u306f\u3001\u4fe1\u7528\u3067\u306f\u306a\u304f\u6697\u53f7\u5316\u3055\u308c\u305f\u8a3c\u660e\u306b\u57fa\u3065\u304f\u96fb\u5b50\u53d6\u5f15\u30b7\u30b9\u30c6\u30e0\u3067\u3042\u308a\u3001\u3053\u308c\u306b\u3088\u308a\u5e0c\u671b\u3059\u308b\u4e8c\u8005\u304c\u4fe1\u7528\u3067\u304d\u308b\u7b2c\u4e09\u8005\u6a5f\u95a2\u3092\u4ecb\u3055\u305a\u306b\u76f4\u63a5\u53d6\u5f15\u3067\u304d\u308b\u3088\u3046"},"chksum":"8c9032"}

+ 1 - 0
test/ref/ethereum/76CF8C-ETH[99.99895,50000].regtest.sigtx

@@ -0,0 +1 @@
+{"MMGenTransaction":{"coin_id":"ETH","chain":"developmentchain","txid":"76CF8C","send_amt":"99.99895","timestamp":"20250317_045533","blockcount":16,"serialized":"f86f01850ba43b740082520894657afae1dfa9a866fb29b3e52d7b3eafdf1d55f989056bc3a335360c600080820a96a0a6120a53a55a819f5305c30c4b35c5471586ce664e17c28342cc0cb5f7a1f7b9a004541f89040b482d610364be3eb157b34129bb1c291bcd5a51e229288972f10a","inputs":[{"amt":"100","comment":"","addr":"1f2f0641c80c1f6a13edf22e184292584ddcc9d9","confs":0,"mmid":"98831F3A:E:1"}],"outputs":[{"addr":"657afae1dfa9a866fb29b3e52d7b3eafdf1d55f9","amt":"99.99895","is_chg":true,"mmid":"98831F3A:E:12"}],"comment":"\u5fc5\u8981\u306a\u306e\u306f\u3001\u4fe1\u7528\u3067\u306f\u306a\u304f\u6697\u53f7\u5316\u3055\u308c\u305f\u8a3c\u660e\u306b\u57fa\u3065\u304f\u96fb\u5b50\u53d6\u5f15\u30b7\u30b9\u30c6\u30e0\u3067\u3042\u308a\u3001\u3053\u308c\u306b\u3088\u308a\u5e0c\u671b\u3059\u308b\u4e8c\u8005\u304c\u4fe1\u7528\u3067\u304d\u308b\u7b2c\u4e09\u8005\u6a5f\u95a2\u3092\u4ecb\u3055\u305a\u306b\u76f4\u63a5\u53d6\u5f15\u3067\u304d\u308b\u3088\u3046","coin_txid":"0f89a9fa2d2b4e93db97f771ddb100d46221ede42fc2e120a904395d7614d28f"},"chksum":"369d94"}

+ 0 - 6
test/ref/ethereum/B472BD-ETH[23.45495,40000].testnet.rawtx

@@ -1,6 +0,0 @@
-81edb6
-ETH KOVAN B472BD 23.45495 20180725_111111 7654321
-{"nonce": "0", "chainId": "42", "from": "97ccc3a117b3696340c42561361054b1c9c793d5", "startGas": "0.000000000000021", "to": "b7d06382a998817a16ba487a99636bf8cae29d9d", "data": "", "amt": "23.45495", "gasPrice": "0.00000004"}
-[{'confs': 0, 'label': u'', 'mmid': '98831F3A:E:1', 'amt': '123.456', 'addr': '97ccc3a117b3696340c42561361054b1c9c793d5'}]
-[{'mmid': '98831F3A:E:2', 'amt': '23.45495', 'addr': '2a6db46c87407e6d28fcb97d3bd0f5cf4aafca46'}]
-qRHzrPVpZFYxnQvk3atLzUtp41bZupJ2UQNnKe3ZnmqFsEngS6vaCCvesKKy9khzVq6y2RqarVBcZLnjtXxMpbAcdEtyBWiBYmZdoU8SN4uAbroHT1c7gEbmUNVKKdqHD86ZRRqDNpdh1ztmLiMAy3ibM83puwJHNpGGHgUGjZ1RSEgyVKCs2rZ9wXN8rBMibDDPYo1LgtAst2FkB36Mgf4Vf7ekoRAdiRNGd5YZ3RXAVsSdnZcyn4rdeQDMDkCq7JJDoB25eNEuXQutZFUcf2fEfxkMbW1sXJDNFQq

+ 0 - 7
test/ref/ethereum/B472BD-ETH[23.45495,40000].testnet.sigtx

@@ -1,7 +0,0 @@
-e9feb9
-ETH KOVAN B472BD 23.45495 20180725_111111 7654321
-f86d808509502f900082520894b7d06382a998817a16ba487a99636bf8cae29d9d89014580b8c15e8c60008077a057b7b73b9102fda68922eba0fbb70171425f1f27bad944838d3ded819eb03a63a00acb20276cbb30209923f959c63595b1b35663cbb30d186c6a5a2dbccb3e07fa
-[{'confs': 0, 'label': '', 'mmid': '98831F3A:E:1', 'amt': '123.456', 'addr': '97ccc3a117b3696340c42561361054b1c9c793d5'}]
-[{'mmid': '98831F3A:E:2', 'amt': '23.45495', 'addr': '2a6db46c87407e6d28fcb97d3bd0f5cf4aafca46'}]
-qRHzrPVpZFYxnQvk3atLzUtp41bZupJ2UQNnKe3ZnmqFsEngS6vaCCvesKKy9khzVq6y2RqarVBcZLnjtXxMpbAcdEtyBWiBYmZdoU8SN4uAbroHT1c7gEbmUNVKKdqHD86ZRRqDNpdh1ztmLiMAy3ibM83puwJHNpGGHgUGjZ1RSEgyVKCs2rZ9wXN8rBMibDDPYo1LgtAst2FkB36Mgf4Vf7ekoRAdiRNGd5YZ3RXAVsSdnZcyn4rdeQDMDkCq7JJDoB25eNEuXQutZFUcf2fEfxkMbW1sXJDNFQq
-0f277d20bf3793f94521a809943a659478bdfa6836a399f0568a93aeb4ce5184

+ 2 - 2
test/tooltest2_d/data.py

@@ -745,8 +745,8 @@ tests = {
 									None),],
 			'eth_mainnet': [(['test/ref/ethereum/88FEFD-ETH[23.45495,40000].rawtx'], None),],
 			'eth_testnet': [([
-				'test/ref/ethereum/B472BD-ETH[23.45495,40000].testnet.rawtx',
-				'test/ref/ethereum/B472BD-ETH[23.45495,40000].testnet.sigtx'
+				'test/ref/ethereum/76CF8C-ETH[99.99895,50000].regtest.rawtx',
+				'test/ref/ethereum/76CF8C-ETH[99.99895,50000].regtest.sigtx'
 				], None),],
 			'mm1_mainnet': [(['test/ref/ethereum/5881D2-MM1[1.23456,50000].rawtx'], None),],
 			'mm1_testnet': [(['test/ref/ethereum/6BDB25-MM1[1.23456,50000].testnet.rawtx'], None),],