Browse Source

offline transaction signing with automount for BTC, BCH, LTC and ETH/ERC20

Previously supported only for XMR, offline transaction autosigning with no
filename arguments and automatic mounting/unmounting of the removable device
on the online machine is now available for all coins MMGen Wallet supports
transacting with.  To activate, invoke ‘mmgen-txcreate’ and ‘mmgen-txsend’
with the --autosign option.

Be aware that transactions must be created, signed and sent one at a time when
using this feature.  For bulk transaction signing, you must use the old manual
mounting method.

Example create-sign-send workflow for BTC:

    $ mmgen-txcreate --autosign bc1qxmymxf8p5ckvlxkmkwgw8ap5t2xuaffmrpexap,0.00123 B

    (remove device - insert offline - wait for signing - remove - insert online)

    $ mmgen-txsend --autosign

Unsigned or unsent transactions may be aborted as follows:

    $ mmgen-txsend --abort

And sent RBF transactions may be fee-bumped:

    $ mmgen-txbump --autosign

You can check the status of the current transaction, whether sent or unsent,
with the following command:

    $ mmgen-txsend --status

That’s all there is to it!

Testing (add the -e option to see script output):

    $ test/cmdtest.py autosign_automount
    $ test/cmdtest.py --coin=eth autosign_eth
The MMGen Project 1 year ago
parent
commit
1c5c3319d4

+ 47 - 2
mmgen/autosign.py

@@ -28,6 +28,7 @@ class Signable:
 
 
 	non_xmr_signables = (
 	non_xmr_signables = (
 		'transaction',
 		'transaction',
+		'automount_transaction',
 		'message')
 		'message')
 
 
 	xmr_signables = (              # order is important!
 	xmr_signables = (              # order is important!
@@ -62,6 +63,13 @@ class Signable:
 		def unsubmitted(self):
 		def unsubmitted(self):
 			return self._unprocessed( '_unsubmitted', self.sigext, self.subext )
 			return self._unprocessed( '_unsubmitted', self.sigext, self.subext )
 
 
+		@property
+		def unsubmitted_raw(self):
+			return self._unprocessed( '_unsubmitted_raw', self.rawext, self.subext )
+
+		unsent = unsubmitted
+		unsent_raw = unsubmitted_raw
+
 		def _unprocessed(self,attrname,rawext,sigext):
 		def _unprocessed(self,attrname,rawext,sigext):
 			if not hasattr(self,attrname):
 			if not hasattr(self,attrname):
 				dirlist = sorted(self.dir.iterdir())
 				dirlist = sorted(self.dir.iterdir())
@@ -91,20 +99,45 @@ class Signable:
 				e = f'in ‘{getattr(self.parent, self.dir_name)}’' if show_dir else 'on removable device',
 				e = f'in ‘{getattr(self.parent, self.dir_name)}’' if show_dir else 'on removable device',
 			))
 			))
 
 
+		def check_create_ok(self):
+			if len(self.unsigned):
+				self.die_wrong_num_txs('unsigned', msg='Cannot create transaction')
+
 		def get_unsubmitted(self, tx_type='unsubmitted'):
 		def get_unsubmitted(self, tx_type='unsubmitted'):
 			if len(self.unsubmitted) == 1:
 			if len(self.unsubmitted) == 1:
 				return self.unsubmitted[0]
 				return self.unsubmitted[0]
 			else:
 			else:
 				self.die_wrong_num_txs(tx_type)
 				self.die_wrong_num_txs(tx_type)
 
 
+		def get_unsent(self):
+			return self.get_unsubmitted('unsent')
+
 		def get_submitted(self):
 		def get_submitted(self):
 			if len(self.submitted) == 0:
 			if len(self.submitted) == 0:
 				self.die_wrong_num_txs('submitted')
 				self.die_wrong_num_txs('submitted')
 			else:
 			else:
 				return self.submitted
 				return self.submitted
 
 
+		def get_abortable(self):
+			if len(self.unsent_raw) != 1:
+				self.die_wrong_num_txs('unsent_raw', desc='unsent')
+			if len(self.unsent) > 1:
+				self.die_wrong_num_txs('unsent')
+			if self.unsent:
+				if self.unsent[0].stem != self.unsent_raw[0].stem:
+					die(1, f'{self.unsent[0]}, {self.unsent_raw[0]}: file mismatch')
+			return self.unsent_raw + self.unsent
+
+		async def get_last_created(self):
+			from .tx import CompletedTX
+			ext = '.' + Signable.automount_transaction.subext
+			files = [f for f in self.dir.iterdir() if f.name.endswith(ext)]
+			return sorted(
+				[await CompletedTX(cfg=self.cfg, filename=str(txfile), quiet_open=True) for txfile in files],
+				key = lambda x: x.timestamp)[-1]
+
 	class transaction(base):
 	class transaction(base):
-		desc = 'transaction'
+		desc = 'non-automount transaction'
 		rawext = 'rawtx'
 		rawext = 'rawtx'
 		sigext = 'sigtx'
 		sigext = 'sigtx'
 		dir_name = 'tx_dir'
 		dir_name = 'tx_dir'
@@ -112,7 +145,10 @@ class Signable:
 
 
 		async def sign(self,f):
 		async def sign(self,f):
 			from .tx import UnsignedTX
 			from .tx import UnsignedTX
-			tx1 = UnsignedTX( cfg=self.cfg, filename=f )
+			tx1 = UnsignedTX(
+					cfg       = self.cfg,
+					filename  = f,
+					automount = self.name=='automount_transaction')
 			if tx1.proto.sign_mode == 'daemon':
 			if tx1.proto.sign_mode == 'daemon':
 				from .rpc import rpc_init
 				from .rpc import rpc_init
 				tx1.rpc = await rpc_init( self.cfg, tx1.proto, ignore_wallet=True )
 				tx1.rpc = await rpc_init( self.cfg, tx1.proto, ignore_wallet=True )
@@ -168,6 +204,14 @@ class Signable:
 			for f in bad_files:
 			for f in bad_files:
 				yield red(f.name)
 				yield red(f.name)
 
 
+	class automount_transaction(transaction):
+		desc   = 'automount transaction'
+		dir_name = 'txauto_dir'
+		rawext = 'arawtx'
+		sigext = 'asigtx'
+		subext = 'asubtx'
+		multiple_ok = False
+
 	class xmr_signable(transaction): # mixin class
 	class xmr_signable(transaction): # mixin class
 
 
 		def need_daemon_restart(self,m,new_idx):
 		def need_daemon_restart(self,m,new_idx):
@@ -278,6 +322,7 @@ class Autosign:
 
 
 	non_xmr_dirs = {
 	non_xmr_dirs = {
 		'tx_dir':     'tx',
 		'tx_dir':     'tx',
+		'txauto_dir': 'txauto',
 		'msg_dir':    'msg',
 		'msg_dir':    'msg',
 	}
 	}
 	xmr_dirs = {
 	xmr_dirs = {

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-14.1.dev14
+14.1.dev15

+ 1 - 3
mmgen/main_autosign.py

@@ -87,9 +87,7 @@ the status LED indicates whether the program is busy or in standby mode, i.e.
 ready for device insertion or removal.
 ready for device insertion or removal.
 
 
 The removable device must have a partition labeled MMGEN_TX with a user-
 The removable device must have a partition labeled MMGEN_TX with a user-
-writable root directory and a directory named ‘/tx’, where unsigned MMGen
-transactions are placed.  Optionally, the directory ‘/msg’ may be created
-and unsigned message files produced by ‘mmgen-msg’ placed there.
+writable root directory.
 
 
 On both the signing and online machines the mountpoint ‘{asi.mountpoint}’
 On both the signing and online machines the mountpoint ‘{asi.mountpoint}’
 (as currently configured) must exist and ‘/etc/fstab’ must contain the
 (as currently configured) must exist and ‘/etc/fstab’ must contain the

+ 34 - 9
mmgen/main_txbump.py

@@ -33,10 +33,17 @@ opts_data = {
                 creating a new transaction, and optionally sign and send the
                 creating a new transaction, and optionally sign and send the
                 new transaction
                 new transaction
 		 """,
 		 """,
-		'usage':   f'[opts] <{gc.proj_name} TX file> [seed source] ...',
+		'usage':   f'[opts] [{gc.proj_name} TX file] [seed source] ...',
 		'options': """
 		'options': """
 -h, --help             Print this help message
 -h, --help             Print this help message
 --, --longhelp         Print help message for long options (common options)
 --, --longhelp         Print help message for long options (common options)
+-a, --autosign         Bump the most recent transaction created and sent with
+                       the --autosign option. The removable device is mounted
+                       and unmounted automatically.  The transaction file
+                       argument must be omitted.  Note that only sent trans-
+                       actions may be bumped with this option.  To redo an
+                       unsent --autosign transaction, first delete it using
+                       ‘mmgen-txsend --abort’ and then create a new one
 -b, --brain-params=l,p Use seed length 'l' and hash preset 'p' for
 -b, --brain-params=l,p Use seed length 'l' and hash preset 'p' for
                        brainwallet input
                        brainwallet input
 -c, --comment-file=  f Source the transaction's comment from file 'f'
 -c, --comment-file=  f Source the transaction's comment from file 'f'
@@ -103,10 +110,10 @@ FMT CODES:
 
 
 cfg = Config(opts_data=opts_data)
 cfg = Config(opts_data=opts_data)
 
 
-tx_file = cfg._args.pop(0)
-
-from .fileutil import check_infile
-check_infile(tx_file)
+if not cfg.autosign:
+	tx_file = cfg._args.pop(0)
+	from .fileutil import check_infile
+	check_infile(tx_file)
 
 
 from .tx import CompletedTX, BumpTX, UnsignedTX, OnlineSignedTX
 from .tx import CompletedTX, BumpTX, UnsignedTX, OnlineSignedTX
 from .tx.sign import txsign,get_seed_files,get_keyaddrlist,get_keylist
 from .tx.sign import txsign,get_seed_files,get_keyaddrlist,get_keylist
@@ -120,20 +127,37 @@ silent = cfg.yes and cfg.fee is not None and cfg.output_to_reduce is not None
 
 
 async def main():
 async def main():
 
 
-	orig_tx = await CompletedTX(cfg=cfg,filename=tx_file)
+	if cfg.autosign:
+		from .tx.util import init_removable_device
+		from .autosign import Signable
+		asi = init_removable_device(cfg)
+		asi.do_mount()
+		si = Signable.automount_transaction(asi)
+		if si.unsigned or si.unsent:
+			state = 'unsigned' if si.unsigned else 'unsent'
+			die(1,
+				'Only sent transactions can be bumped with --autosign.  Instead of bumping\n'
+				f'your {state} transaction, abort it with ‘mmgen-txsend --abort’ and create\n'
+				'a new one.')
+		orig_tx = await si.get_last_created()
+		kal = kl = sign_and_send = None
+	else:
+		orig_tx = await CompletedTX(cfg=cfg, filename=tx_file)
 
 
 	if not silent:
 	if not silent:
 		msg(green('ORIGINAL TRANSACTION'))
 		msg(green('ORIGINAL TRANSACTION'))
 		msg(orig_tx.info.format(terse=True))
 		msg(orig_tx.info.format(terse=True))
 
 
-	kal = get_keyaddrlist(cfg,orig_tx.proto)
-	kl = get_keylist(cfg)
-	sign_and_send = bool(seed_files or kl or kal)
+	if not cfg.autosign:
+		kal = get_keyaddrlist(cfg, orig_tx.proto)
+		kl = get_keylist(cfg)
+		sign_and_send = any([seed_files, kl, kal])
 
 
 	from .tw.ctl import TwCtl
 	from .tw.ctl import TwCtl
 	tx = await BumpTX(
 	tx = await BumpTX(
 		cfg  = cfg,
 		cfg  = cfg,
 		data = orig_tx.__dict__,
 		data = orig_tx.__dict__,
+		automount = cfg.autosign,
 		check_sent = cfg.autosign or sign_and_send,
 		check_sent = cfg.autosign or sign_and_send,
 		twctl = await TwCtl(cfg,orig_tx.proto) if orig_tx.proto.tokensym else None )
 		twctl = await TwCtl(cfg,orig_tx.proto) if orig_tx.proto.tokensym else None )
 
 
@@ -181,6 +205,7 @@ async def main():
 			die(2,'Transaction could not be signed')
 			die(2,'Transaction could not be signed')
 	else:
 	else:
 		tx.file.write(
 		tx.file.write(
+			outdir                = asi.txauto_dir if cfg.autosign else None,
 			ask_write             = not cfg.yes,
 			ask_write             = not cfg.yes,
 			ask_write_default_yes = False,
 			ask_write_default_yes = False,
 			ask_overwrite         = not cfg.yes)
 			ask_overwrite         = not cfg.yes)

+ 11 - 0
mmgen/main_txcreate.py

@@ -32,6 +32,9 @@ opts_data = {
 		'options': """
 		'options': """
 -h, --help            Print this help message
 -h, --help            Print this help message
 --, --longhelp        Print help message for long options (common options)
 --, --longhelp        Print help message for long options (common options)
+-a, --autosign        Create a transaction for offline autosigning (see
+                      ‘mmgen-autosign’). The removable device is mounted and
+                      unmounted automatically
 -A, --fee-adjust=  f  Adjust transaction fee by factor 'f' (see below)
 -A, --fee-adjust=  f  Adjust transaction fee by factor 'f' (see below)
 -B, --no-blank        Don't blank screen before displaying unspent outputs
 -B, --no-blank        Don't blank screen before displaying unspent outputs
 -c, --comment-file=f  Source the transaction's comment from file 'f'
 -c, --comment-file=f  Source the transaction's comment from file 'f'
@@ -83,6 +86,13 @@ cfg = Config(opts_data=opts_data)
 
 
 async def main():
 async def main():
 
 
+	if cfg.autosign:
+		from .tx.util import init_removable_device
+		from .autosign import Signable
+		asi = init_removable_device(cfg)
+		asi.do_mount()
+		Signable.automount_transaction(asi).check_create_ok()
+
 	from .tx import NewTX
 	from .tx import NewTX
 	tx1 = await NewTX(cfg=cfg,proto=cfg._proto)
 	tx1 = await NewTX(cfg=cfg,proto=cfg._proto)
 
 
@@ -95,6 +105,7 @@ async def main():
 		do_info  = cfg.info )
 		do_info  = cfg.info )
 
 
 	tx2.file.write(
 	tx2.file.write(
+		outdir                = asi.txauto_dir if cfg.autosign else None,
 		ask_write             = not cfg.yes,
 		ask_write             = not cfg.yes,
 		ask_overwrite         = not cfg.yes,
 		ask_overwrite         = not cfg.yes,
 		ask_write_default_yes = False)
 		ask_write_default_yes = False)

+ 59 - 10
mmgen/main_txsend.py

@@ -23,19 +23,31 @@ mmgen-txsend: Broadcast a transaction signed by 'mmgen-txsign' to the network
 import sys
 import sys
 
 
 from .cfg import gc,Config
 from .cfg import gc,Config
-from .util import async_run
+from .util import async_run, msg, suf, die, fmt_list
+from .fileutil import shred_file
 
 
 opts_data = {
 opts_data = {
-	'sets': [('yes', True, 'quiet', True)],
+	'sets': [
+		('yes', True, 'quiet', True),
+		('abort', True, 'autosign', True),
+	],
 	'text': {
 	'text': {
 		'desc':    f'Send a signed {gc.proj_name} cryptocoin transaction',
 		'desc':    f'Send a signed {gc.proj_name} cryptocoin transaction',
-		'usage':   '[opts] <signed transaction file>',
+		'usage':   '[opts] [signed transaction file]',
 		'options': """
 		'options': """
 -h, --help      Print this help message
 -h, --help      Print this help message
 --, --longhelp  Print help message for long options (common options)
 --, --longhelp  Print help message for long options (common options)
+-a, --autosign  Send an autosigned transaction created by ‘mmgen-txcreate
+                --autosign’.  The removable device is mounted and unmounted
+                automatically. The transaction file argument must be omitted
+                when using this option
+-A, --abort     Abort an unsent transaction created by ‘mmgen-txcreate
+                --autosign’ and delete it from the removable device.  The
+                transaction may be signed or unsigned.
 -d, --outdir= d Specify an alternate directory 'd' for output
 -d, --outdir= d Specify an alternate directory 'd' for output
 -q, --quiet     Suppress warnings; overwrite files without prompting
 -q, --quiet     Suppress warnings; overwrite files without prompting
--s, --status    Get status of a sent transaction
+-s, --status    Get status of a sent transaction (or the current transaction,
+                whether sent or unsent, when used with --autosign)
 -v, --verbose   Be more verbose
 -v, --verbose   Be more verbose
 -y, --yes       Answer 'yes' to prompts, suppress non-essential output
 -y, --yes       Answer 'yes' to prompts, suppress non-essential output
 """
 """
@@ -44,10 +56,41 @@ opts_data = {
 
 
 cfg = Config(opts_data=opts_data)
 cfg = Config(opts_data=opts_data)
 
 
+if cfg.autosign and cfg.outdir:
+	die(1, '--outdir cannot be used in combination with --autosign')
+
 if len(cfg._args) == 1:
 if len(cfg._args) == 1:
 	infile = cfg._args[0]
 	infile = cfg._args[0]
 	from .fileutil import check_infile
 	from .fileutil import check_infile
 	check_infile(infile)
 	check_infile(infile)
+elif not cfg._args and cfg.autosign:
+	from .tx.util import init_removable_device
+	from .autosign import Signable
+	asi = init_removable_device(cfg)
+	asi.do_mount()
+	si = Signable.automount_transaction(asi)
+	if cfg.abort:
+		files = si.get_abortable() # raises AutosignTXError if no unsent TXs available
+		from .ui import keypress_confirm
+		if keypress_confirm(
+				cfg,
+				'The following file{} will be securely deleted:\n{}\nOK?'.format(
+					suf(files),
+					fmt_list(map(str, files), fmt='col', indent='  '))):
+			for f in files:
+				msg(f'Shredding file ‘{f}’')
+				shred_file(f)
+			sys.exit(0)
+		else:
+			die(1, 'Exiting at user request')
+	elif cfg.status:
+		if si.unsent:
+			die(1, 'Transaction is unsent')
+		if si.unsigned:
+			die(1, 'Transaction is unsigned')
+	else:
+		infile = si.get_unsent()
+		cfg._util.qmsg(f'Got signed transaction file ‘{infile}’')
 else:
 else:
 	cfg._opts.usage()
 	cfg._opts.usage()
 
 
@@ -59,10 +102,14 @@ async def main():
 
 
 	from .tx import OnlineSignedTX, SentTX
 	from .tx import OnlineSignedTX, SentTX
 
 
-	tx = await OnlineSignedTX(
-		cfg        = cfg,
-		filename   = infile,
-		quiet_open = True)
+	if cfg.status and cfg.autosign:
+		tx = await si.get_last_created()
+	else:
+		tx = await OnlineSignedTX(
+			cfg        = cfg,
+			filename   = infile,
+			automount  = cfg.autosign,
+			quiet_open = True)
 
 
 	from .rpc import rpc_init
 	from .rpc import rpc_init
 	tx.rpc = await rpc_init(cfg,tx.proto)
 	tx.rpc = await rpc_init(cfg,tx.proto)
@@ -78,11 +125,13 @@ async def main():
 	if not cfg.yes:
 	if not cfg.yes:
 		tx.info.view_with_prompt('View transaction details?')
 		tx.info.view_with_prompt('View transaction details?')
 		if tx.add_comment(): # edits an existing comment, returns true if changed
 		if tx.add_comment(): # edits an existing comment, returns true if changed
-			tx.file.write(ask_write_default_yes=True)
+			if not cfg.autosign:
+				tx.file.write(ask_write_default_yes=True)
 
 
 	if await tx.send():
 	if await tx.send():
-		tx2 = await SentTX(cfg=cfg, data=tx.__dict__)
+		tx2 = await SentTX(cfg=cfg, data=tx.__dict__, automount=cfg.autosign)
 		tx2.file.write(
 		tx2.file.write(
+			outdir        = asi.txauto_dir if cfg.autosign else None,
 			ask_overwrite = False,
 			ask_overwrite = False,
 			ask_write     = False)
 			ask_write     = False)
 		tx2.print_contract_addr()
 		tx2.print_contract_addr()

+ 6 - 0
mmgen/proto/btc/tx/bump.py

@@ -16,6 +16,7 @@ from ....tx import bump as TxBase
 from ....util import msg
 from ....util import msg
 from .new import New
 from .new import New
 from .completed import Completed
 from .completed import Completed
+from .unsigned import AutomountUnsigned
 
 
 class Bump(Completed,New,TxBase.Bump):
 class Bump(Completed,New,TxBase.Bump):
 	desc = 'fee-bumped transaction'
 	desc = 'fee-bumped transaction'
@@ -52,3 +53,8 @@ class Bump(Completed,New,TxBase.Bump):
 				c = self.coin ))
 				c = self.coin ))
 			return False
 			return False
 		return ret
 		return ret
+
+class AutomountBump(Bump):
+	desc      = 'unsigned fee-bumped automount transaction'
+	ext       = AutomountUnsigned.ext
+	automount = AutomountUnsigned.automount

+ 6 - 0
mmgen/proto/btc/tx/online.py

@@ -77,3 +77,9 @@ class OnlineSigned(Signed,TxBase.OnlineSigned):
 
 
 class Sent(TxBase.Sent, OnlineSigned):
 class Sent(TxBase.Sent, OnlineSigned):
 	pass
 	pass
+
+class AutomountOnlineSigned(TxBase.AutomountOnlineSigned, OnlineSigned):
+	pass
+
+class AutomountSent(TxBase.AutomountSent, AutomountOnlineSigned):
+	pass

+ 3 - 0
mmgen/proto/btc/tx/signed.py

@@ -30,3 +30,6 @@ class Signed(Completed,TxBase.Signed):
 				Your transaction fee estimates will be inaccurate
 				Your transaction fee estimates will be inaccurate
 				Please re-create and re-sign the transaction using the option --vsize-adj={1/ratio:1.2f}
 				Please re-create and re-sign the transaction using the option --vsize-adj={1/ratio:1.2f}
 			""").strip())
 			""").strip())
+
+class AutomountSigned(TxBase.AutomountSigned, Signed):
+	pass

+ 4 - 1
mmgen/proto/btc/tx/unsigned.py

@@ -67,7 +67,7 @@ class Unsigned(Completed,TxBase.Unsigned):
 		try:
 		try:
 			self.update_serialized(ret['hex'])
 			self.update_serialized(ret['hex'])
 			from ....tx import SignedTX
 			from ....tx import SignedTX
-			new = await SignedTX(cfg=self.cfg,data=self.__dict__)
+			new = await SignedTX(cfg=self.cfg, data=self.__dict__, automount=self.automount)
 			tx_decoded = await self.rpc.call( 'decoderawtransaction', ret['hex'] )
 			tx_decoded = await self.rpc.call( 'decoderawtransaction', ret['hex'] )
 			new.compare_size_and_estimated_size(tx_decoded)
 			new.compare_size_and_estimated_size(tx_decoded)
 			new.coin_txid = CoinTxID(self.deserialized.txid)
 			new.coin_txid = CoinTxID(self.deserialized.txid)
@@ -81,3 +81,6 @@ class Unsigned(Completed,TxBase.Unsigned):
 				import sys,traceback
 				import sys,traceback
 				ymsg( '\n' + ''.join(traceback.format_exception(*sys.exc_info())) )
 				ymsg( '\n' + ''.join(traceback.format_exception(*sys.exc_info())) )
 			return False
 			return False
+
+class AutomountUnsigned(TxBase.AutomountUnsigned, Unsigned):
+	pass

+ 6 - 0
mmgen/proto/eth/tx/bump.py

@@ -33,3 +33,9 @@ class Bump(Completed,New,TxBase.Bump):
 
 
 class TokenBump(TokenCompleted,TokenNew,Bump):
 class TokenBump(TokenCompleted,TokenNew,Bump):
 	desc = 'fee-bumped transaction'
 	desc = 'fee-bumped transaction'
+
+class AutomountBump(Bump):
+	pass
+
+class TokenAutomountBump(TokenBump):
+	pass

+ 12 - 0
mmgen/proto/eth/tx/online.py

@@ -77,3 +77,15 @@ class Sent(TxBase.Sent, OnlineSigned):
 
 
 class TokenSent(TxBase.Sent, TokenOnlineSigned):
 class TokenSent(TxBase.Sent, TokenOnlineSigned):
 	pass
 	pass
+
+class AutomountOnlineSigned(TxBase.AutomountOnlineSigned, OnlineSigned):
+	pass
+
+class AutomountSent(TxBase.AutomountSent, AutomountOnlineSigned):
+	pass
+
+class TokenAutomountOnlineSigned(TxBase.AutomountOnlineSigned, TokenOnlineSigned):
+	pass
+
+class TokenAutomountSent(TxBase.AutomountSent, TokenAutomountOnlineSigned):
+	pass

+ 6 - 0
mmgen/proto/eth/tx/signed.py

@@ -54,3 +54,9 @@ class TokenSigned(TokenCompleted,Signed):
 	def parse_txfile_serialized_data(self):
 	def parse_txfile_serialized_data(self):
 		raise NotImplementedError(
 		raise NotImplementedError(
 			'Signed transaction files cannot be parsed offline, because tracking wallet is required!')
 			'Signed transaction files cannot be parsed offline, because tracking wallet is required!')
+
+class AutomountSigned(TxBase.AutomountSigned, Signed):
+	pass
+
+class TokenAutomountSigned(TxBase.AutomountSigned, TokenSigned):
+	pass

+ 7 - 1
mmgen/proto/eth/tx/unsigned.py

@@ -80,7 +80,7 @@ class Unsigned(Completed,TxBase.Unsigned):
 			await self.do_sign(keys[0].sec.wif)
 			await self.do_sign(keys[0].sec.wif)
 			msg('OK')
 			msg('OK')
 			from ....tx import SignedTX
 			from ....tx import SignedTX
-			return await SignedTX(cfg=self.cfg,data=self.__dict__)
+			return await SignedTX(cfg=self.cfg, data=self.__dict__, automount=self.automount)
 		except Exception as e:
 		except Exception as e:
 			msg(f'{e}: transaction signing failed!')
 			msg(f'{e}: transaction signing failed!')
 			return False
 			return False
@@ -107,3 +107,9 @@ class TokenUnsigned(TokenCompleted,Unsigned):
 				gasPrice  = o['gasPrice'],
 				gasPrice  = o['gasPrice'],
 				nonce     = o['nonce'])
 				nonce     = o['nonce'])
 		(self.serialized,self.coin_txid) = await t.txsign(tx_in,wif,o['from'],chain_id=o['chainId'])
 		(self.serialized,self.coin_txid) = await t.txsign(tx_in,wif,o['from'],chain_id=o['chainId'])
+
+class AutomountUnsigned(TxBase.AutomountUnsigned, Unsigned):
+	pass
+
+class TokenAutomountUnsigned(TxBase.AutomountUnsigned, TokenUnsigned):
+	pass

+ 8 - 1
mmgen/tx/__init__.py

@@ -52,6 +52,11 @@ def _get_cls_info(clsname,modname,args,kwargs):
 
 
 	kwargs['proto'] = proto
 	kwargs['proto'] = proto
 
 
+	if 'automount' in kwargs:
+		if kwargs['automount']:
+			clsname = 'Automount' + clsname
+		del kwargs['automount']
+
 	return ( kwargs['cfg'], proto, clsname, modname, kwargs )
 	return ( kwargs['cfg'], proto, clsname, modname, kwargs )
 
 
 
 
@@ -74,7 +79,9 @@ async def _get_obj_async( _clsname, _modname, *args, **kwargs ):
 	if proto and proto.tokensym and clsname in (
 	if proto and proto.tokensym and clsname in (
 			'New',
 			'New',
 			'OnlineSigned',
 			'OnlineSigned',
-			'Sent'):
+			'AutomountOnlineSigned',
+			'Sent',
+			'AutomountSent'):
 		from ..tw.ctl import TwCtl
 		from ..tw.ctl import TwCtl
 		kwargs['twctl'] = await TwCtl(cfg,proto)
 		kwargs['twctl'] = await TwCtl(cfg,proto)
 
 

+ 8 - 6
mmgen/tx/completed.py

@@ -54,15 +54,17 @@ class Completed(Base):
 		"""
 		"""
 		see twctl:import_token()
 		see twctl:import_token()
 		"""
 		"""
-		from .unsigned import Unsigned
-		from .online import Sent
-		for cls in (Unsigned, Sent):
+		from .unsigned import Unsigned, AutomountUnsigned
+		from .online import Sent, AutomountSent
+		for cls in (Unsigned, AutomountUnsigned, Sent, AutomountSent):
 			if ext == getattr(cls, 'ext'):
 			if ext == getattr(cls, 'ext'):
 				return cls
 				return cls
 
 
 		if proto.tokensym:
 		if proto.tokensym:
 			from .online import OnlineSigned as Signed
 			from .online import OnlineSigned as Signed
+			from .online import AutomountOnlineSigned as AutomountSigned
 		else:
 		else:
-			from .signed import Signed
-		if ext == Signed.ext:
-			return Signed
+			from .signed import Signed, AutomountSigned
+		for cls in (Signed, AutomountSigned):
+			if ext == getattr(cls, 'ext'):
+				return cls

+ 1 - 1
mmgen/tx/new.py

@@ -436,7 +436,7 @@ class New(Base):
 		self.cfg._util.qmsg('Transaction successfully created')
 		self.cfg._util.qmsg('Transaction successfully created')
 
 
 		from . import UnsignedTX
 		from . import UnsignedTX
-		new = UnsignedTX(cfg=self.cfg,data=self.__dict__)
+		new = UnsignedTX(cfg=self.cfg, data=self.__dict__, automount=self.cfg.autosign)
 
 
 		if not self.cfg.yes:
 		if not self.cfg.yes:
 			new.info.view_with_prompt('View transaction details?')
 			new.info.view_with_prompt('View transaction details?')

+ 8 - 1
mmgen/tx/online.py

@@ -12,7 +12,7 @@
 tx.online: online signed transaction class
 tx.online: online signed transaction class
 """
 """
 
 
-from .signed import Signed
+from .signed import Signed, AutomountSigned
 
 
 class OnlineSigned(Signed):
 class OnlineSigned(Signed):
 
 
@@ -31,6 +31,13 @@ class OnlineSigned(Signed):
 			expect  = 'YES' if self.cfg.quiet or self.cfg.yes else 'YES, I REALLY WANT TO DO THIS' )
 			expect  = 'YES' if self.cfg.quiet or self.cfg.yes else 'YES, I REALLY WANT TO DO THIS' )
 		msg('Sending transaction')
 		msg('Sending transaction')
 
 
+class AutomountOnlineSigned(AutomountSigned, OnlineSigned):
+	pass
+
 class Sent(OnlineSigned):
 class Sent(OnlineSigned):
 	desc = 'sent transaction'
 	desc = 'sent transaction'
 	ext = 'subtx'
 	ext = 'subtx'
+
+class AutomountSent(AutomountOnlineSigned):
+	desc = 'sent automount transaction'
+	ext = 'asubtx'

+ 2 - 2
mmgen/tx/sign.py

@@ -112,8 +112,8 @@ def _pop_matching_fns(args,cmplist): # strips found args
 	return list(reversed([args.pop(args.index(a)) for a in reversed(args) if get_extension(a) in cmplist]))
 	return list(reversed([args.pop(args.index(a)) for a in reversed(args) if get_extension(a) in cmplist]))
 
 
 def get_tx_files(cfg, args):
 def get_tx_files(cfg, args):
-	from .unsigned import Unsigned
-	ret = _pop_matching_fns(args,[Unsigned.ext])
+	from .unsigned import Unsigned, AutomountUnsigned
+	ret = _pop_matching_fns(args, [(AutomountUnsigned if cfg.autosign else Unsigned).ext])
 	if not ret:
 	if not ret:
 		die(1,'You must specify a raw transaction file!')
 		die(1,'You must specify a raw transaction file!')
 	return ret
 	return ret

+ 4 - 0
mmgen/tx/signed.py

@@ -18,3 +18,7 @@ class Signed(Completed):
 	desc = 'signed transaction'
 	desc = 'signed transaction'
 	ext  = 'sigtx'
 	ext  = 'sigtx'
 	signed = True
 	signed = True
+
+class AutomountSigned(Signed):
+	desc = 'signed automount transaction'
+	ext  = 'asigtx'

+ 6 - 0
mmgen/tx/unsigned.py

@@ -18,6 +18,7 @@ from ..util import remove_dups
 class Unsigned(Completed):
 class Unsigned(Completed):
 	desc = 'unsigned transaction'
 	desc = 'unsigned transaction'
 	ext  = 'rawtx'
 	ext  = 'rawtx'
+	automount = False
 
 
 	def delete_attrs(self,desc,attr):
 	def delete_attrs(self,desc,attr):
 		for e in getattr(self,desc):
 		for e in getattr(self,desc):
@@ -28,3 +29,8 @@ class Unsigned(Completed):
 		return remove_dups(
 		return remove_dups(
 			(e.mmid.sid for e in getattr(self,desc) if e.mmid),
 			(e.mmid.sid for e in getattr(self,desc) if e.mmid),
 			quiet = True )
 			quiet = True )
+
+class AutomountUnsigned(Unsigned):
+	desc = 'unsigned automount transaction'
+	ext  = 'arawtx'
+	automount = True

+ 35 - 0
mmgen/tx/util.py

@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2024 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
+
+"""
+tx.util: transaction utilities
+"""
+
+def get_autosign_obj(cfg):
+	from ..cfg import Config
+	from ..autosign import Autosign
+	return Autosign(
+		Config({
+			'mountpoint': cfg.autosign_mountpoint,
+			'test_suite': cfg.test_suite,
+			'test_suite_root_pfx': cfg.test_suite_root_pfx,
+			'coins': cfg.coin,
+			'online': True, # used only in online environment (txcreate, txsend)
+		})
+	)
+
+def init_removable_device(cfg):
+	asi = get_autosign_obj(cfg)
+	if not asi.get_insert_status():
+		from ..util import die
+		die(1, 'Removable device not present!')
+	import atexit
+	atexit.register(lambda: asi.do_umount())
+	return asi

+ 4 - 0
test/cmdtest_py_d/cfg.py

@@ -34,6 +34,8 @@ cmd_groups_dfl = {
 	'output':           ('CmdTestOutput',{'modname':'misc','full_data':True}),
 	'output':           ('CmdTestOutput',{'modname':'misc','full_data':True}),
 	'autosign_clean':   ('CmdTestAutosignClean', {'modname':'autosign'}),
 	'autosign_clean':   ('CmdTestAutosignClean', {'modname':'autosign'}),
 	'autosign':         ('CmdTestAutosign',{}),
 	'autosign':         ('CmdTestAutosign',{}),
+	'autosign_automount': ('CmdTestAutosignAutomount', {'modname':'automount'}),
+	'autosign_eth':     ('CmdTestAutosignETH', {'modname':'automount_eth'}),
 	'regtest':          ('CmdTestRegtest',{}),
 	'regtest':          ('CmdTestRegtest',{}),
 #	'chainsplit':       ('CmdTestChainsplit',{}),
 #	'chainsplit':       ('CmdTestChainsplit',{}),
 	'ethdev':           ('CmdTestEthdev',{}),
 	'ethdev':           ('CmdTestEthdev',{}),
@@ -221,6 +223,8 @@ cfgs = { # addr_idx_lists (except 31,32,33,34) must contain exactly 8 addresses
 	'39': {}, # xmr_autosign
 	'39': {}, # xmr_autosign
 	'40': {}, # cfgfile
 	'40': {}, # cfgfile
 	'41': {}, # opts
 	'41': {}, # opts
+	'49': {}, # autosign_automount
+	'59': {}, # autosign_eth
 	'99': {}, # dummy
 	'99': {}, # dummy
 }
 }
 
 

+ 259 - 0
test/cmdtest_py_d/ct_automount.py

@@ -0,0 +1,259 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2024 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_py_d.ct_automount: autosigning with automount tests for the cmdtest.py test suite
+"""
+import os, time
+from pathlib import Path
+
+from .ct_autosign import CmdTestAutosignThreaded
+from .ct_regtest import CmdTestRegtest, rt_pw
+from .common import get_file_with_ext
+from ..include.common import cfg
+
+class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest):
+	'automounted transacting operations via regtest mode'
+
+	networks = ('btc', 'bch', 'ltc')
+	tmpdir_nums = [49]
+
+	rtFundAmt = None # pylint
+	rt_data = {
+		'rtFundAmt': {'btc':'500', 'bch':'500', 'ltc':'5500'},
+	}
+
+	cmd_group = (
+		('setup',                            'regtest mode setup'),
+		('walletgen_alice',                  'wallet generation (Alice)'),
+		('addrgen_alice',                    'address generation (Alice)'),
+		('addrimport_alice',                 'importing Alice’s addresses'),
+		('fund_alice',                       'funding Alice’s wallet'),
+		('generate',                         'mining a block'),
+		('alice_bal1',                       'checking Alice’s balance'),
+		('alice_txcreate1',                  'creating a transaction'),
+		('alice_txcreate_bad_have_unsigned', 'creating the transaction again (error)'),
+		('copy_wallet',                      'copying Alice’s wallet'),
+		('alice_run_autosign_setup',         'running ‘autosign setup’ (with default wallet)'),
+		('autosign_start_thread',            'starting autosign wait loop'),
+		('alice_txstatus1',                  'getting transaction status (unsigned)'),
+		('alice_txstatus2',                  'getting transaction status (unsent)'),
+		('alice_txsend1',                    'sending a transaction, editing comment'),
+		('alice_txstatus3',                  'getting transaction status (in mempool)'),
+		('alice_txsend_bad_no_unsent',       'sending the transaction again (error)'),
+		('generate',                         'mining a block'),
+		('alice_txstatus4',                  'getting transaction status (one confirmation)'),
+		('alice_txcreate2',                  'creating a transaction'),
+		('alice_txsend_abort1',              'aborting the transaction (raw only)'),
+		('alice_txsend_abort2',              'aborting the transaction again (error)'),
+		('alice_txcreate3',                  'creating a transaction'),
+		('alice_txsend_abort3',              'aborting the transaction (user exit)'),
+		('alice_txsend_abort4',              'aborting the transaction (raw + signed)'),
+		('alice_txsend_abort5',              'aborting the transaction again (error)'),
+		('generate',                         'mining a block'),
+		('alice_txcreate4',                  'creating a transaction'),
+		('alice_txbump1',                    'bumping the unsigned transaction (error)'),
+		('alice_txbump2',                    'bumping the unsent transaction (error)'),
+		('alice_txsend2',                    'sending the transaction'),
+		('alice_txbump3',                    'bumping the transaction'),
+		('alice_txsend3',                    'sending the bumped transaction'),
+		('autosign_kill_thread',             'stopping autosign wait loop'),
+		('stop',                             'stopping regtest daemon'),
+		('txview',                           'viewing transactions'),
+	)
+
+	def __init__(self, trunner, cfgs, spawn):
+
+		self.coins = [cfg.coin.lower()]
+
+		CmdTestAutosignThreaded.__init__(self, trunner, cfgs, spawn)
+		CmdTestRegtest.__init__(self, trunner, cfgs, spawn)
+
+		if trunner == None:
+			return
+
+		self.opts.append('--alice')
+
+	def _alice_txcreate(self, chg_addr, opts=[], exit_val=0):
+		self.insert_device_online()
+		sid = self._user_sid('alice')
+		t = self.spawn(
+			'mmgen-txcreate',
+			opts
+			+ ['--alice', '--autosign']
+			+ [f'{self.burn_addr},1.23456', f'{sid}:{chg_addr}'])
+		if exit_val:
+			t.read()
+			self.remove_device_online()
+			t.req_exit_val = exit_val
+			return t
+		t = self.txcreate_ui_common(
+			t,
+			inputs          = '1',
+			interactive_fee = '32s',
+			file_desc       = 'Unsigned automount transaction')
+		t.read()
+		self.remove_device_online()
+		return t
+
+	def alice_txcreate1(self):
+		return self._alice_txcreate(chg_addr='C:5')
+
+	def alice_txcreate2(self):
+		return self._alice_txcreate(chg_addr='L:5')
+
+	alice_txcreate3 = alice_txcreate2
+
+	def alice_txcreate4(self):
+		if cfg.coin == 'BCH':
+			return 'skip'
+		return self._alice_txcreate(chg_addr='L:4')
+
+	def _alice_txsend_abort(self, err=False, user_exit=False, del_expect=[]):
+		self.insert_device_online()
+		t = self.spawn('mmgen-txsend', ['--quiet', '--abort'])
+		if err:
+			t.expect('No unsent transactions')
+			t.req_exit_val = 2
+		else:
+			t.expect('(y/N): ', 'n' if user_exit else 'y')
+			if user_exit:
+				t.expect('Exiting at user request')
+				t.req_exit_val = 1
+			else:
+				for pat in del_expect:
+					t.expect(pat, regex=True)
+		self.remove_device_online()
+		return t
+
+	def alice_txsend_abort1(self):
+		return self._alice_txsend_abort(del_expect=['Shredding .*arawtx'])
+
+	def alice_txsend_abort2(self):
+		return self._alice_txsend_abort(err=True)
+
+	def alice_txsend_abort3(self):
+		return self._alice_txsend_abort(user_exit=True)
+
+	def alice_txsend_abort4(self):
+		self._wait_signed('transaction')
+		return self._alice_txsend_abort(del_expect=[r'Shredding .*arawtx', r'Shredding .*asigtx'])
+
+	alice_txsend_abort5 = alice_txsend_abort2
+
+	def alice_txcreate_bad_have_unsigned(self):
+		return self._alice_txcreate(chg_addr='C:5', exit_val=2)
+
+	def copy_wallet(self):
+		self.spawn('', msg_only=True)
+		if cfg.coin == 'BTC':
+			return 'skip_msg'
+		src  = Path(self.tr.data_dir, 'regtest', cfg.coin.lower(), 'alice')
+		dest = Path(self.tr.data_dir, 'regtest', 'btc', 'alice')
+		dest.mkdir(parents=True, exist_ok=True)
+		wf = Path(get_file_with_ext(src, 'mmdat')).absolute()
+		link_path = dest / wf.name
+		if not link_path.exists():
+			link_path.symlink_to(wf)
+		return 'ok'
+
+	def alice_run_autosign_setup(self):
+		self.insert_device()
+		t = self.run_setup(mn_type='default', use_dfl_wallet=True, passwd=rt_pw)
+		t.read()
+		self.remove_device()
+		return t
+
+	def alice_txsend1(self):
+		return self._alice_txsend('This one’s worth a comment', no_wait=True)
+
+	def alice_txsend2(self):
+		if cfg.coin == 'BCH':
+			return 'skip'
+		return self._alice_txsend()
+
+	def alice_txsend3(self):
+		if cfg.coin == 'BCH':
+			return 'skip'
+		return self._alice_txsend()
+
+	def _alice_txstatus(self, expect, exit_val=None):
+		self.insert_device_online()
+		t = self.spawn('mmgen-txsend', ['--alice', '--autosign', '--status', '--verbose'])
+		t.expect(expect)
+		self.remove_device_online()
+		if exit_val:
+			t.req_exit_val = exit_val
+		return t
+
+	def alice_txstatus1(self):
+		return self._alice_txstatus('unsigned', 1)
+
+	def alice_txstatus2(self):
+		self._wait_signed('transaction')
+		return self._alice_txstatus('unsent', 1)
+
+	def alice_txstatus3(self):
+		return self._alice_txstatus('in mempool')
+
+	def alice_txstatus4(self):
+		return self._alice_txstatus('1 confirmation')
+
+	def _alice_txsend(self, comment=None, no_wait=False):
+		if not no_wait:
+			self._wait_signed('transaction')
+		self.insert_device_online()
+		t = self.spawn('mmgen-txsend', ['--quiet', '--autosign'])
+		t.view_tx('t')
+		t.do_comment(comment)
+		self._do_confirm_send(t, quiet=True)
+		t.written_to_file('Sent automount transaction')
+		t.read()
+		self.remove_device_online()
+		return t
+
+	def alice_txsend_bad_no_unsent(self):
+		self.insert_device_online()
+		t = self.spawn('mmgen-txsend', ['--quiet', '--autosign'])
+		t.expect('No unsent transactions')
+		t.read()
+		t.req_exit_val = 2
+		self.remove_device_online()
+		return t
+
+	def _alice_txbump(self, bad_tx_desc=None):
+		if cfg.coin == 'BCH':
+			return 'skip'
+		self.insert_device_online()
+		t = self.spawn('mmgen-txbump', ['--autosign'])
+		if bad_tx_desc:
+			t.expect('Only sent transactions')
+			t.expect(bad_tx_desc)
+			t.req_exit_val = 1
+		else:
+			t.expect(f'to deduct the fee from .* change output\): ', '\n', regex=True)
+			t.expect(r'(Y/n): ', 'y')  # output OK?
+			t.expect('transaction fee: ', '200s\n')
+			t.expect(r'(Y/n): ', 'y')  # fee OK?
+			t.expect(r'(y/N): ', '\n') # add comment?
+			t.expect(r'(y/N): ', 'y')  # save?
+		t.read()
+		self.remove_device_online()
+		return t
+
+	def alice_txbump1(self):
+		return self._alice_txbump(bad_tx_desc='unsigned transaction')
+
+	def alice_txbump2(self):
+		self._wait_signed('transaction')
+		return self._alice_txbump(bad_tx_desc='unsent transaction')
+
+	def alice_txbump3(self):
+		return self._alice_txbump()

+ 141 - 0
test/cmdtest_py_d/ct_automount_eth.py

@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2024 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_py_d.ct_automount_eth: Ethereum automount autosigning tests for the cmdtest.py test suite
+"""
+import os, re
+
+from .ct_autosign import CmdTestAutosignThreaded
+from .ct_ethdev import CmdTestEthdev, parity_devkey_fn
+from .common import dfl_words_file
+from ..include.common import cfg
+
+class CmdTestAutosignETH(CmdTestAutosignThreaded, CmdTestEthdev):
+	'automounted transacting operations for Ethereum via ethdev'
+
+	networks = ('eth', 'etc')
+	tmpdir_nums = [59]
+
+	cmd_group = (
+		('setup',                  f'dev mode tests for coin {cfg.coin} (start daemon)'),
+		('addrgen',                'generating addresses'),
+		('addrimport',             'importing addresses'),
+		('addrimport_dev_addr',    "importing dev faucet address 'Ox00a329c..'"),
+		('fund_dev_address',       'funding the default (Parity dev) address'),
+		('fund_mmgen_address',     'funding an MMGen address'),
+		('create_tx',              'creating a transaction'),
+		('run_autosign_setup',     'running ‘autosign setup’'),
+		('autosign_start_thread',  'starting autosign wait loop'),
+		('send_tx',                'sending the transaction'),
+		('token_compile1',         'compiling ERC20 token #1'),
+		('token_deploy1a',         'deploying ERC20 token #1 (SafeMath)'),
+		('token_deploy1b',         'deploying ERC20 token #1 (Owned)'),
+		('token_deploy1c',         'deploying ERC20 token #1 (Token)'),
+		('tx_status2',             'getting the transaction status'),
+		('token_fund_user',        'transferring token funds from dev to user'),
+		('token_addrgen_addr1',    'generating token addresses'),
+		('token_addrimport_addr1', 'importing token addresses using token address (MM1)'),
+		('token_bal1',             f'the {cfg.coin} balance and token balance'),
+		('create_token_tx',        'creating a token transaction'),
+		('send_token_tx',          'sending a token transaction'),
+		('token_bal2',             f'the {cfg.coin} balance and token balance'),
+		('autosign_kill_thread',   'stopping autosign wait loop'),
+		('txview',                 'viewing transactions'),
+		('stop',                   'stopping daemon'),
+	)
+
+	def __init__(self, trunner, cfgs, spawn):
+
+		self.coins = [cfg.coin.lower()]
+
+		CmdTestAutosignThreaded.__init__(self, trunner, cfgs, spawn)
+		CmdTestEthdev.__init__(self, trunner, cfgs, spawn)
+
+		if trunner == None:
+			return
+
+		self.opts.append('--alice')
+
+	def fund_mmgen_address(self):
+		keyfile = os.path.join(self.tmpdir, parity_devkey_fn)
+		t = self.spawn(
+			'mmgen-txdo',
+			['--quiet']
+			+ [f'--keys-from-file={keyfile}']
+			+ ['--fee=40G', '98831F3A:E:1,123.456', dfl_words_file],
+		)
+		t.expect('efresh balance:\b', 'q')
+		t.expect('from: ', '10')
+		t.expect('(Y/n): ', 'y')
+		t.expect('(Y/n): ', 'y')
+		t.expect('(y/N): ', 'n')
+		t.expect('view: ', 'n')
+		t.expect('confirm: ', 'YES')
+		return t
+
+	def create_tx(self):
+		self.insert_device_online()
+		t = self.txcreate(
+			args = ['--autosign', '98831F3A:E:11,54.321'],
+			menu = [],
+			acct = '1')
+		t.read()
+		self.remove_device_online()
+		return t
+
+	def run_autosign_setup(self):
+		self.insert_device()
+		t = self.run_setup(mn_type='bip39', mn_file='test/ref/98831F3A.bip39', use_dfl_wallet=None)
+		t.read()
+		self.remove_device()
+		return t
+
+	def send_tx(self, add_args=[]):
+		self._wait_signed('transaction')
+		self.insert_device_online()
+		t = self.spawn('mmgen-txsend', ['--quiet', '--autosign'] + add_args)
+		t.view_tx('t')
+		t.expect('(y/N): ', 'n')
+		self._do_confirm_send(t, quiet=True)
+		t.written_to_file('Sent automount transaction')
+		self.remove_device_online()
+		return t
+
+	def token_fund_user(self):
+		return self.token_transfer_ops(op='do_transfer', num_tokens=1)
+
+	def token_addrgen_addr1(self):
+		return self.token_addrgen(num_tokens=1)
+
+	def token_bal1(self):
+		return self.token_bal(pat=r':E:11\s+1000\s+54\.321\s+')
+
+	def token_bal2(self):
+		return self.token_bal(pat=r':E:11\s+998.76544\s+54.318\d+\s+.*:E:12\s+1\.23456\s+')
+
+	def token_bal(self, pat):
+		t = self.spawn('mmgen-tool', ['--quiet', '--token=mm1', 'twview', 'wide=1'])
+		text = t.read(strip_color=True)
+		assert re.search(pat, text, re.DOTALL), f'output failed to match regex {pat}'
+		return t
+
+	def create_token_tx(self):
+		self.insert_device_online()
+		t = self.token_txcreate(
+			args      = ['--autosign', '98831F3A:E:12,1.23456'],
+			token     = 'MM1',
+			file_desc = 'Unsigned automount transaction')
+		t.read()
+		self.remove_device_online()
+		return t
+
+	def send_token_tx(self):
+		return self.send_tx(add_args=['--token=MM1'])

+ 30 - 4
test/cmdtest_py_d/ct_autosign.py

@@ -209,6 +209,16 @@ class CmdTestAutosignClean(CmdTestAutosignBase):
 			):
 			):
 				(self.asi.tx_dir / fn).touch()
 				(self.asi.tx_dir / fn).touch()
 
 
+			for fn in (
+				'a.arawtx', 'a.asigtx', 'a.asubtx',
+				'b.arawtx', 'b.asigtx',
+				'c.asubtx',
+				'd.arawtx', 'd.asubtx',
+				'e.arawtx',
+				'f.asigtx', 'f.asubtx',
+			):
+				(self.asi.txauto_dir / fn).touch()
+
 			for fn in (
 			for fn in (
 				'a.rawmsg.json', 'a.sigmsg.json',
 				'a.rawmsg.json', 'a.sigmsg.json',
 				'b.rawmsg.json',
 				'b.rawmsg.json',
@@ -270,6 +280,7 @@ class CmdTestAutosignClean(CmdTestAutosignBase):
 
 
 		chk_non_xmr = """
 		chk_non_xmr = """
 			tx:          a.sigtx b.sigtx c.rawtx d.sigtx
 			tx:          a.sigtx b.sigtx c.rawtx d.sigtx
+			txauto:      a.asubtx b.asigtx c.asubtx d.asubtx e.arawtx f.asubtx
 			msg:         a.sigmsg.json b.rawmsg.json c.sigmsg.json d.sigmsg.json
 			msg:         a.sigmsg.json b.rawmsg.json c.sigmsg.json d.sigmsg.json
 		"""
 		"""
 		chk_xmr = """
 		chk_xmr = """
@@ -281,10 +292,10 @@ class CmdTestAutosignClean(CmdTestAutosignBase):
 		shred_count = 0
 		shred_count = 0
 
 
 		if not self.asi.xmr_only:
 		if not self.asi.xmr_only:
-			for k in ('tx_dir','msg_dir'):
+			for k in ('tx_dir', 'txauto_dir', 'msg_dir'):
 				shutil.rmtree(getattr(self.asi, k))
 				shutil.rmtree(getattr(self.asi, k))
 			chk += chk_non_xmr.rstrip()
 			chk += chk_non_xmr.rstrip()
-			shred_count += 4
+			shred_count += 9
 
 
 		if self.asi.have_xmr:
 		if self.asi.have_xmr:
 			shutil.rmtree(self.asi.xmr_dir)
 			shutil.rmtree(self.asi.xmr_dir)
@@ -371,6 +382,21 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
 	def do_umount_online(self, *args, **kwargs):
 	def do_umount_online(self, *args, **kwargs):
 		return self._mount_ops('asi_online', 'do_umount', *args, **kwargs)
 		return self._mount_ops('asi_online', 'do_umount', *args, **kwargs)
 
 
+	async def txview(self):
+		self.spawn('', msg_only=True)
+		self.do_mount()
+		src = Path(self.asi.txauto_dir)
+		from mmgen.tx import CompletedTX
+		txs = sorted(
+			[await CompletedTX(cfg=cfg, filename=path, quiet_open=True) for path in sorted(src.iterdir())],
+			key = lambda x: x.timestamp)
+		for tx in txs:
+			imsg(blue(f'\nViewing ‘{tx.infile.name}’:'))
+			out = tx.info.format(terse=True)
+			imsg(indent(out, indent='  '))
+		self.do_umount()
+		return 'ok'
+
 class CmdTestAutosign(CmdTestAutosignBase):
 class CmdTestAutosign(CmdTestAutosignBase):
 	'autosigning transactions for all supported coins'
 	'autosigning transactions for all supported coins'
 	coins           = ['btc','bch','ltc','eth']
 	coins           = ['btc','bch','ltc','eth']
@@ -754,9 +780,9 @@ class CmdTestAutosignLive(CmdTestAutosignBTC):
 
 
 		def prompt_insert_sign(t):
 		def prompt_insert_sign(t):
 			omsg(orange(insert_msg))
 			omsg(orange(insert_msg))
-			t.expect(f'{self.tx_count} transactions signed')
+			t.expect(f'{self.tx_count} non-automount transactions signed')
 			if self.bad_tx_count:
 			if self.bad_tx_count:
-				t.expect(f'{self.bad_tx_count} transactions failed to sign')
+				t.expect(f'{self.bad_tx_count} non-automount transactions failed to sign')
 			t.expect('Waiting')
 			t.expect('Waiting')
 
 
 		if led_opts:
 		if led_opts:

+ 10 - 3
test/test-release.d/cfg.sh

@@ -153,8 +153,15 @@ init_tests() {
 	d_etc="operations for Ethereum Classic using devnet"
 	d_etc="operations for Ethereum Classic using devnet"
 	t_etc="parity $cmdtest_py --coin=etc ethdev"
 	t_etc="parity $cmdtest_py --coin=etc ethdev"
 
 
-	d_autosign="transaction and message autosigning"
-	t_autosign="- $cmdtest_py autosign autosign_clean"
+	d_autosign="transaction autosigning with automount"
+	t_autosign="
+		- $cmdtest_py autosign autosign_clean autosign_automount
+		- $cmdtest_py --coin=bch autosign_automount
+		s $cmdtest_py --coin=ltc autosign_automount
+		- $cmdtest_py --coin=eth autosign_eth
+		s $cmdtest_py --coin=etc autosign_eth
+	"
+	[ "$FAST" ]  && t_autosign_skip='s'
 
 
 	d_autosign_btc="transaction and message autosigning (Bitcoin only)"
 	d_autosign_btc="transaction and message autosigning (Bitcoin only)"
 	t_autosign_btc="- $cmdtest_py autosign_btc"
 	t_autosign_btc="- $cmdtest_py autosign_btc"
@@ -164,7 +171,7 @@ init_tests() {
 
 
 	d_btc="overall operations with emulated RPC data (Bitcoin)"
 	d_btc="overall operations with emulated RPC data (Bitcoin)"
 	t_btc="
 	t_btc="
-		- $cmdtest_py --exclude regtest,autosign,autosign_clean,ref_altcoin
+		- $cmdtest_py --exclude regtest,autosign,autosign_clean,autosign_automount,ref_altcoin
 		- $cmdtest_py --segwit
 		- $cmdtest_py --segwit
 		- $cmdtest_py --segwit-random
 		- $cmdtest_py --segwit-random
 		- $cmdtest_py --bech32
 		- $cmdtest_py --bech32